bmad-module-skill-forge 2.2.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (437) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +49 -49
  3. package/docs/404.md +10 -8
  4. package/docs/_data/pinned.yaml +22 -13
  5. package/docs/_internal/STABILITY.md +58 -35
  6. package/docs/agents.md +39 -53
  7. package/docs/architecture.md +65 -51
  8. package/docs/bmad-synergy.md +53 -51
  9. package/docs/campaign.md +127 -75
  10. package/docs/concepts.md +34 -28
  11. package/docs/examples.md +70 -68
  12. package/docs/forge-auto.md +34 -31
  13. package/docs/getting-started.md +118 -57
  14. package/docs/how-it-works.md +25 -21
  15. package/docs/index.md +15 -15
  16. package/docs/skill-model.md +70 -59
  17. package/docs/troubleshooting.md +143 -18
  18. package/docs/verifying-a-skill.md +87 -57
  19. package/docs/why-skf.md +24 -22
  20. package/docs/workflows.md +274 -110
  21. package/package.json +18 -10
  22. package/src/forger/preferences.yaml +8 -1
  23. package/src/knowledge/agentskills-spec.md +2 -0
  24. package/src/knowledge/ccc-bridge.md +40 -24
  25. package/src/knowledge/confidence-tiers.md +9 -5
  26. package/src/knowledge/overview.md +3 -3
  27. package/src/knowledge/progressive-capability.md +8 -8
  28. package/src/knowledge/provenance-tracking.md +9 -5
  29. package/src/knowledge/qmd-registry.md +3 -3
  30. package/src/knowledge/skf-knowledge-index.csv +2 -2
  31. package/src/knowledge/skill-lifecycle.md +18 -11
  32. package/src/knowledge/split-body-strategy.md +0 -3
  33. package/src/knowledge/tool-resolution.md +4 -2
  34. package/src/knowledge/version-paths.md +101 -15
  35. package/src/module-help.csv +3 -3
  36. package/src/module.yaml +10 -0
  37. package/src/shared/data/ast-grep-recipes.yaml +608 -0
  38. package/src/shared/data/ide-context-files.json +30 -0
  39. package/src/shared/health-check.md +31 -59
  40. package/src/shared/references/description-guard-protocol.md +12 -9
  41. package/src/shared/references/feasibility-report-schema.md +19 -13
  42. package/src/shared/references/headless-gate-convention.md +55 -3
  43. package/src/shared/references/output-contract-schema.md +19 -8
  44. package/src/shared/references/pipeline-contracts.md +94 -29
  45. package/src/shared/references/tessl-review.md +77 -0
  46. package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
  47. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
  48. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
  49. package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
  50. package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
  51. package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
  52. package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
  53. package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
  54. package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
  55. package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
  56. package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
  57. package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
  58. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
  59. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
  60. package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
  61. package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
  62. package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
  63. package/src/shared/scripts/skf-atomic-write.py +233 -64
  64. package/src/shared/scripts/skf-build-change-manifest.py +2753 -28
  65. package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
  66. package/src/shared/scripts/skf-check-unit-records.py +184 -0
  67. package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
  68. package/src/shared/scripts/skf-classify-changed-files.py +772 -0
  69. package/src/shared/scripts/skf-comention-pairs.py +1333 -123
  70. package/src/shared/scripts/skf-compare-file-hashes.py +267 -105
  71. package/src/shared/scripts/skf-count-imports.py +1867 -0
  72. package/src/shared/scripts/skf-count-tokens.py +1 -1
  73. package/src/shared/scripts/skf-derive-assembly-shape.py +21 -3
  74. package/src/shared/scripts/skf-description-guard.py +99 -11
  75. package/src/shared/scripts/skf-detect-docs.py +694 -2
  76. package/src/shared/scripts/skf-detect-language.py +486 -107
  77. package/src/shared/scripts/skf-detect-registry.py +887 -0
  78. package/src/shared/scripts/skf-detect-scripts-assets.py +197 -55
  79. package/src/shared/scripts/skf-detect-tools.py +209 -27
  80. package/src/shared/scripts/skf-detect-workspaces.py +506 -30
  81. package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
  82. package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
  83. package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
  84. package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
  85. package/src/shared/scripts/skf-extract-public-api.py +5247 -96
  86. package/src/shared/scripts/skf-extraction-inventory.py +1280 -0
  87. package/src/shared/scripts/skf-extraction-snapshot.py +664 -0
  88. package/src/shared/scripts/skf-fetch-temporal.py +674 -0
  89. package/src/shared/scripts/skf-find-cycles.py +97 -29
  90. package/src/shared/scripts/skf-find-test-report.py +476 -0
  91. package/src/shared/scripts/skf-forge-tier-rw.py +469 -205
  92. package/src/shared/scripts/skf-github-fetch.py +365 -0
  93. package/src/shared/scripts/skf-github-probe.py +700 -0
  94. package/src/shared/scripts/skf-hash-content.py +180 -20
  95. package/src/shared/scripts/skf-language-corpora.py +1 -1
  96. package/src/shared/scripts/skf-load-provenance.py +161 -27
  97. package/src/shared/scripts/skf-manifest-ops.py +1 -1
  98. package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
  99. package/src/shared/scripts/skf-merge-doc-urls.py +143 -20
  100. package/src/shared/scripts/skf-names-present.py +221 -0
  101. package/src/shared/scripts/skf-pair-intersect.py +119 -28
  102. package/src/shared/scripts/skf-parse-gaps.py +1384 -0
  103. package/src/shared/scripts/skf-preapply.py +1 -1
  104. package/src/shared/scripts/skf-preflight.py +176 -28
  105. package/src/shared/scripts/skf-promote-staged.py +430 -0
  106. package/src/shared/scripts/skf-provenance-gap-dispatch.py +64 -8
  107. package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
  108. package/src/shared/scripts/skf-quick-batch.py +774 -0
  109. package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
  110. package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
  111. package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
  112. package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
  113. package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
  114. package/src/shared/scripts/skf-resolve-authoritative-files.py +155 -14
  115. package/src/shared/scripts/skf-resolve-package.py +523 -106
  116. package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
  117. package/src/shared/scripts/skf-run-lock.py +577 -0
  118. package/src/shared/scripts/skf-scan-manifests.py +998 -57
  119. package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
  120. package/src/shared/scripts/skf-severity-classify.py +712 -108
  121. package/src/shared/scripts/skf-shape-detect.py +298 -204
  122. package/src/shared/scripts/skf-shard-body.py +9 -8
  123. package/src/shared/scripts/skf-skill-inventory.py +2412 -89
  124. package/src/shared/scripts/skf-skills-module.py +472 -0
  125. package/src/shared/scripts/skf-source-tree.py +1796 -0
  126. package/src/shared/scripts/skf-structural-diff.py +787 -121
  127. package/src/shared/scripts/skf-tessl-review.py +706 -0
  128. package/src/shared/scripts/skf-update-active-symlink.py +120 -67
  129. package/src/shared/scripts/skf-update-run-state.py +624 -0
  130. package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
  131. package/src/shared/scripts/skf-validate-brief-schema.py +23 -2
  132. package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
  133. package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
  134. package/src/shared/scripts/skf-validate-output.py +712 -64
  135. package/src/shared/scripts/skf-validate-pins.py +1 -1
  136. package/src/shared/scripts/skf-verify-no-trace.py +116 -10
  137. package/src/shared/scripts/skf-verify-provenance-completeness.py +3945 -152
  138. package/src/shared/scripts/skf-write-skill-brief.py +406 -25
  139. package/src/shared/tool-requirements.yaml +201 -0
  140. package/src/skf-analyze-source/SKILL.md +41 -36
  141. package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
  142. package/src/skf-analyze-source/customize.toml +36 -24
  143. package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
  144. package/src/skf-analyze-source/references/continue.md +2 -13
  145. package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
  146. package/src/skf-analyze-source/references/generate-briefs.md +121 -55
  147. package/src/skf-analyze-source/references/headless-contract.md +42 -15
  148. package/src/skf-analyze-source/references/identify-units.md +96 -110
  149. package/src/skf-analyze-source/references/init.md +73 -71
  150. package/src/skf-analyze-source/references/map-and-detect.md +123 -105
  151. package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
  152. package/src/skf-analyze-source/references/recommend.md +51 -70
  153. package/src/skf-analyze-source/references/scan-project.md +60 -84
  154. package/src/skf-analyze-source/references/scan-root.md +39 -0
  155. package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
  156. package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
  157. package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
  158. package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
  159. package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
  160. package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
  161. package/src/skf-audit-skill/SKILL.md +43 -51
  162. package/src/skf-audit-skill/assets/drift-report-template.md +28 -3
  163. package/src/skf-audit-skill/customize.toml +41 -20
  164. package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
  165. package/src/skf-audit-skill/references/doc-drift.md +141 -0
  166. package/src/skf-audit-skill/references/headless-contract.md +29 -0
  167. package/src/skf-audit-skill/references/health-check.md +2 -0
  168. package/src/skf-audit-skill/references/init.md +149 -210
  169. package/src/skf-audit-skill/references/re-index.md +103 -110
  170. package/src/skf-audit-skill/references/report.md +96 -33
  171. package/src/skf-audit-skill/references/semantic-diff.md +34 -25
  172. package/src/skf-audit-skill/references/severity-classify.md +79 -65
  173. package/src/skf-audit-skill/references/severity-rules.md +15 -6
  174. package/src/skf-audit-skill/references/structural-diff.md +80 -90
  175. package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
  176. package/src/skf-audit-skill/scripts/render-drift-tables.py +712 -0
  177. package/src/skf-brief-skill/SKILL.md +37 -26
  178. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  179. package/src/skf-brief-skill/assets/scope-templates.md +18 -25
  180. package/src/skf-brief-skill/assets/skill-brief-schema.md +30 -23
  181. package/src/skf-brief-skill/customize.toml +48 -20
  182. package/src/skf-brief-skill/references/analyze-target.md +144 -127
  183. package/src/skf-brief-skill/references/confirm-brief.md +11 -9
  184. package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
  185. package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
  186. package/src/skf-brief-skill/references/gather-intent.md +87 -182
  187. package/src/skf-brief-skill/references/headless-args.md +84 -16
  188. package/src/skf-brief-skill/references/health-check.md +1 -1
  189. package/src/skf-brief-skill/references/invocation-contract.md +18 -13
  190. package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
  191. package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
  192. package/src/skf-brief-skill/references/scope-definition.md +123 -47
  193. package/src/skf-brief-skill/references/step-auto-brief.md +26 -100
  194. package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
  195. package/src/skf-brief-skill/references/version-resolution.md +7 -10
  196. package/src/skf-brief-skill/references/write-brief.md +66 -57
  197. package/src/skf-campaign/SKILL.md +31 -34
  198. package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
  199. package/src/skf-campaign/customize.toml +47 -16
  200. package/src/skf-campaign/references/campaign-contracts.md +39 -29
  201. package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
  202. package/src/skf-campaign/references/health-check.md +7 -16
  203. package/src/skf-campaign/references/step-01-setup.md +46 -84
  204. package/src/skf-campaign/references/step-02-strategy.md +31 -30
  205. package/src/skf-campaign/references/step-03-pins.md +26 -18
  206. package/src/skf-campaign/references/step-04-provenance.md +24 -23
  207. package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
  208. package/src/skf-campaign/references/step-06-batch.md +41 -34
  209. package/src/skf-campaign/references/step-07-capstone.md +40 -25
  210. package/src/skf-campaign/references/step-08-verify.md +28 -29
  211. package/src/skf-campaign/references/step-09-refine.md +29 -30
  212. package/src/skf-campaign/references/step-10-export.md +48 -36
  213. package/src/skf-campaign/references/step-11-maintenance.md +31 -34
  214. package/src/skf-campaign/references/step-resume.md +37 -68
  215. package/src/skf-campaign/scripts/campaign-deps.py +57 -18
  216. package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
  217. package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
  218. package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
  219. package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
  220. package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
  221. package/src/skf-campaign/scripts/campaign-report.py +204 -37
  222. package/src/skf-campaign/scripts/campaign-state.py +1128 -0
  223. package/src/skf-campaign/scripts/campaign-status.py +10 -74
  224. package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
  225. package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
  226. package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
  227. package/src/skf-campaign/templates/campaign-report-template.md +5 -1
  228. package/src/skf-campaign/templates/kickoff-template.md +0 -14
  229. package/src/skf-create-skill/SKILL.md +27 -20
  230. package/src/skf-create-skill/assets/compile-assembly-rules.md +11 -8
  231. package/src/skf-create-skill/assets/skill-sections.md +21 -11
  232. package/src/skf-create-skill/customize.toml +20 -8
  233. package/src/skf-create-skill/references/authoritative-files-protocol.md +36 -58
  234. package/src/skf-create-skill/references/batch-mode.md +81 -0
  235. package/src/skf-create-skill/references/ccc-index-check.md +27 -0
  236. package/src/skf-create-skill/references/compile.md +119 -70
  237. package/src/skf-create-skill/references/component-extraction.md +113 -91
  238. package/src/skf-create-skill/references/enrich.md +41 -23
  239. package/src/skf-create-skill/references/entry-points-by-hand.md +37 -0
  240. package/src/skf-create-skill/references/extract.md +204 -162
  241. package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
  242. package/src/skf-create-skill/references/extraction-patterns-tracing.md +11 -11
  243. package/src/skf-create-skill/references/extraction-patterns.md +588 -201
  244. package/src/skf-create-skill/references/generate-artifacts.md +145 -93
  245. package/src/skf-create-skill/references/health-check.md +1 -1
  246. package/src/skf-create-skill/references/load-brief.md +53 -36
  247. package/src/skf-create-skill/references/relabel-rule.md +12 -0
  248. package/src/skf-create-skill/references/report.md +37 -57
  249. package/src/skf-create-skill/references/source-resolution-protocols.md +58 -169
  250. package/src/skf-create-skill/references/step-auto-shard.md +20 -93
  251. package/src/skf-create-skill/references/step-doc-rot.md +44 -60
  252. package/src/skf-create-skill/references/step-doc-sources.md +28 -19
  253. package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
  254. package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
  255. package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
  256. package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
  257. package/src/skf-create-skill/references/validate.md +173 -99
  258. package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
  259. package/src/skf-create-stack-skill/SKILL.md +29 -56
  260. package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
  261. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
  262. package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
  263. package/src/skf-create-stack-skill/customize.toml +23 -12
  264. package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
  265. package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
  266. package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
  267. package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
  268. package/src/skf-create-stack-skill/references/generate-output.md +138 -128
  269. package/src/skf-create-stack-skill/references/init.md +90 -32
  270. package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
  271. package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
  272. package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
  273. package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
  274. package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
  275. package/src/skf-create-stack-skill/references/report.md +39 -33
  276. package/src/skf-create-stack-skill/references/validate.md +20 -39
  277. package/src/skf-drop-skill/SKILL.md +28 -51
  278. package/src/skf-drop-skill/customize.toml +44 -30
  279. package/src/skf-drop-skill/references/execute.md +83 -146
  280. package/src/skf-drop-skill/references/health-check.md +2 -0
  281. package/src/skf-drop-skill/references/invocation-contract.md +64 -0
  282. package/src/skf-drop-skill/references/report.md +46 -30
  283. package/src/skf-drop-skill/references/select.md +122 -124
  284. package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
  285. package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
  286. package/src/skf-export-skill/SKILL.md +40 -62
  287. package/src/skf-export-skill/assets/managed-section-format.md +14 -50
  288. package/src/skf-export-skill/assets/snippet-format.md +3 -1
  289. package/src/skf-export-skill/customize.toml +42 -13
  290. package/src/skf-export-skill/references/generate-snippet.md +54 -46
  291. package/src/skf-export-skill/references/health-check.md +2 -0
  292. package/src/skf-export-skill/references/invocation-contract.md +15 -0
  293. package/src/skf-export-skill/references/load-skill.md +88 -62
  294. package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
  295. package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
  296. package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
  297. package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
  298. package/src/skf-export-skill/references/package.md +15 -73
  299. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
  300. package/src/skf-export-skill/references/result-envelope.md +31 -16
  301. package/src/skf-export-skill/references/summary.md +41 -17
  302. package/src/skf-export-skill/references/token-report.md +4 -12
  303. package/src/skf-export-skill/references/update-context.md +118 -237
  304. package/src/skf-forger/SKILL.md +61 -37
  305. package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
  306. package/src/skf-forger/customize.toml +43 -0
  307. package/src/skf-forger/references/pipeline-mode.md +73 -18
  308. package/src/skf-forger/scripts/forge-status.py +299 -0
  309. package/src/skf-forger/scripts/parse-pipeline.py +322 -54
  310. package/src/skf-forger/scripts/pipeline-gate.py +428 -0
  311. package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
  312. package/src/skf-quick-skill/SKILL.md +54 -53
  313. package/src/skf-quick-skill/assets/skill-template.md +4 -2
  314. package/src/skf-quick-skill/customize.toml +51 -20
  315. package/src/skf-quick-skill/references/batch-mode.md +74 -21
  316. package/src/skf-quick-skill/references/compile.md +30 -31
  317. package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
  318. package/src/skf-quick-skill/references/finalize.md +69 -18
  319. package/src/skf-quick-skill/references/halt-contract.md +41 -29
  320. package/src/skf-quick-skill/references/health-check.md +5 -3
  321. package/src/skf-quick-skill/references/quick-extract.md +114 -45
  322. package/src/skf-quick-skill/references/registry-resolution.md +9 -64
  323. package/src/skf-quick-skill/references/resolve-target.md +116 -66
  324. package/src/skf-quick-skill/references/write-and-validate.md +79 -40
  325. package/src/skf-refine-architecture/SKILL.md +43 -53
  326. package/src/skf-refine-architecture/customize.toml +49 -20
  327. package/src/skf-refine-architecture/references/compile.md +97 -54
  328. package/src/skf-refine-architecture/references/exit-codes.md +30 -0
  329. package/src/skf-refine-architecture/references/finding-storage.md +21 -0
  330. package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
  331. package/src/skf-refine-architecture/references/improvements.md +14 -20
  332. package/src/skf-refine-architecture/references/init.md +98 -57
  333. package/src/skf-refine-architecture/references/issue-detection.md +32 -39
  334. package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
  335. package/src/skf-refine-architecture/references/report.md +65 -54
  336. package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
  337. package/src/skf-rename-skill/SKILL.md +19 -54
  338. package/src/skf-rename-skill/customize.toml +32 -26
  339. package/src/skf-rename-skill/references/execute.md +230 -184
  340. package/src/skf-rename-skill/references/exit-codes.md +7 -7
  341. package/src/skf-rename-skill/references/health-check.md +1 -7
  342. package/src/skf-rename-skill/references/invocation-contract.md +25 -0
  343. package/src/skf-rename-skill/references/report.md +63 -52
  344. package/src/skf-rename-skill/references/select.md +197 -115
  345. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
  346. package/src/skf-setup/SKILL.md +26 -26
  347. package/src/skf-setup/customize.toml +14 -10
  348. package/src/skf-setup/references/auto-index.md +50 -63
  349. package/src/skf-setup/references/ccc-index.md +29 -90
  350. package/src/skf-setup/references/detect-and-tier.md +58 -67
  351. package/src/skf-setup/references/health-check.md +2 -0
  352. package/src/skf-setup/references/invocation-contract.md +14 -0
  353. package/src/skf-setup/references/report.md +43 -166
  354. package/src/skf-setup/references/tier-rules.md +5 -5
  355. package/src/skf-setup/references/write-config.md +34 -57
  356. package/src/skf-test-skill/SKILL.md +17 -57
  357. package/src/skf-test-skill/assets/output-section-formats.md +33 -34
  358. package/src/skf-test-skill/customize.toml +27 -13
  359. package/src/skf-test-skill/references/coherence-check.md +134 -62
  360. package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
  361. package/src/skf-test-skill/references/coverage-check.md +209 -264
  362. package/src/skf-test-skill/references/detect-mode.md +5 -5
  363. package/src/skf-test-skill/references/external-validators.md +94 -82
  364. package/src/skf-test-skill/references/health-check.md +6 -7
  365. package/src/skf-test-skill/references/init.md +164 -91
  366. package/src/skf-test-skill/references/invocation-contract.md +66 -0
  367. package/src/skf-test-skill/references/migration-section-rules.md +5 -2
  368. package/src/skf-test-skill/references/report.md +164 -142
  369. package/src/skf-test-skill/references/score.md +80 -107
  370. package/src/skf-test-skill/references/scoring-rules.md +42 -155
  371. package/src/skf-test-skill/references/source-access-protocol.md +18 -94
  372. package/src/skf-test-skill/references/step-hard-gate.md +51 -33
  373. package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
  374. package/src/skf-test-skill/scripts/build-result-context.py +380 -0
  375. package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
  376. package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
  377. package/src/skf-test-skill/scripts/compute-score.py +223 -45
  378. package/src/skf-test-skill/scripts/gap-ledger.py +1666 -0
  379. package/src/skf-test-skill/scripts/hard-gate.py +203 -0
  380. package/src/skf-test-skill/scripts/load-coverage-inputs.py +1198 -0
  381. package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
  382. package/src/skf-test-skill/scripts/reconcile-coverage.py +547 -79
  383. package/src/skf-test-skill/scripts/score-signatures.py +479 -0
  384. package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
  385. package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
  386. package/src/skf-test-skill/scripts/verify-declared-numerator.py +133 -56
  387. package/src/skf-test-skill/templates/test-report-template.md +21 -15
  388. package/src/skf-update-skill/SKILL.md +38 -25
  389. package/src/skf-update-skill/customize.toml +27 -12
  390. package/src/skf-update-skill/references/detect-changes.md +253 -223
  391. package/src/skf-update-skill/references/gap-driven.md +336 -0
  392. package/src/skf-update-skill/references/health-check.md +31 -5
  393. package/src/skf-update-skill/references/init.md +254 -84
  394. package/src/skf-update-skill/references/invocation-contract.md +12 -0
  395. package/src/skf-update-skill/references/manual-section-rules.md +6 -23
  396. package/src/skf-update-skill/references/merge.md +182 -49
  397. package/src/skf-update-skill/references/re-extract.md +53 -262
  398. package/src/skf-update-skill/references/report.md +102 -35
  399. package/src/skf-update-skill/references/write.md +357 -155
  400. package/src/skf-update-skill/scripts/skf-new-file-diff.py +183 -46
  401. package/src/skf-verify-stack/SKILL.md +23 -42
  402. package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
  403. package/src/skf-verify-stack/customize.toml +55 -22
  404. package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
  405. package/src/skf-verify-stack/references/coverage.md +67 -64
  406. package/src/skf-verify-stack/references/exit-codes.md +21 -8
  407. package/src/skf-verify-stack/references/init.md +107 -62
  408. package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
  409. package/src/skf-verify-stack/references/integrations.md +106 -65
  410. package/src/skf-verify-stack/references/report.md +59 -42
  411. package/src/skf-verify-stack/references/requirements.md +32 -9
  412. package/src/skf-verify-stack/references/synthesize.md +53 -39
  413. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
  414. package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
  415. package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
  416. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
  417. package/tools/cli/commands/install.js +11 -4
  418. package/tools/cli/commands/status.js +15 -3
  419. package/tools/cli/commands/update.js +11 -4
  420. package/tools/cli/lib/tool-check.js +320 -0
  421. package/tools/cli/lib/version-check.js +22 -9
  422. package/tools/cli/skf-cli.js +4 -1
  423. package/.gitattributes +0 -16
  424. package/.nvmrc +0 -1
  425. package/docs/_internal/RELEASING.md +0 -679
  426. package/release-audits/v1.0.0-launch-audit.md +0 -728
  427. package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
  428. package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
  429. package/src/skf-campaign/manifest.yaml +0 -15
  430. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
  431. package/src/skf-create-skill/references/ecosystem-check.md +0 -74
  432. package/src/skf-drop-skill/references/headless-contract.md +0 -18
  433. package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
  434. package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
  435. package/src/skf-update-skill/references/validate.md +0 -124
  436. package/tools/validate-docs-drift.js +0 -233
  437. package/tools/validate-docs-links.js +0 -665
