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
@@ -1,17 +1,18 @@
1
1
  #!/usr/bin/env python3
2
2
  # /// script
3
- # requires-python = ">=3.10"
3
+ # requires-python = ">=3.11"
4
4
  # ///
5
5
  """SKF Rewrite Skill Name — field-scoped, meaning-independent rename transforms.
6
6
 
7
7
  Performs the deterministic in-file name substitutions the rename workflow needs,
8
8
  so the LLM never hand-edits JSON or eyeballs "only within the frontmatter". Each
9
- transform is scoped to a single field / region and applied by exact-key or
10
- anchored-pattern match, then written atomically (stage to <target>.skf-tmp,
11
- fsync, rename) in the same call — a process kill mid-rewrite leaves the original
12
- file intact.
9
+ transform is scoped to a single field / region (for the context snippet, the
10
+ places its template writes the name) and applied by exact-key, anchored-pattern
11
+ or name-token match, then written atomically (stage to <target>.skf-tmp, fsync,
12
+ rename) in the same call — a process kill mid-rewrite leaves the original file
13
+ intact.
13
14
 
14
- Four file kinds (one per --kind), each doing exactly one field-scoped edit:
15
+ Four file kinds (one per --kind), each editing only the skill's name:
15
16
 
16
17
  skill-frontmatter Replace the top-level `name:` value inside the YAML
17
18
  frontmatter block ONLY (between the leading `---` and the
@@ -23,31 +24,99 @@ Four file kinds (one per --kind), each doing exactly one field-scoped edit:
23
24
  metadata-json JSON round-trip: parse, set `name` = <new-name>, re-emit
24
25
  with indent=2. Key order is preserved (dict insertion
25
26
  order); no manual string surgery, so no risk of reordering
26
- or dropping keys.
27
-
28
- provenance-json Same JSON round-trip, but the field is `skill_name`.
29
-
30
- context-snippet Rewrite the display header `[<old> v...]` -> `[<new> v...]`
31
- (version suffix preserved) and every `root:` path. Root
32
- rewrite parses `root: {prefix}{old}/`, preserves the prefix
33
- verbatim, and swaps the trailing `{old}/` segment for
34
- `{new}/` — so any IDE prefix (.claude/skills/,
27
+ or dropping keys. With --moved-folder FOLDER (repeatable),
28
+ every string value that is a path into FOLDER/<old>/ is
29
+ pointed at FOLDER/<new>/, and the package folder
30
+ FOLDER/<old>/<version>/<old>/ at FOLDER/<new>/<version>/<new>/:
31
+ the rename moves those folders, and skf-verify-no-trace.py
32
+ counts the old name in such a path as a leftover. A path
33
+ counts only where it begins, at the start of the value or
34
+ of a word in it, spelled as FOLDER itself or as its last
35
+ components (a path from the project root, such as
36
+ forge-data/<old>/...); the old name must be a whole path
37
+ segment there, by the verifier's token rule. So a URL, a
38
+ path into another folder and a path that only holds
39
+ FOLDER's name further in stay. The values of the keys that
40
+ name the upstream source or a file in it (UPSTREAM_KEYS:
41
+ the verifier's SOURCE_FACT_KEYS, `source_file` and
42
+ `co_import_files`) never change, even when one holds such
43
+ a path: the rename does not move the source. Keys, every
44
+ other value and file names (`test-report-<old>.md`) stay.
45
+
46
+ provenance-json Same JSON round-trip, but the field is `skill_name`; takes
47
+ --moved-folder the same way.
48
+
49
+ context-snippet Rewrite the name where every SKF snippet template writes
50
+ it, so a snippet that follows a template leaves nothing
51
+ for skf-verify-no-trace.py, which counts any mention there
52
+ as a leftover: the display header `[<old> v...]`, the first
53
+ word of the `|IMPORTANT:` line and its `writing <old> code`
54
+ phrase. In those slots a complete skill-name token of the
55
+ old name becomes the new name, matched by the verifier's
56
+ own rule (_name_token_re, a pinned copy). Nothing else in
57
+ the snippet changes: a field label (`root:`, `|api:`), a
58
+ fixed word of the template ("training data", "SKILL.md")
59
+ or the library's own content that is the old name stays,
60
+ and the verifier then stops the rename rather than let it
61
+ commit a changed snippet. Each `root:` path parses as
62
+ `root: {prefix}{old}/`: the prefix is kept verbatim and
63
+ the trailing `{old}/` segment becomes
64
+ `{new}/`, so any IDE prefix (.claude/skills/,
35
65
  .windsurf/skills/, draft skills/, ...) is handled without
36
- enumeration and an old name that is a substring of the
37
- prefix is left alone. The legacy nested draft form
66
+ enumeration and a prefix that contains the old name is
67
+ never changed. The legacy nested draft form
38
68
  `root: skills/{old}/active/{old}/` is flattened to
39
69
  `root: skills/{new}/`.
40
70
 
71
+ Batch mode (--skill-group, no target): the rename workflow's whole in-place
72
+ pass over a copied skill folder in one call, as execute.md section 2 runs it.
73
+ For each version in --versions, in the order given, it moves the package
74
+ folder <skill-group>/<version>/<old>/ to <skill-group>/<version>/<new>/; a
75
+ version without that folder (a manifest version with no folder, or a version
76
+ folder an interrupted run left without a package) is skipped and named in
77
+ `package_warnings`. Then it rewrites, for each version it moved, the
78
+ package's SKILL.md (skill-frontmatter), metadata.json (metadata-json) and
79
+ context-snippet.md (context-snippet) and, with --forge-group, every
80
+ version's <forge-group>/<version>/provenance-map.json (provenance-json),
81
+ package or not, each as the single-file mode does; --moved-folder applies to
82
+ the two JSON kinds. A file that is not there is listed in `missing_files`,
83
+ never a failure. The first move or rewrite that fails stops the batch:
84
+ `status` is "error" and `error` names its `stage` ("inner-rename" or
85
+ "rewrite"), `path` and `message`; the caller rolls the copied folders back.
86
+
87
+ The batch prints one JSON object on stdout, and writes it to --result-to
88
+ FILE too (atomically), the record the rename's report reads back:
89
+
90
+ {"status": "ok"|"error", "old_name", "new_name", "skill_group",
91
+ "forge_group": str|null, "renamed_versions": [...],
92
+ "files_rewritten": [{"kind", "path"}],
93
+ "counts": {"skill-frontmatter": n, "metadata-json": n,
94
+ "context-snippet": n, "provenance-json": n},
95
+ "package_warnings": [...], "missing_files": [...],
96
+ "error": null | {"stage", "path", "message"}}
97
+
98
+ `counts` tallies files_rewritten by kind. A --result-to file that cannot be
99
+ written fails a batch that had not failed yet: `error.stage` is "record",
100
+ with that file as its `path`, so the caller rolls back as for a rewrite.
101
+
41
102
  Exit codes:
42
- 0 success (file processed; written iff content changed)
43
- 1 user error (bad args, invalid new name, target not found)
44
- 2 operation failure (unparseable structure, atomic write failed)
103
+ 0 success (file processed, written iff content changed; or batch done)
104
+ 1 user error (bad args, --moved-folder with a kind other than the two JSON
105
+ kinds, invalid new name, target not found)
106
+ 2 operation failure (unparseable structure, atomic write failed, or a
107
+ batch move, rewrite or --result-to write that failed)
45
108
 
46
109
  CLI examples:
47
110
  python3 skf-rewrite-skill-name.py SKILL.md \
48
111
  --kind skill-frontmatter --old-name rename --new-name rename-skill
49
112
  python3 skf-rewrite-skill-name.py context-snippet.md \
50
113
  --kind context-snippet --old-name rename --new-name rename-skill --dry-run
114
+ python3 skf-rewrite-skill-name.py forge-data/rename/1.0.0/provenance-map.json \
115
+ --kind provenance-json --old-name rename --new-name rename-skill \
116
+ --moved-folder /project/skills --moved-folder /project/forge-data
117
+ python3 skf-rewrite-skill-name.py --skill-group /project/skills/rename-skill \
118
+ --versions 1.0.0,0.9.0 --old-name rename --new-name rename-skill \
119
+ --moved-folder /project/skills --result-to run/rename-rewrite.json
51
120
  """