@@ -1,6 +1,6 @@
1
1
  # /// script
2
- # requires-python = ">=3.10"
3
- # dependencies = ["pyyaml"]
2
+ # requires-python = ">=3.11"
3
+ # dependencies = ["pyyaml", "jsonschema>=4.0"]
4
4
  # ///
5
5
  """SKF Write Skill Brief — Schema-validated atomic writer for skill-brief.yaml.
6
6
 
@@ -16,13 +16,26 @@ seams (key order, YAML formatting, conditional field inclusion rules,
16
16
  atomic-write behaviour) that the LLM cannot fully close on every
17
17
  invocation. This script is the single source of truth.
18
18
 
19
- Subcommand:
19
+ Subcommands:
20
20
 
21
21
  write Read brief context as JSON on stdin, validate against
22
22
  src/shared/scripts/schemas/skill-brief.v1.json, apply
23
23
  version-precedence rules, render the canonical YAML, and
24
24
  atomically write to --target. Emits a JSON success envelope
25
- on stdout.
25
+ on stdout. With --base-brief, the context is a brief already
26
+ on disk instead, with the changes the other two flags give
27
+ (see "Base brief form" below).
28
+
29
+ amend Read answers as JSON on stdin and apply them to the brief
30
+ already at --target: set scope.registry_path and
31
+ scope.demo_patterns, append literal paths to scope.include,
32
+ append the entries to scope.amendments, refuse an amendment
33
+ that adds a skill-brief.v1.json error (skf-validate-brief-schema.py's
34
+ rules), copy the brief to <target>.bak, and write it atomically.
35
+ Every other field keeps its value and its place; comments are
36
+ not kept (the .bak copy holds them). create-skill step 3d
37
+ records its answers so, and step 3's authoritative-files
38
+ protocol its promote, skip and headless-deferral decisions.
26
39
 
27
40
  Context payload shape (consumed by `write`):
28
41
 
@@ -47,14 +60,20 @@ Context payload shape (consumed by `write`):
47
60
  "notes": "",
48
61
  # Conditionally present (preserved verbatim on a ratify/re-write):
49
62
  "tier_a_include": ["code/core/src/**"], # stratified-scope monorepos
50
- "amendments": [{...}] # post-authoring audit log
63
+ "amendments": [{...}], # post-authoring audit log
64
+ # component-library scopes (create-skill step 3d writes the first
65
+ # two back when the user confirms them):
66
+ "registry_path": "registry/index.ts",
67
+ "ui_variants": [{"name": "shadcnui", "package": "packages/ui"}],
68
+ "demo_patterns": ["**/demo/**", "**/*.stories.*"]
51
69
  },
52
70
 
53
71
  # Conditionally present:
54
72
  "doc_urls": [{"url": "...", "label": "...", "source": "..."}],
55
73
  # `source` (optional per #432): language-registry | readme-detection |
56
74
  # homepage | pages-api | docs-folder. Threaded through when present;
57
- # absent → entry renders as {url, label} only (no false drift).
75
+ # absent → entry renders as {url, label} only (no false drift); any
76
+ # other value is refused before anything is written (exit 1).
58
77
  "scripts_intent": "detect" | "none" | free-text,
59
78
  "assets_intent": "detect" | "none" | free-text,
60
79
  "source_authority": "official" | "community" | "internal",
@@ -66,8 +85,12 @@ Version precedence (resolved into the rendered YAML's `version` field):
66
85
  1. version_resolved if explicitly supplied (caller already ran the
67
86
  precedence rule). Used by step 5 when it has confirmed values.
68
87
  2. Otherwise: target_version if non-null.
69
- 3. Otherwise: detected_version if non-null.
70
- 4. Otherwise: "1.0.0".
88
+ 3. Otherwise: detected_version when it is full X.Y.Z semver (SEMVER_RE);
89
+ a detected version of another shape (`0.1`, `2.0.0rc1`) is passed as
90
+ found and skipped here, with a warning.
91
+ 4. Otherwise: "1.0.0", with a warning when no detected_version was given
92
+ (a docs-only brief aside), so a headless caller tells the default
93
+ from a detected 1.0.0.
71
94
 
72
95
  When target_version is set, the rendered YAML includes a `target_version`
73
96
  field whose value MUST match `version` (the script enforces this
@@ -98,6 +121,9 @@ Flat input form (`--from-flat`):
98
121
  "scope_notes": "",
99
122
  "scope_tier_a_include": null | ["code/core/src/**"],
100
123
  "scope_amendments": null | [{...}],
124
+ "scope_registry_path": null | "registry/index.ts",
125
+ "scope_ui_variants": null | [{"name": "...", "package": "..."}],
126
+ "scope_demo_patterns": null | ["**/demo/**"],
101
127
  "doc_urls": null | [{"url": "...", "label": "...", "source": "..."}],
102
128
  "scripts_intent": null | "detect" | "none" | "...",
103
129
  "assets_intent": null | "detect" | "none" | "...",
@@ -106,6 +132,21 @@ Flat input form (`--from-flat`):
106
132
  "source_ref": null | "v0.5.0"
107
133
  }
108
134
 
135
+ Base brief form (`write --base-brief <file> [--doc-urls-file <file>]
136
+ [--patch-file <file>]`):
137
+
138
+ The context is the brief in --base-brief (a skill-brief.yaml), so a
139
+ caller that rewrites a brief never retypes it and no field it holds is
140
+ dropped. --doc-urls-file replaces its doc_urls with the `doc_urls` of
141
+ that file (the output of skf-merge-doc-urls.py). --patch-file lays a
142
+ JSON object of changes over it: an object merges key by key, null
143
+ removes the key, and any other value (a string, a list) replaces it.
144
+ version_resolved is the patch's version_resolved, else the result's
145
+ `version`, so the version the brief holds (or the one the patch sets)
146
+ is written; a brief with a target_version needs the patch to change
147
+ both. stdin is not read, and --from-flat does not apply. A date the
148
+ YAML holds unquoted is read as its ISO text.
149
+
109
150
  Output (success):
110
151
 
111
152
  {
@@ -116,6 +157,37 @@ Output (success):
116
157
  "warnings": ["string", ...]
117
158
  }
118
159
 
160
+ Answers payload (consumed by `amend`), each key optional, one at least:
161
+
162
+ {
163
+ "registry_path": "registry/index.ts",
164
+ "demo_patterns": ["**/examples/**"],
165
+ "include": ["docs/llms.txt"],
166
+ "amendments": [{"path": "...", "action": "...", "category": "...",
167
+ "reason": "...", "evidence": "...",
168
+ "date": "2026-10-01", "workflow": "..."}]
169
+ }
170
+
171
+ Each amendment needs a non-empty path, action, reason and workflow and
172
+ an ISO date; category, when given, is a non-empty string.
173
+
174
+ `include` holds literal paths, each appended to scope.include unless the
175
+ list already holds it. An empty scope.include already covers every file,
176
+ so it stays empty: appending one path would narrow the scope to that
177
+ file. The output's `included` lists the paths appended.
178
+
179
+ Output of `amend` (success):
180
+
181
+ {
182
+ "status": "ok",
183
+ "brief_path": "/abs/path/skill-brief.yaml",
184
+ "backup": "/abs/path/skill-brief.yaml.bak",
185
+ "set": ["registry_path", "demo_patterns"], # the fields given
186
+ "included": ["docs/llms.txt"], # paths appended to scope.include
187
+ "appended": <integer>, # amendment entries
188
+ "bytes": <integer>
189
+ }
190
+
119
191
  Errors emit `{"status": "error", "message": "...", "field": "..."|null}`
120
192
  to stderr and exit non-zero.
121
193
 
@@ -123,7 +195,11 @@ Exit codes:
123
195
  0 — success
124
196
  1 — validation failure (bad context, schema violation, invariant
125
197
  violation, version-precedence underflow with no fallback path)
198
+ or, for amend, a bad payload, a brief with no scope mapping or
199
+ an amendment that adds a skill-brief.v1.json error
126
200
  2 — I/O failure (atomic write failed, parent directory not writable)
201
+ or, for amend, a brief that cannot be read or a backup that
202
+ cannot be written
127
203
 
128
204
  Cross-platform: pure stdlib + PyYAML. Atomic write via temp + fsync +
129
205
  rename, mirroring skf-atomic-write.py and the helper in
@@ -133,6 +209,8 @@ skf-forge-tier-rw.py.
133
209
  from __future__ import annotations
134
210
 
135
211
  import argparse
212
+ import datetime
213
+ import importlib.util
136
214
  import json
137
215
  import os
138
216
  import re
@@ -160,6 +238,8 @@ VALID_SCOPE_TYPES = {
160
238
  "reference-app",
161
239
  "docs-only",
162
240
  }
241
+ # skill-brief.v1.json's doc_urls[].source enum (a test pins it to the schema).
242
+ VALID_DOC_URL_SOURCES = {"language-registry", "readme-detection", "homepage", "pages-api", "docs-folder"}
163
243
 
164
244
 
165
245
  def _die(message: str, field: str | None = None, code: int = 1) -> None:
@@ -176,6 +256,9 @@ def resolve_version(ctx: dict[str, Any]) -> str:
176
256
  Uses `is not None` checks (not truthiness) so an explicitly-supplied
177
257
  empty string surfaces as a SEMVER_RE validation failure downstream
178
258
  rather than silently falling through to the next precedence level.
259
+ The detected version is the exception: callers pass it as the manifest
260
+ gives it, so one that is not full X.Y.Z semver falls through to the
261
+ default, as validate_context's warning says.
179
262
  """
180
263
  vr = ctx.get("version_resolved")
181
264
  if vr is not None:
@@ -184,7 +267,7 @@ def resolve_version(ctx: dict[str, Any]) -> str:
184
267
  if tv is not None:
185
268
  return tv
186
269
  dv = ctx.get("detected_version")
187
- if dv is not None:
270
+ if isinstance(dv, str) and SEMVER_RE.match(dv):
188
271
  return dv
189
272
  return "1.0.0"
190
273
 
@@ -243,6 +326,10 @@ def validate_context(ctx: dict[str, Any]) -> list[str]:
243
326
  url = entry.get("url")
244
327
  if not url or not isinstance(url, str):
245
328
  _die(f"doc_urls[{i}].url is required and must be a non-empty string", field="doc_urls")
329
+ source = entry.get("source")
330
+ if source and (not isinstance(source, str) or source not in VALID_DOC_URL_SOURCES):
331
+ _die(f"doc_urls[{i}].source must be one of {sorted(VALID_DOC_URL_SOURCES)}. Got: {source!r}",
332
+ field="doc_urls")
246
333
 
247
334
  # source_authority (default 'community') with docs-only force rule
248
335
  source_authority = ctx.get("source_authority", "community")
@@ -333,6 +420,8 @@ def validate_context(ctx: dict[str, Any]) -> list[str]:
333
420
  if not isinstance(entry, dict):