52
121
 
53
122
  from __future__ import annotations
@@ -64,6 +133,7 @@ from pathlib import Path
64
133
  NAME_RE = re.compile(r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$")
65
134
 
66
135
  KINDS = ("skill-frontmatter", "metadata-json", "context-snippet", "provenance-json")
136
+ JSON_KINDS = ("metadata-json", "provenance-json")
67
137
 
68
138
 
69
139
  def _die(code: int, message: str) -> None:
@@ -106,6 +176,17 @@ def atomic_write_text(target: Path, text: str) -> int:
106
176
  # --- Pure transforms (import-friendly for unit tests) -------------------------
107
177
 
108
178
 
179
+ # Keep identical to _name_token_re in skf-verify-no-trace.py
180
+ # (test/test-skf-rewrite-skill-name.py pins the copies).
181
+ def _name_token_re(old_name: str) -> "re.Pattern[str]":
182
+ """Match old_name only where it is a complete skill-name token.
183
+
184
+ Negative lookbehind/lookahead on `[a-z0-9-]` so `rename` matches inside
185
+ `/rename/` or `"rename"` but not inside `rename-skill` or `renamed`.
186
+ """
187
+ return re.compile(r"(?<![a-z0-9-])" + re.escape(old_name) + r"(?![a-z0-9-])")
188
+
189
+
109
190
  def rewrite_frontmatter_name(content: str, new_name: str):
110
191
  """Replace the top-level `name:` value inside the frontmatter block only.
111
192
 
@@ -187,41 +268,285 @@ def _rewrite_root_path(path: str, old_name: str, new_name: str) -> str:
187
268
  return path
188
269
 
189
270
 
190
- def rewrite_context_snippet(content: str, old_name: str, new_name: str):
191
- """Rewrite the display header and every `root:` path in a context snippet.
271
+ # The places every SKF snippet template writes the skill's name, besides the
272
+ # root path: the display header `[<name> v...]`, and on the `|IMPORTANT:` line
273
+ # its first word and the `writing <name> code` phrase. The rest of a snippet is
274
+ # the template's fixed words and field labels (`root:`, `|api:`, "training
275
+ # data", "SKILL.md") or the library's own content, which a rename must not touch.
276
+ _HEADER_SLOT_RE = re.compile(r"\[(?P<slot>[^\]\s]+)(?=\s+v[^\]]*\])")
277
+ _IMPORTANT_LINE_RE = re.compile(r"^\|IMPORTANT:[^\n]*", re.M)
278
+ _IMPORTANT_SLOT_RES = (
279
+ re.compile(r"^\|IMPORTANT:[ \t]*(?P<slot>\S+)"),
280
+ re.compile(r"\bwriting[ \t]+(?P<slot>\S+?)[ \t]+code\b"),
281
+ )
282
+ _ROOT_PATH_RE = re.compile(r"root:\s*(?P<path>\S+)")
283
+
192
284
 
193
- Returns (new_content, details) where details records what changed. Never
194
- raises — a snippet missing the header or a root just yields no change there.
285
+ def rewrite_context_snippet(content: str, old_name: str, new_name: str):
286
+ """Rewrite the name where SKF's snippet templates write it.
287
+
288
+ skf-verify-no-trace.py counts any complete-token mention of the old name in
289
+ context-snippet.md as a leftover, and every SKF snippet template writes the
290
+ name in its display header, on its `|IMPORTANT:` line (first word, and
291
+ `writing <name> code`) and in its `root:` path. In the header and the
292
+ IMPORTANT-line slots every token _name_token_re matches (the verifier's
293
+ rule) becomes new_name. Each `root:` path goes through _rewrite_root_path,
294
+ which keeps its prefix (the IDE's skill folder) and swaps only the trailing
295
+ `{old}/` segment.
296
+
297
+ Nothing else changes: a field label, a fixed word of the template or the
298
+ library's content that happens to be the old name stays, and the verifier
299
+ then stops the rename (it rolls back with the old skill intact) instead of
300
+ committing a changed snippet.
301
+
302
+ Returns (new_content, details): `header_rewritten` (a `[<old> v...]` header
303
+ was present), `roots` (each root path changed) and `mentions` (how many
304
+ tokens were rewritten in the header and IMPORTANT-line slots). Never
305
+ raises: a snippet with no mention just yields no change.
306
+ """
307
+ details = {"header_rewritten": False, "roots": [], "mentions": 0}
308
+ token_re = _name_token_re(old_name)
309
+
310
+ # (start, end, kind) of each slot. Root paths are rewritten as paths.
311
+ spans = [(m.start("slot"), m.end("slot"), "header") for m in _HEADER_SLOT_RE.finditer(content)]
312
+ for line in _IMPORTANT_LINE_RE.finditer(content):
313
+ for slot_re in _IMPORTANT_SLOT_RES:
314
+ for m in slot_re.finditer(line.group(0)):
315
+ spans.append((line.start() + m.start("slot"), line.start() + m.end("slot"), "important"))
316
+ spans += [(m.start("path"), m.end("path"), "root") for m in _ROOT_PATH_RE.finditer(content)]
317
+
318
+ out = []
319
+ pos = 0
320
+ for start, end, kind in sorted(set(spans)):
321
+ if start < pos: # inside a slot already rewritten
322
+ continue
323
+ out.append(content[pos:start])
324
+ text = content[start:end]
325
+ if kind == "root":
326
+ new_text = _rewrite_root_path(text, old_name, new_name)
327
+ if new_text != text:
328
+ details["roots"].append({"old": text, "new": new_text})
329
+ else:
330
+ new_text, count = token_re.subn(new_name, text)
331
+ details["mentions"] += count
332
+ if count and kind == "header":
333
+ details["header_rewritten"] = True
334
+ out.append(new_text)
335
+ pos = end
336
+ out.append(content[pos:])
337
+ return "".join(out), details
338
+
339
+
340
+ # Keep identical to SOURCE_FACT_KEYS in skf-verify-no-trace.py
341
+ # (test/test-skf-rewrite-skill-name.py pins the copies). The keys whose value
342
+ # names the upstream source the skill was made from, not the skill.
343
+ SOURCE_FACT_KEYS = frozenset({
344
+ "source_repo",
345
+ "source_root",
346
+ "source_commit",
347
+ "source_ref",
348
+ "source_package",
349
+ "source_library",
350
+ })
351
+
352
+ # The values rewrite_moved_paths never changes: the upstream source, and the
353
+ # files in it (`source_file` of an export, script, asset or promoted doc, and
354
+ # the `co_import_files` of a stack integration). The rename moves the skill's
355
+ # folders, not the source, so a path such as `skills/<old>/x.py` there is the
356
+ # source's own layout; skf-verify-no-trace.py then reports a `source_file`
357
+ # that names the old name instead of the rename rewriting it.
358
+ UPSTREAM_KEYS = SOURCE_FACT_KEYS | {"source_file", "co_import_files"}
359
+
360
+ # A path begins at the start of a value or of a word in it: never inside a URL
361
+ # or after another folder of a longer path.
362
+ _PATH_START = r"(?<![^\s\"'`(\[<=,;])"
363
+
364
+
365
+ def _moved_path_re(folder: str, old_name: str) -> "re.Pattern[str]":
366
+ """`<folder>/<old>` where a path begins, and the package folder in it.
367
+
368
+ `folder` is a folder the rename moves `<old>` out of
369
+ (`{skills_output_folder}` or `{forge_data_folder}`), as given. The path
370
+ must begin with it, spelled in full or as its last components (so a value
371
+ relative to the project root, `forge-data/<old>/...` or
372
+ `_bmad-output/skills/<old>/...`, matches as well as an absolute one), at the
373
+ start of the value or of a word in it, optionally after `./`. Either
374
+ separator matches. The old name is matched by the verifier's token rule and
375
+ must also end its path segment, so `forge-data/<old>/<v>/test-report-<old>.md`
376
+ matches only at the folder. A following `/<version>/<old>` is the package
377
+ folder the rename also moves (`{skill_group}/{version}/{skill-name}/`).
195
378
  """
196
- details = {"header_rewritten": False, "roots": []}
379
+ parts = re.split(r"[/\\]", folder.rstrip("/\\"))
380
+ sep = r"[/\\]"
381
+ spellings = dict.fromkeys(sep.join(re.escape(p) for p in parts[k:]) for k in range(len(parts)))
382
+ heads = "|".join(sorted((s for s in spellings if s), key=len, reverse=True))
383
+ name = _name_token_re(old_name).pattern
384
+ seg_end = r"(?![A-Za-z0-9._-])"
385
+ return re.compile(
386
+ _PATH_START + r"(?P<head>(?:\.[/\\])?(?:" + heads + r")[/\\])" + name + seg_end
387
+ + r"(?:(?P<mid>[/\\][^/\\\s]+[/\\])" + name + seg_end + r")?"
388
+ )
197
389
 
198
- # Display header: [<old> v<version>] -> [<new> v<version>], suffix preserved.
199
- header_re = re.compile(r"\[" + re.escape(old_name) + r"(\s+v[^\]]*)\]")
200
390
 
201
- def _header_sub(m):
202
- details["header_rewritten"] = True
203
- return "[" + new_name + m.group(1) + "]"
391
+ def rewrite_moved_paths(content: str, old_name: str, new_name: str, folders):
392
+ """Point every JSON string value that is a path into a moved folder at the new name.
204
393
 
205
- content = header_re.sub(_header_sub, content)
394
+ The rename moves `<folder>/<old>/` to `<folder>/<new>/` for each of
395
+ `folders` (and the package folder `<old>/<version>/<old>/` inside the skill
396
+ folder), so a value such as a test report's
397
+ `forge-data/<old>/<v>/test-report-<old>.md` would name a folder that no
398
+ longer exists, and the verifier would count it as a leftover. A path counts
399
+ where it begins (see _moved_path_re). The values of UPSTREAM_KEYS, at any
400
+ depth, keys and every other value stay; a file named after the skill keeps
401
+ its name, as the rename keeps the file's name. Returns (new_content, paths)
402
+ where paths lists each changed value as {"old", "new"}. Raises ValueError on
403
+ invalid JSON.
404
+ """
405
+ try:
406
+ data = json.loads(content)
407
+ except json.JSONDecodeError as e:
408
+ raise ValueError(f"invalid JSON: {e}") from e
409
+ patterns = [_moved_path_re(f, old_name) for f in dict.fromkeys(folders) if _folder_name(f)]
410
+ paths = []
411
+
412
+ def _sub(m):
413
+ return m.group("head") + new_name + ((m.group("mid") + new_name) if m.group("mid") else "")
414
+
415
+ def _walk(node):
416
+ if isinstance(node, dict):
417
+ for key, value in node.items():
418
+ if key not in UPSTREAM_KEYS:
419
+ node[key] = _walk(value)
420
+ elif isinstance(node, list):
421
+ node[:] = [_walk(value) for value in node]
422
+ elif isinstance(node, str):
423
+ new_value = node
424
+ for pattern in patterns:
425
+ new_value = pattern.sub(_sub, new_value)
426
+ if new_value != node:
427
+ paths.append({"old": node, "new": new_value})
428
+ return new_value
429
+ return node
430
+
431
+ data = _walk(data)
432
+ if not paths:
433
+ return content, paths
434
+ return json.dumps(data, indent=2) + "\n", paths
435
+
436
+
437
+ # --- Batch: every version of a copied skill folder in one call ----------------
438
+
439
+ # The package files each moved version rewrites, in order, then the forge file
440
+ # every version rewrites when the forge folder moves.
441
+ PACKAGE_FILES = (
442
+ ("skill-frontmatter", "SKILL.md"),
443
+ ("metadata-json", "metadata.json"),
444
+ ("context-snippet", "context-snippet.md"),
445
+ )
446
+ FORGE_FILE = ("provenance-json", "provenance-map.json")
447
+
448
+
449
+ def _plain_segment(value: str) -> bool:
450
+ """True for a version or a name that is one folder name and nothing else."""
451
+ return value not in ("", ".", "..") and not any(c in value for c in "/\\:\0")
452
+
453
+
454
+ def run_batch(skill_group: Path, versions, old_name: str, new_name: str,
455
+ forge_group: Path | None = None, moved_folders=()) -> dict:
456
+ """Move each version's package to the new name, then rewrite its files.
457
+
458
+ See "Batch mode" in the module docstring. Never raises on a file it
459
+ processes: a failure ends the batch with `status` "error".
460
+ """
461
+ result = {
462
+ "status": "ok",
463
+ "old_name": old_name,
464
+ "new_name": new_name,
465
+ "skill_group": str(skill_group),
466
+ "forge_group": str(forge_group) if forge_group is not None else None,
467
+ "renamed_versions": [],
468
+ "files_rewritten": [],
469
+ "counts": {kind: 0 for kind in KINDS},
470
+ "package_warnings": [],
471
+ "missing_files": [],
472
+ "error": None,
473
+ }
206
474
 
207
- # root: <path> — path is a non-whitespace token.
208
- root_re = re.compile(r"(root:\s*)(\S+)")
475
+ def fail(stage: str, path: Path, message: str) -> dict:
476
+ result["status"] = "error"
477
+ result["error"] = {"stage": stage, "path": str(path), "message": message}
478
+ return result
209
479
 
210
- def _root_sub(m):
211
- prefix, path = m.group(1), m.group(2)
212
- new_path = _rewrite_root_path(path, old_name, new_name)
213
- if new_path != path:
214
- details["roots"].append({"old": path, "new": new_path})
215
- return prefix + new_path
480
+ for version in versions:
481
+ source = skill_group / version / old_name
482
+ target = skill_group / version / new_name
483
+ if not source.is_dir():
484
+ result["package_warnings"].append(
485
+ f"{version}: no {old_name}/ package in {skill_group / version}; skipped")
486
+ continue
487
+ if os.path.lexists(target):
488
+ return fail("inner-rename", target, "the new package folder already exists")
489
+ try:
490
+ os.rename(source, target)
491
+ except OSError as e:
492
+ return fail("inner-rename", source, str(e))
493
+ result["renamed_versions"].append(version)
494
+
495
+ for version in versions:
496
+ files = []
497
+ if version in result["renamed_versions"]:
498
+ files += [(kind, skill_group / version / new_name / name) for kind, name in PACKAGE_FILES]
499
+ if forge_group is not None:
500
+ files.append((FORGE_FILE[0], forge_group / version / FORGE_FILE[1]))
501
+ for kind, path in files:
502
+ if not path.is_file():
503
+ result["missing_files"].append(str(path))
504
+ continue
505
+ moved = moved_folders if kind in JSON_KINDS else ()
506
+ try:
507
+ done = process(path, kind, old_name, new_name, False, moved)
508
+ except ValueError as e:
509
+ return fail("rewrite", path, f"{kind} transform failed: {e}")
510
+ except OSError as e:
511
+ return fail("rewrite", path, f"atomic write failed: {e}")
512
+ if done["wrote"] is not None:
513
+ result["files_rewritten"].append({"kind": kind, "path": done["wrote"]})
514
+ result["counts"][kind] += 1
515
+ return result
216
516
 
217
- content = root_re.sub(_root_sub, content)
218
- return content, details
517
+
518
+ def _main_batch(args) -> None:
519
+ if not _plain_segment(args.old_name):
520
+ _die(1, f"--old-name must be one folder name, got: {args.old_name!r}")
521
+ versions = [v.strip() for v in args.versions.split(",") if v.strip()]
522
+ bad = [v for v in versions if not _plain_segment(v)]
523
+ if bad:
524
+ _die(1, f"--versions must name version folders, got: {bad!r}")
525
+ if not args.skill_group.is_dir():
526
+ _die(1, f"skill group not found: {args.skill_group}")
527
+ result = run_batch(args.skill_group, versions, args.old_name, args.new_name,
528
+ args.forge_group, args.moved_folder)
529
+ if args.result_to is not None:
530
+ try:
531
+ atomic_write_text(args.result_to, json.dumps(result, indent=2) + "\n")
532
+ except OSError as e:
533
+ if result["error"] is None:
534
+ result["status"] = "error"
535
+ result["error"] = {"stage": "record", "path": str(args.result_to), "message": f"cannot write it: {e}"}
536
+ print(json.dumps(result, indent=2))
537
+ sys.exit(0 if result["status"] == "ok" else 2)
219
538
 
220
539
 
221
540
  # --- CLI orchestration --------------------------------------------------------
222
541
 
223
542
 
224
- def process(target: Path, kind: str, old_name: str, new_name: str, dry_run: bool) -> dict:
543
+ def _folder_name(folder: str) -> str:
544
+ """The last component of a folder path, whatever its separators."""
545
+ return re.split(r"[/\\]", folder.rstrip("/\\"))[-1]
546
+
547
+
548
+ def process(target: Path, kind: str, old_name: str, new_name: str, dry_run: bool,
549
+ moved_folders=()) -> dict:
225
550
  original = target.read_text(encoding="utf-8")
226
551
 
227
552
  result = {
@@ -251,10 +576,16 @@ def process(target: Path, kind: str, old_name: str, new_name: str, dry_run: bool
251
576
  new_content, details = rewrite_context_snippet(original, old_name, new_name)
252
577
  result["header_rewritten"] = details["header_rewritten"]
253
578
  result["roots_rewritten"] = details["roots"]
254
- result["matched"] = details["header_rewritten"] or bool(details["roots"])
579
+ result["mentions_rewritten"] = details["mentions"]
580
+ result["matched"] = details["mentions"] > 0 or bool(details["roots"])
255
581
  else: # pragma: no cover - argparse choices guard this
256
582
  raise ValueError(f"unknown kind: {kind}")
257
583
 
584
+ if kind in JSON_KINDS:
585
+ new_content, paths = rewrite_moved_paths(
586
+ new_content, old_name, new_name, list(moved_folders))
587
+ result["paths_rewritten"] = paths
588
+
258
589
  changed = new_content != original
259
590
  result["changed"] = changed
260
591
  if dry_run:
@@ -270,28 +601,67 @@ def process(target: Path, kind: str, old_name: str, new_name: str, dry_run: bool
270
601
  return result
271
602
 
272
603
 
273
- def main() -> None:
604
+ def _build_parser() -> argparse.ArgumentParser:
274
605
  parser = argparse.ArgumentParser(
275
606
  description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
276
607
  )
277
- parser.add_argument("target", type=Path, help="File to rewrite (SKILL.md, metadata.json, ...)")
278
- parser.add_argument("--kind", required=True, choices=KINDS, help="File kind / transform to apply")
608
+ parser.add_argument("target", type=Path, nargs="?",
609
+ help="File to rewrite (SKILL.md, metadata.json, ...); leave out with --skill-group")
610
+ parser.add_argument("--kind", choices=KINDS, help="File kind / transform to apply (single-file mode)")
279
611
  parser.add_argument("--old-name", required=True, help="Current skill name")
280
612
  parser.add_argument("--new-name", required=True, help="New skill name (kebab-case)")
613
+ parser.add_argument(
614
+ "--moved-folder", action="append", default=[], metavar="FOLDER",
615
+ help="metadata-json / provenance-json only, repeatable: a folder the rename moves <old>/ "
616
+ "to <new>/ in ({skills_output_folder}, and {forge_data_folder} when the forge folder "
617
+ "moves); path values into FOLDER/<old>/ are pointed at FOLDER/<new>/")
281
618
  parser.add_argument("--dry-run", action="store_true", help="Compute without writing; emit new_content")
282
619
  parser.add_argument("--verbose", action="store_true", help="Diagnostics to stderr")
620
+ batch = parser.add_argument_group("batch mode (see the module docstring)")
621
+ batch.add_argument("--skill-group", type=Path, metavar="FOLDER",
622
+ help="the copied skill folder whose versions to rename, in place of a target")
623
+ batch.add_argument("--versions", metavar="V1,V2",
624
+ help="batch: the comma-separated version folders to rename (required with --skill-group)")
625
+ batch.add_argument("--forge-group", type=Path, metavar="FOLDER",
626
+ help="batch: the copied forge folder whose provenance maps to rewrite")
627
+ batch.add_argument("--result-to", type=Path, metavar="FILE",
628
+ help="batch: also write the result JSON to FILE")
629
+ return parser
630
+
631
+
632
+ def main() -> None:
633
+ parser = _build_parser()
283
634
  args = parser.parse_args()
284
635
 
285
636
  if not args.old_name:
286
637
  _die(1, "old-name must be non-empty")
287
638
  if not NAME_RE.match(args.new_name) or len(args.new_name) > 64:
288
639
  _die(1, f"new-name must be kebab-case, 1-64 chars, got: {args.new_name!r}")
640
+ if any(not _folder_name(f) for f in args.moved_folder):
641
+ _die(1, f"--moved-folder must name a folder, got: {args.moved_folder!r}")
642
+
643
+ batch_only = {"--versions": args.versions, "--forge-group": args.forge_group, "--result-to": args.result_to}
644
+ if args.skill_group is not None:
645
+ if args.target is not None or args.kind is not None or args.dry_run:
646
+ _die(1, "--skill-group takes no target, --kind or --dry-run")
647
+ if args.versions is None:
648
+ _die(1, "--skill-group needs --versions")
649
+ _main_batch(args)
650
+ given = [flag for flag, value in batch_only.items() if value is not None]
651
+ if given:
652
+ _die(1, f"{', '.join(given)} {'needs' if len(given) == 1 else 'need'} --skill-group")
653
+ if args.target is None or args.kind is None:
654
+ _die(1, "pass a target file and --kind, or --skill-group for the batch")
655
+
656
+ if args.moved_folder and args.kind not in JSON_KINDS:
657
+ _die(1, f"--moved-folder applies to {' and '.join(JSON_KINDS)} only, not {args.kind}")
289
658
 
290
659
  if not args.target.exists():
291
660
  _die(1, f"target not found: {args.target}")
292
661
 
293
662
  try:
294
- result = process(args.target, args.kind, args.old_name, args.new_name, args.dry_run)
663
+ result = process(args.target, args.kind, args.old_name, args.new_name, args.dry_run,
664
+ args.moved_folder)
295
665
  except ValueError as e:
296
666
  _die(2, f"{args.kind} transform failed for {args.target}: {e}")
297
667
  except OSError as e:
@@ -304,5 +674,21 @@ def main() -> None:
304
674
  sys.exit(0)
305
675
 
306
676
 
677
+ def _force_utf8(*streams) -> None:
678
+ """Reconfigure stdout and stderr to UTF-8, keeping each stream's error handler.
679
+
680
+ A Windows console pipes them as cp1252, which cannot print every character
681
+ the --help text or a path may hold.
682
+ """
683
+ for stream in streams:
684
+ if hasattr(stream, "reconfigure"):
685
+ errors = getattr(stream, "errors", None)
686
+ if errors is None:
687
+ stream.reconfigure(encoding="utf-8")
688
+ else:
689
+ stream.reconfigure(encoding="utf-8", errors=errors)
690
+
691
+
307
692
  if __name__ == "__main__":
693
+ _force_utf8(sys.stdout, sys.stderr)
308
694
  main()