334
421
  _die(f"scope.amendments[{i}] must be an object", field="scope.amendments")
335
422
 
423
+ _check_component_fields(scope)
424
+
336
425
  # target_ref / source_ref — optional git refs (top-level). target_ref is a
337
426
  # remote-monorepo tag escape hatch; source_ref is the auto-resolved ref.
338
427
  # Both must round-trip on a ratify/re-write rather than being dropped.
@@ -357,10 +446,46 @@ def validate_context(ctx: dict[str, Any]) -> list[str]:
357
446
  warnings.append(
358
447
  f"detected_version {detected!r} is not full X.Y.Z semver — falling through to default 1.0.0"
359
448
  )
449
+ elif detected is None and tv is None and ctx.get("version_resolved") is None and source_type != "docs-only":
450
+ # The default would otherwise reach a headless caller as if detected.
451
+ warnings.append(
452
+ "no source version detected: version defaults to 1.0.0; for a remote source skf-create-skill "
453
+ "tries tag 1.0.0 or v1.0.0; set target_version in the brief to pin it"
454
+ )
360
455
 
361
456
  return warnings
362
457
 
363
458
 
459
+ def _check_component_fields(scope: dict[str, Any]) -> None:
460
+ """scope.registry_path / scope.ui_variants / scope.demo_patterns: optional
461
+ component-library fields. A brief can author them, and create-skill
462
+ step 3d writes a confirmed registry path and demo patterns back (amend),
463
+ so a ratify/re-write preserves them verbatim. Light checks mirror the
464
+ schema."""
465
+ registry_path = scope.get("registry_path")
466
+ if registry_path is not None and (not isinstance(registry_path, str) or not registry_path):
467
+ _die("scope.registry_path must be a non-empty string when present", field="scope.registry_path")
468
+ demo_patterns = scope.get("demo_patterns")
469
+ if demo_patterns is not None:
470
+ if not isinstance(demo_patterns, list):
471
+ _die("scope.demo_patterns must be an array of glob strings", field="scope.demo_patterns")
472
+ for i, pat in enumerate(demo_patterns):
473
+ if not isinstance(pat, str) or not pat:
474
+ _die(f"scope.demo_patterns[{i}] must be a non-empty glob string", field="scope.demo_patterns")
475
+ ui_variants = scope.get("ui_variants")
476
+ if ui_variants is not None:
477
+ if not isinstance(ui_variants, list):
478
+ _die("scope.ui_variants must be an array of variant objects", field="scope.ui_variants")
479
+ for i, variant in enumerate(ui_variants):
480
+ if not isinstance(variant, dict):
481
+ _die(f"scope.ui_variants[{i}] must be an object", field="scope.ui_variants")
482
+ for key in ("name", "package"):
483
+ value = variant.get(key)
484
+ if value is not None and (not isinstance(value, str) or not value):
485
+ _die(f"scope.ui_variants[{i}].{key} must be a non-empty string when present",
486
+ field="scope.ui_variants")
487
+
488
+
364
489
  def assemble_brief(ctx: dict[str, Any], resolved_version: str) -> dict[str, Any]:
365
490
  """Build the final brief dict that will be YAML-dumped, in canonical key order."""
366
491
  source_type = ctx.get("source_type", "source")
@@ -416,6 +541,13 @@ def assemble_brief(ctx: dict[str, Any], resolved_version: str) -> dict[str, Any]
416
541
  if scope_amendments is not None:
417
542
  brief["scope"]["amendments"] = scope_amendments
418
543
 
544
+ # Conditional: the component-library fields, verbatim, after amendments
545
+ # and in the schema doc's order. Absent -> keys omitted.
546
+ for field in ("registry_path", "ui_variants", "demo_patterns"):
547
+ value = ctx["scope"].get(field)
548
+ if value is not None:
549
+ brief["scope"][field] = value
550
+
419
551
  # Conditional: target_version (must equal version)
420
552
  tv = ctx.get("target_version")
421
553
  if tv is not None:
@@ -477,10 +609,11 @@ def render_yaml(brief: dict[str, Any]) -> str:
477
609
 
478
610
  The step 5 §3 template shows leading and trailing `---` markers, but those were
479
611
  wrapping the example YAML for documentation purposes — actual on-disk YAML uses
480
- only the leading `---` (or none). A trailing `---` would start a second empty
481
- document and break callers that use `yaml.safe_load` (which expects a single
482
- document) — and skf-create-skill / audit-skill / update-skill all use
483
- `yaml.safe_load`, so consistency with their loaders matters.
612
+ only the leading `---` (or none). A trailing `---` starts a second, empty
613
+ document: SKF's own readers skip it (they parse a brief with
614
+ skf-resolve-authoritative-files.py's parse_brief_yaml, which reads the SKF 1.x
615
+ briefs that ended so), but other YAML tools that expect a single document,
616
+ such as `yaml.safe_load`, refuse it, so the writer leaves it out.
484
617
  """
485
618
  body = yaml.safe_dump(
486
619
  brief,
@@ -528,6 +661,9 @@ _FLAT_SCOPE_KEYS = (
528
661
  "scope_rationale",
529
662
  "scope_tier_a_include",
530
663
  "scope_amendments",
664
+ "scope_registry_path",
665
+ "scope_ui_variants",
666
+ "scope_demo_patterns",
531
667
  )
532
668
 
533
669
 
@@ -589,23 +725,115 @@ def flat_to_nested(flat: dict[str, Any]) -> dict[str, Any]:
589
725
  scope["tier_a_include"] = flat["scope_tier_a_include"]
590
726
  if "scope_amendments" in flat and flat["scope_amendments"] is not None:
591
727
  scope["amendments"] = flat["scope_amendments"]
728
+ # Optional component-library fields: the same null-drop semantics.
729
+ for field in ("registry_path", "ui_variants", "demo_patterns"):
730
+ if flat.get(f"scope_{field}") is not None:
731
+ scope[field] = flat[f"scope_{field}"]
592
732
  nested["scope"] = scope
593
733
  return nested
594
734
 
595
735
 
596
- def cmd_write(target: Path, from_flat: bool = False) -> int:
597
- raw = sys.stdin.read()
598
- if not raw or not raw.strip():
599
- _die("write: empty stdin (expected JSON brief context)")
736
+ def _plain(value: Any) -> Any:
737
+ """A YAML-loaded value with each unquoted date turned into its ISO text."""
738
+ if isinstance(value, dict):
739
+ return {key: _plain(item) for key, item in value.items()}
740
+ if isinstance(value, list):
741
+ return [_plain(item) for item in value]
742
+ if isinstance(value, (datetime.date, datetime.datetime)):
743
+ return value.isoformat()
744
+ return value
745
+
746
+
747
+ def _overlay(base: dict[str, Any], patch: dict[str, Any]) -> dict[str, Any]:
748
+ """`patch` laid over `base`: an object merges key by key, null removes the key, any other value replaces."""
749
+ merged = dict(base)
750
+ for key, value in patch.items():
751
+ if value is None:
752
+ merged.pop(key, None)
753
+ elif isinstance(value, dict) and isinstance(merged.get(key), dict):
754
+ merged[key] = _overlay(merged[key], value)
755
+ else:
756
+ merged[key] = value
757
+ return merged
758
+
759
+
760
+ def _read_json_object(path: Path, flag: str) -> dict[str, Any]:
600
761
  try:
601
- ctx = json.loads(raw)
602
- except json.JSONDecodeError as e:
603
- _die(f"write: invalid JSON on stdin: {e}")
604
- if not isinstance(ctx, dict):
605
- _die("write: context payload must be a JSON object")
762
+ raw = path.read_bytes()
763
+ except OSError as e:
764
+ _die(f"write: cannot read the {flag} file {path}: {e}", code=2)
765
+ try:
766
+ data = json.loads(raw.decode("utf-8-sig"))
767
+ except (UnicodeDecodeError, json.JSONDecodeError) as e:
768
+ _die(f"write: the {flag} file {path} is not JSON: {e}")
769
+ if not isinstance(data, dict):
770
+ _die(f"write: the {flag} file {path} must hold a JSON object")
771
+ return data
772
+
773
+
774
+ RESOLVER = Path(__file__).resolve().parent / "skf-resolve-authoritative-files.py"
775
+
776
+
777
+ def _parse_brief_yaml(text: str) -> Any:
778
+ """A brief's YAML as skf-resolve-authoritative-files.py (from this folder)
779
+ reads it: an SKF 1.x brief's trailing `---` is no second document."""
780
+ spec = importlib.util.spec_from_file_location("skf_resolve_authoritative_files", RESOLVER)
781
+ if spec is None or spec.loader is None:
782
+ _die(f"cannot load {RESOLVER.name}", code=2)
783
+ resolver = importlib.util.module_from_spec(spec)
784
+ try:
785
+ spec.loader.exec_module(resolver)
786
+ except (OSError, ImportError, SyntaxError) as exc:
787
+ _die(f"cannot load {RESOLVER.name} beside {Path(__file__).name}: {exc}", code=2)
788
+ return resolver.parse_brief_yaml(text)
789
+
790
+
791
+ def base_context(base_brief: Path, doc_urls_file: Path | None, patch_file: Path | None) -> dict[str, Any]:
792
+ """The write context of the base brief form (see the module docstring)."""
793
+ try:
794
+ original = base_brief.read_bytes()
795
+ except OSError as e:
796
+ _die(f"write: cannot read the base brief {base_brief}: {e}", code=2)
797
+ try:
798
+ brief = _parse_brief_yaml(original.decode("utf-8-sig"))
799
+ except (UnicodeDecodeError, yaml.YAMLError) as e:
800
+ _die(f"write: the base brief {base_brief} is not YAML: {e}")
801
+ if not isinstance(brief, dict):
802
+ _die(f"write: the base brief {base_brief} holds no brief mapping")
803
+ ctx = _plain(brief)
804
+ if doc_urls_file is not None:
805
+ merged = _read_json_object(doc_urls_file, "--doc-urls-file")
806
+ if "doc_urls" not in merged:
807
+ _die(f"write: the --doc-urls-file file {doc_urls_file} holds no doc_urls", field="doc_urls")
808
+ ctx["doc_urls"] = merged["doc_urls"]
809
+ if patch_file is not None:
810
+ ctx = _overlay(ctx, _read_json_object(patch_file, "--patch-file"))
811
+ if ctx.get("version_resolved") is None:
812
+ ctx["version_resolved"] = ctx.get("version")
813
+ return ctx
814
+
815
+
816
+ def cmd_write(target: Path, from_flat: bool = False, base_brief: Path | None = None,
817
+ doc_urls_file: Path | None = None, patch_file: Path | None = None) -> int:
818
+ if base_brief is not None:
819
+ if from_flat:
820
+ _die("write: --from-flat and --base-brief exclude each other")
821
+ ctx = base_context(base_brief, doc_urls_file, patch_file)
822
+ elif doc_urls_file is not None or patch_file is not None:
823
+ _die("write: --doc-urls-file and --patch-file change a --base-brief")
824
+ else:
825
+ raw = sys.stdin.read()
826
+ if not raw or not raw.strip():
827
+ _die("write: empty stdin (expected JSON brief context)")
828
+ try:
829
+ ctx = json.loads(raw)
830
+ except json.JSONDecodeError as e:
831
+ _die(f"write: invalid JSON on stdin: {e}")
832
+ if not isinstance(ctx, dict):
833
+ _die("write: context payload must be a JSON object")
606
834
 
607
- if from_flat:
608
- ctx = flat_to_nested(ctx)
835
+ if from_flat:
836
+ ctx = flat_to_nested(ctx)
609
837
 
610
838
  warnings = validate_context(ctx)
611
839
  resolved_version = resolve_version(ctx)
@@ -631,6 +859,121 @@ def cmd_write(target: Path, from_flat: bool = False) -> int:
631
859
  return 0
632
860
 
633
861
 
862
+ AMEND_FIELDS = ("registry_path", "demo_patterns", "include", "amendments")
863
+ BRIEF_VALIDATOR = Path(__file__).resolve().parent / "skf-validate-brief-schema.py"
864
+
865
+
866
+ def _brief_errors(brief: dict[str, Any]) -> list[dict[str, Any]]:
867
+ """What skf-validate-brief-schema.py reports for `brief` as it would read it on disk."""
868
+ spec = importlib.util.spec_from_file_location("skf_validate_brief_schema", BRIEF_VALIDATOR)
869
+ if spec is None or spec.loader is None:
870
+ _die(f"amend: cannot load {BRIEF_VALIDATOR.name}", code=2)
871
+ validator = importlib.util.module_from_spec(spec)
872
+ spec.loader.exec_module(validator)
873
+ return validator.validate_brief(yaml.safe_load(render_yaml(brief)))["errors"]
874
+
875
+
876
+ def _added_errors(before: dict[str, Any], after: dict[str, Any]) -> list[dict[str, Any]]:
877
+ """The schema errors `after` has that `before` did not: an amend refuses
878
+ only what it breaks, never a brief for an error it already held."""
879
+ known = {json.dumps(e, sort_keys=True) for e in _brief_errors(before)}
880
+ return [e for e in _brief_errors(after) if json.dumps(e, sort_keys=True) not in known]
881
+
882
+
883
+ def _check_include(paths: Any) -> list[str]:
884
+ """The literal paths of an amend payload's `include`."""
885
+ if not isinstance(paths, list) or not all(isinstance(p, str) and p.strip() for p in paths):
886
+ _die("amend: include must be an array of non-empty paths", field="scope.include")
887
+ return paths
888
+
889
+
890
+ def _check_amendments(entries: Any) -> list[dict[str, Any]]:
891
+ """The amendment entries of an amend payload, checked (see the docstring)."""
892
+ if not isinstance(entries, list):
893
+ _die("amend: amendments must be an array of amendment objects", field="scope.amendments")
894
+ for i, entry in enumerate(entries):
895
+ if not isinstance(entry, dict):
896
+ _die(f"amend: amendments[{i}] must be an object", field="scope.amendments")
897
+ for key in ("path", "action", "reason", "workflow"):
898
+ if not isinstance(entry.get(key), str) or not entry[key]:
899
+ _die(f"amend: amendments[{i}].{key} must be a non-empty string", field="scope.amendments")
900
+ if not isinstance(entry.get("date"), str) or not ISO_DATE_RE.match(entry["date"]):
901
+ _die(f"amend: amendments[{i}].date must be an ISO date (YYYY-MM-DD)", field="scope.amendments")
902
+ category = entry.get("category")
903
+ if category is not None and (not isinstance(category, str) or not category):
904
+ _die(f"amend: amendments[{i}].category must be a non-empty string when present",
905
+ field="scope.amendments")
906
+ return entries
907
+
908
+
909
+ def cmd_amend(target: Path) -> int:
910
+ raw = sys.stdin.read()
911
+ try:
912
+ payload = json.loads(raw) if raw.strip() else None
913
+ except json.JSONDecodeError as e:
914
+ _die(f"amend: invalid JSON on stdin: {e}")
915
+ if not isinstance(payload, dict):
916
+ _die("amend: stdin must be a JSON object holding registry_path, demo_patterns, include or amendments")
917
+ unknown = sorted(set(payload) - set(AMEND_FIELDS))
918
+ if unknown:
919
+ _die(f"amend: unknown field(s) {unknown}; expected some of {list(AMEND_FIELDS)}")
920
+ given = [field for field in AMEND_FIELDS if payload.get(field) not in (None, [])]
921
+ if not given:
922
+ _die("amend: nothing to amend")
923
+ entries = _check_amendments(payload["amendments"]) if "amendments" in given else []
924
+ include = _check_include(payload["include"]) if "include" in given else []
925
+
926
+ try:
927
+ original = target.read_bytes()
928
+ except OSError as e:
929
+ _die(f"amend: cannot read {target}: {e}", code=2)
930
+ try:
931
+ brief = _parse_brief_yaml(original.decode("utf-8-sig"))
932
+ except (UnicodeDecodeError, yaml.YAMLError) as e:
933
+ _die(f"amend: {target} is not a YAML brief: {e}")
934
+ if not isinstance(brief, dict) or not isinstance(brief.get("scope"), dict):
935
+ _die(f"amend: {target} holds no scope mapping", field="scope")
936
+ before = yaml.safe_load(render_yaml(brief))
937
+ scope = brief["scope"]
938
+ if entries:
939
+ existing = scope.get("amendments")
940
+ if existing is not None and not isinstance(existing, list):
941
+ _die("amend: the brief's scope.amendments is not an array", field="scope.amendments")
942
+ scope["amendments"] = [*(existing or []), *entries]
943
+ for field in ("registry_path", "demo_patterns"):
944
+ if field in given:
945
+ scope[field] = payload[field]
946
+ _check_component_fields(scope)
947
+ included: list[str] = []
948
+ current = scope.get("include")
949
+ if include and isinstance(current, list) and current:
950
+ for path in include:
951
+ if path not in current and path not in included:
952
+ included.append(path)
953
+ scope["include"] = [*current, *included]
954
+ errors = _added_errors(before, brief)
955
+ if errors:
956
+ _die(f"amend: the amendment breaks skill-brief.v1.json: {errors[0].get('message')}",
957
+ field=errors[0].get("field"))
958
+
959
+ backup = target.with_name(target.name + ".bak")
960
+ try:
961
+ backup.write_bytes(original)
962
+ except OSError as e:
963
+ _die(f"amend: cannot write {backup}: {e}", code=2)
964
+ bytes_written = atomic_write(target, render_yaml(brief))
965
+ print(json.dumps({
966
+ "status": "ok",
967
+ "brief_path": str(target.resolve()),
968
+ "backup": str(backup.resolve()),
969
+ "set": [field for field in given if field in ("registry_path", "demo_patterns")],
970
+ "included": included,
971
+ "appended": len(entries),
972
+ "bytes": bytes_written,
973
+ }))
974
+ return 0
975
+
976
+
634
977
  def main() -> int:
635
978
  parser = argparse.ArgumentParser(
636
979
  prog="skf-write-skill-brief",
@@ -650,12 +993,50 @@ def main() -> int:
650
993
  "assembly site in step 5."
651
994
  ),
652
995
  )
996
+ p_write.add_argument("--base-brief", type=Path, metavar="FILE",
997
+ help="start from the brief in FILE (a skill-brief.yaml) instead of reading stdin")
998
+ p_write.add_argument("--doc-urls-file", type=Path, metavar="FILE",
999
+ help="with --base-brief: replace doc_urls with the doc_urls of FILE "
1000
+ "(skf-merge-doc-urls.py output)")
1001
+ p_write.add_argument("--patch-file", type=Path, metavar="FILE",
1002
+ help="with --base-brief: lay the JSON object in FILE over the brief "
1003
+ "(objects merge, null removes, other values replace)")
1004
+
1005
+ p_amend = sub.add_parser(
1006
+ "amend",
1007
+ help=(
1008
+ "Apply answers (JSON on stdin) to an existing brief: set scope.registry_path and "
1009
+ "scope.demo_patterns, append paths to scope.include and entries to scope.amendments, "
1010
+ "check the brief schema, keep a .bak copy, atomic write"
1011
+ ),
1012
+ )
1013
+ p_amend.add_argument("--target", type=Path, required=True, help="Path to the skill-brief.yaml to amend")
653
1014
 
654
1015
  args = parser.parse_args()
655
1016
  if args.cmd == "write":
656
- return cmd_write(args.target, from_flat=args.from_flat)
1017
+ return cmd_write(args.target, from_flat=args.from_flat, base_brief=args.base_brief,
1018
+ doc_urls_file=args.doc_urls_file, patch_file=args.patch_file)
1019
+ if args.cmd == "amend":
1020
+ return cmd_amend(args.target)
657
1021
  return 2
658
1022
 
659
1023
 
1024
+ def _force_utf8(*streams) -> None:
1025
+ """Reconfigure stdin, stdout and stderr to UTF-8, keeping each stream's error handler.
1026
+
1027
+ A Windows console pipes them as cp1252, which would garble a payload's
1028
+ non-ASCII text (a description, an amendment's reason) read from stdin
1029
+ (reconfigured here, before the first read).
1030
+ """
1031
+ for stream in streams:
1032
+ if hasattr(stream, "reconfigure"):
1033
+ errors = getattr(stream, "errors", None)
1034
+ if errors is None:
1035
+ stream.reconfigure(encoding="utf-8")
1036
+ else:
1037
+ stream.reconfigure(encoding="utf-8", errors=errors)
1038
+
1039
+
660
1040
  if __name__ == "__main__":
1041
+ _force_utf8(sys.stdin, sys.stdout, sys.stderr)
661
1042
  sys.exit(main())