bmad-module-skill-forge 2.1.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 +51 -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 +14 -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 +117 -9
  75. package/src/shared/scripts/skf-detect-docs.py +892 -4
  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 +215 -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 +17 -6
  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 +55 -18
  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 -57
  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 -89
  348. package/src/skf-setup/references/detect-and-tier.md +58 -66
  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 -645
  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,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,7 +60,12 @@ 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:
@@ -66,7 +84,9 @@ Version precedence (resolved into the rendered YAML's `version` field):
66
84
  1. version_resolved if explicitly supplied (caller already ran the
67
85
  precedence rule). Used by step 5 when it has confirmed values.
68
86
  2. Otherwise: target_version if non-null.
69
- 3. Otherwise: detected_version if non-null.
87
+ 3. Otherwise: detected_version when it is full X.Y.Z semver (SEMVER_RE);
88
+ a detected version of another shape (`0.1`, `2.0.0rc1`) is passed as
89
+ found and skipped here, with a warning.
70
90
  4. Otherwise: "1.0.0".
71
91
 
72
92
  When target_version is set, the rendered YAML includes a `target_version`
@@ -98,6 +118,9 @@ Flat input form (`--from-flat`):
98
118
  "scope_notes": "",
99
119
  "scope_tier_a_include": null | ["code/core/src/**"],
100
120
  "scope_amendments": null | [{...}],
121
+ "scope_registry_path": null | "registry/index.ts",
122
+ "scope_ui_variants": null | [{"name": "...", "package": "..."}],
123
+ "scope_demo_patterns": null | ["**/demo/**"],
101
124
  "doc_urls": null | [{"url": "...", "label": "...", "source": "..."}],
102
125
  "scripts_intent": null | "detect" | "none" | "...",
103
126
  "assets_intent": null | "detect" | "none" | "...",
@@ -106,6 +129,21 @@ Flat input form (`--from-flat`):
106
129
  "source_ref": null | "v0.5.0"
107
130
  }
108
131
 
132
+ Base brief form (`write --base-brief <file> [--doc-urls-file <file>]
133
+ [--patch-file <file>]`):
134
+
135
+ The context is the brief in --base-brief (a skill-brief.yaml), so a
136
+ caller that rewrites a brief never retypes it and no field it holds is
137
+ dropped. --doc-urls-file replaces its doc_urls with the `doc_urls` of
138
+ that file (the output of skf-merge-doc-urls.py). --patch-file lays a
139
+ JSON object of changes over it: an object merges key by key, null
140
+ removes the key, and any other value (a string, a list) replaces it.
141
+ version_resolved is the patch's version_resolved, else the result's
142
+ `version`, so the version the brief holds (or the one the patch sets)
143
+ is written; a brief with a target_version needs the patch to change
144
+ both. stdin is not read, and --from-flat does not apply. A date the
145
+ YAML holds unquoted is read as its ISO text.
146
+
109
147
  Output (success):
110
148
 
111
149
  {
@@ -116,6 +154,37 @@ Output (success):
116
154
  "warnings": ["string", ...]
117
155
  }
118
156
 
157
+ Answers payload (consumed by `amend`), each key optional, one at least:
158
+
159
+ {
160
+ "registry_path": "registry/index.ts",
161
+ "demo_patterns": ["**/examples/**"],
162
+ "include": ["docs/llms.txt"],
163
+ "amendments": [{"path": "...", "action": "...", "category": "...",
164
+ "reason": "...", "evidence": "...",
165
+ "date": "2026-10-01", "workflow": "..."}]
166
+ }
167
+
168
+ Each amendment needs a non-empty path, action, reason and workflow and
169
+ an ISO date; category, when given, is a non-empty string.
170
+
171
+ `include` holds literal paths, each appended to scope.include unless the
172
+ list already holds it. An empty scope.include already covers every file,
173
+ so it stays empty: appending one path would narrow the scope to that
174
+ file. The output's `included` lists the paths appended.
175
+
176
+ Output of `amend` (success):
177
+
178
+ {
179
+ "status": "ok",
180
+ "brief_path": "/abs/path/skill-brief.yaml",
181
+ "backup": "/abs/path/skill-brief.yaml.bak",
182
+ "set": ["registry_path", "demo_patterns"], # the fields given
183
+ "included": ["docs/llms.txt"], # paths appended to scope.include
184
+ "appended": <integer>, # amendment entries
185
+ "bytes": <integer>
186
+ }
187
+
119
188
  Errors emit `{"status": "error", "message": "...", "field": "..."|null}`
120
189
  to stderr and exit non-zero.
121
190
 
@@ -123,7 +192,11 @@ Exit codes:
123
192
  0 — success
124
193
  1 — validation failure (bad context, schema violation, invariant
125
194
  violation, version-precedence underflow with no fallback path)
195
+ or, for amend, a bad payload, a brief with no scope mapping or
196
+ an amendment that adds a skill-brief.v1.json error
126
197
  2 — I/O failure (atomic write failed, parent directory not writable)
198
+ or, for amend, a brief that cannot be read or a backup that
199
+ cannot be written
127
200
 
128
201
  Cross-platform: pure stdlib + PyYAML. Atomic write via temp + fsync +
129
202
  rename, mirroring skf-atomic-write.py and the helper in
@@ -133,6 +206,8 @@ skf-forge-tier-rw.py.
133
206
  from __future__ import annotations
134
207
 
135
208
  import argparse
209
+ import datetime
210
+ import importlib.util
136
211
  import json
137
212
  import os
138
213
  import re
@@ -176,6 +251,9 @@ def resolve_version(ctx: dict[str, Any]) -> str:
176
251
  Uses `is not None` checks (not truthiness) so an explicitly-supplied
177
252
  empty string surfaces as a SEMVER_RE validation failure downstream
178
253
  rather than silently falling through to the next precedence level.
254
+ The detected version is the exception: callers pass it as the manifest
255
+ gives it, so one that is not full X.Y.Z semver falls through to the
256
+ default, as validate_context's warning says.
179
257
  """
180
258
  vr = ctx.get("version_resolved")
181
259
  if vr is not None:
@@ -184,7 +262,7 @@ def resolve_version(ctx: dict[str, Any]) -> str:
184
262
  if tv is not None:
185
263
  return tv
186
264
  dv = ctx.get("detected_version")
187
- if dv is not None:
265
+ if isinstance(dv, str) and SEMVER_RE.match(dv):
188
266
  return dv
189
267
  return "1.0.0"
190
268
 
@@ -333,6 +411,8 @@ def validate_context(ctx: dict[str, Any]) -> list[str]:
333
411
  if not isinstance(entry, dict):
334
412
  _die(f"scope.amendments[{i}] must be an object", field="scope.amendments")
335
413
 
414
+ _check_component_fields(scope)
415
+
336
416
  # target_ref / source_ref — optional git refs (top-level). target_ref is a
337
417
  # remote-monorepo tag escape hatch; source_ref is the auto-resolved ref.
338
418
  # Both must round-trip on a ratify/re-write rather than being dropped.
@@ -361,6 +441,36 @@ def validate_context(ctx: dict[str, Any]) -> list[str]:
361
441
  return warnings
362
442
 
363
443
 
444
+ def _check_component_fields(scope: dict[str, Any]) -> None:
445
+ """scope.registry_path / scope.ui_variants / scope.demo_patterns: optional
446
+ component-library fields. A brief can author them, and create-skill
447
+ step 3d writes a confirmed registry path and demo patterns back (amend),
448
+ so a ratify/re-write preserves them verbatim. Light checks mirror the
449
+ schema."""
450
+ registry_path = scope.get("registry_path")
451
+ if registry_path is not None and (not isinstance(registry_path, str) or not registry_path):
452
+ _die("scope.registry_path must be a non-empty string when present", field="scope.registry_path")
453
+ demo_patterns = scope.get("demo_patterns")
454
+ if demo_patterns is not None:
455
+ if not isinstance(demo_patterns, list):
456
+ _die("scope.demo_patterns must be an array of glob strings", field="scope.demo_patterns")
457
+ for i, pat in enumerate(demo_patterns):
458
+ if not isinstance(pat, str) or not pat:
459
+ _die(f"scope.demo_patterns[{i}] must be a non-empty glob string", field="scope.demo_patterns")
460
+ ui_variants = scope.get("ui_variants")
461
+ if ui_variants is not None:
462
+ if not isinstance(ui_variants, list):
463
+ _die("scope.ui_variants must be an array of variant objects", field="scope.ui_variants")
464
+ for i, variant in enumerate(ui_variants):
465
+ if not isinstance(variant, dict):
466
+ _die(f"scope.ui_variants[{i}] must be an object", field="scope.ui_variants")
467
+ for key in ("name", "package"):
468
+ value = variant.get(key)
469
+ if value is not None and (not isinstance(value, str) or not value):
470
+ _die(f"scope.ui_variants[{i}].{key} must be a non-empty string when present",
471
+ field="scope.ui_variants")
472
+
473
+
364
474
  def assemble_brief(ctx: dict[str, Any], resolved_version: str) -> dict[str, Any]:
365
475
  """Build the final brief dict that will be YAML-dumped, in canonical key order."""
366
476
  source_type = ctx.get("source_type", "source")
@@ -416,6 +526,13 @@ def assemble_brief(ctx: dict[str, Any], resolved_version: str) -> dict[str, Any]
416
526
  if scope_amendments is not None:
417
527
  brief["scope"]["amendments"] = scope_amendments
418
528
 
529
+ # Conditional: the component-library fields, verbatim, after amendments
530
+ # and in the schema doc's order. Absent -> keys omitted.
531
+ for field in ("registry_path", "ui_variants", "demo_patterns"):
532
+ value = ctx["scope"].get(field)
533
+ if value is not None:
534
+ brief["scope"][field] = value
535
+
419
536
  # Conditional: target_version (must equal version)
420
537
  tv = ctx.get("target_version")
421
538
  if tv is not None:
@@ -528,6 +645,9 @@ _FLAT_SCOPE_KEYS = (
528
645
  "scope_rationale",
529
646
  "scope_tier_a_include",
530
647
  "scope_amendments",
648
+ "scope_registry_path",
649
+ "scope_ui_variants",
650
+ "scope_demo_patterns",
531
651
  )
532
652
 
533
653
 
@@ -589,23 +709,98 @@ def flat_to_nested(flat: dict[str, Any]) -> dict[str, Any]:
589
709
  scope["tier_a_include"] = flat["scope_tier_a_include"]
590
710
  if "scope_amendments" in flat and flat["scope_amendments"] is not None:
591
711
  scope["amendments"] = flat["scope_amendments"]
712
+ # Optional component-library fields: the same null-drop semantics.
713
+ for field in ("registry_path", "ui_variants", "demo_patterns"):
714
+ if flat.get(f"scope_{field}") is not None:
715
+ scope[field] = flat[f"scope_{field}"]
592
716
  nested["scope"] = scope
593
717
  return nested
594
718
 
595
719
 
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)")
720
+ def _plain(value: Any) -> Any:
721
+ """A YAML-loaded value with each unquoted date turned into its ISO text."""
722
+ if isinstance(value, dict):
723
+ return {key: _plain(item) for key, item in value.items()}
724
+ if isinstance(value, list):
725
+ return [_plain(item) for item in value]
726
+ if isinstance(value, (datetime.date, datetime.datetime)):
727
+ return value.isoformat()
728
+ return value
729
+
730
+
731
+ def _overlay(base: dict[str, Any], patch: dict[str, Any]) -> dict[str, Any]:
732
+ """`patch` laid over `base`: an object merges key by key, null removes the key, any other value replaces."""
733
+ merged = dict(base)
734
+ for key, value in patch.items():
735
+ if value is None:
736
+ merged.pop(key, None)
737
+ elif isinstance(value, dict) and isinstance(merged.get(key), dict):
738
+ merged[key] = _overlay(merged[key], value)
739
+ else:
740
+ merged[key] = value
741
+ return merged
742
+
743
+
744
+ def _read_json_object(path: Path, flag: str) -> dict[str, Any]:
600
745
  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")
746
+ raw = path.read_bytes()
747
+ except OSError as e:
748
+ _die(f"write: cannot read the {flag} file {path}: {e}", code=2)
749
+ try:
750
+ data = json.loads(raw.decode("utf-8-sig"))
751
+ except (UnicodeDecodeError, json.JSONDecodeError) as e:
752
+ _die(f"write: the {flag} file {path} is not JSON: {e}")
753
+ if not isinstance(data, dict):
754
+ _die(f"write: the {flag} file {path} must hold a JSON object")
755
+ return data
756
+
757
+
758
+ def base_context(base_brief: Path, doc_urls_file: Path | None, patch_file: Path | None) -> dict[str, Any]:
759
+ """The write context of the base brief form (see the module docstring)."""
760
+ try:
761
+ original = base_brief.read_bytes()
762
+ except OSError as e:
763
+ _die(f"write: cannot read the base brief {base_brief}: {e}", code=2)
764
+ try:
765
+ brief = yaml.safe_load(original.decode("utf-8-sig"))
766
+ except (UnicodeDecodeError, yaml.YAMLError) as e:
767
+ _die(f"write: the base brief {base_brief} is not YAML: {e}")
768
+ if not isinstance(brief, dict):
769
+ _die(f"write: the base brief {base_brief} holds no brief mapping")
770
+ ctx = _plain(brief)
771
+ if doc_urls_file is not None:
772
+ merged = _read_json_object(doc_urls_file, "--doc-urls-file")
773
+ if "doc_urls" not in merged:
774
+ _die(f"write: the --doc-urls-file file {doc_urls_file} holds no doc_urls", field="doc_urls")
775
+ ctx["doc_urls"] = merged["doc_urls"]
776
+ if patch_file is not None:
777
+ ctx = _overlay(ctx, _read_json_object(patch_file, "--patch-file"))
778
+ if ctx.get("version_resolved") is None:
779
+ ctx["version_resolved"] = ctx.get("version")
780
+ return ctx
781
+
782
+
783
+ def cmd_write(target: Path, from_flat: bool = False, base_brief: Path | None = None,
784
+ doc_urls_file: Path | None = None, patch_file: Path | None = None) -> int:
785
+ if base_brief is not None:
786
+ if from_flat:
787
+ _die("write: --from-flat and --base-brief exclude each other")
788
+ ctx = base_context(base_brief, doc_urls_file, patch_file)
789
+ elif doc_urls_file is not None or patch_file is not None:
790
+ _die("write: --doc-urls-file and --patch-file change a --base-brief")
791
+ else:
792
+ raw = sys.stdin.read()
793
+ if not raw or not raw.strip():
794
+ _die("write: empty stdin (expected JSON brief context)")
795
+ try:
796
+ ctx = json.loads(raw)
797
+ except json.JSONDecodeError as e:
798
+ _die(f"write: invalid JSON on stdin: {e}")
799
+ if not isinstance(ctx, dict):
800
+ _die("write: context payload must be a JSON object")
606
801
 
607
- if from_flat:
608
- ctx = flat_to_nested(ctx)
802
+ if from_flat:
803
+ ctx = flat_to_nested(ctx)
609
804
 
610
805
  warnings = validate_context(ctx)
611
806
  resolved_version = resolve_version(ctx)
@@ -631,6 +826,121 @@ def cmd_write(target: Path, from_flat: bool = False) -> int:
631
826
  return 0
632
827
 
633
828
 
829
+ AMEND_FIELDS = ("registry_path", "demo_patterns", "include", "amendments")
830
+ BRIEF_VALIDATOR = Path(__file__).resolve().parent / "skf-validate-brief-schema.py"
831
+
832
+
833
+ def _brief_errors(brief: dict[str, Any]) -> list[dict[str, Any]]:
834
+ """What skf-validate-brief-schema.py reports for `brief` as it would read it on disk."""
835
+ spec = importlib.util.spec_from_file_location("skf_validate_brief_schema", BRIEF_VALIDATOR)
836
+ if spec is None or spec.loader is None:
837
+ _die(f"amend: cannot load {BRIEF_VALIDATOR.name}", code=2)
838
+ validator = importlib.util.module_from_spec(spec)
839
+ spec.loader.exec_module(validator)
840
+ return validator.validate_brief(yaml.safe_load(render_yaml(brief)))["errors"]
841
+
842
+
843
+ def _added_errors(before: dict[str, Any], after: dict[str, Any]) -> list[dict[str, Any]]:
844
+ """The schema errors `after` has that `before` did not: an amend refuses
845
+ only what it breaks, never a brief for an error it already held."""
846
+ known = {json.dumps(e, sort_keys=True) for e in _brief_errors(before)}
847
+ return [e for e in _brief_errors(after) if json.dumps(e, sort_keys=True) not in known]
848
+
849
+
850
+ def _check_include(paths: Any) -> list[str]:
851
+ """The literal paths of an amend payload's `include`."""
852
+ if not isinstance(paths, list) or not all(isinstance(p, str) and p.strip() for p in paths):
853
+ _die("amend: include must be an array of non-empty paths", field="scope.include")
854
+ return paths
855
+
856
+
857
+ def _check_amendments(entries: Any) -> list[dict[str, Any]]:
858
+ """The amendment entries of an amend payload, checked (see the docstring)."""
859
+ if not isinstance(entries, list):
860
+ _die("amend: amendments must be an array of amendment objects", field="scope.amendments")
861
+ for i, entry in enumerate(entries):
862
+ if not isinstance(entry, dict):
863
+ _die(f"amend: amendments[{i}] must be an object", field="scope.amendments")
864
+ for key in ("path", "action", "reason", "workflow"):
865
+ if not isinstance(entry.get(key), str) or not entry[key]:
866
+ _die(f"amend: amendments[{i}].{key} must be a non-empty string", field="scope.amendments")
867
+ if not isinstance(entry.get("date"), str) or not ISO_DATE_RE.match(entry["date"]):
868
+ _die(f"amend: amendments[{i}].date must be an ISO date (YYYY-MM-DD)", field="scope.amendments")
869
+ category = entry.get("category")
870
+ if category is not None and (not isinstance(category, str) or not category):
871
+ _die(f"amend: amendments[{i}].category must be a non-empty string when present",
872
+ field="scope.amendments")
873
+ return entries
874
+
875
+
876
+ def cmd_amend(target: Path) -> int:
877
+ raw = sys.stdin.read()
878
+ try:
879
+ payload = json.loads(raw) if raw.strip() else None
880
+ except json.JSONDecodeError as e:
881
+ _die(f"amend: invalid JSON on stdin: {e}")
882
+ if not isinstance(payload, dict):
883
+ _die("amend: stdin must be a JSON object holding registry_path, demo_patterns, include or amendments")
884
+ unknown = sorted(set(payload) - set(AMEND_FIELDS))
885
+ if unknown:
886
+ _die(f"amend: unknown field(s) {unknown}; expected some of {list(AMEND_FIELDS)}")
887
+ given = [field for field in AMEND_FIELDS if payload.get(field) not in (None, [])]
888
+ if not given:
889
+ _die("amend: nothing to amend")
890
+ entries = _check_amendments(payload["amendments"]) if "amendments" in given else []
891
+ include = _check_include(payload["include"]) if "include" in given else []
892
+
893
+ try:
894
+ original = target.read_bytes()
895
+ except OSError as e:
896
+ _die(f"amend: cannot read {target}: {e}", code=2)
897
+ try:
898
+ brief = yaml.safe_load(original.decode("utf-8-sig"))
899
+ except (UnicodeDecodeError, yaml.YAMLError) as e:
900
+ _die(f"amend: {target} is not a YAML brief: {e}")
901
+ if not isinstance(brief, dict) or not isinstance(brief.get("scope"), dict):
902
+ _die(f"amend: {target} holds no scope mapping", field="scope")
903
+ before = yaml.safe_load(render_yaml(brief))
904
+ scope = brief["scope"]
905
+ if entries:
906
+ existing = scope.get("amendments")
907
+ if existing is not None and not isinstance(existing, list):
908
+ _die("amend: the brief's scope.amendments is not an array", field="scope.amendments")
909
+ scope["amendments"] = [*(existing or []), *entries]
910
+ for field in ("registry_path", "demo_patterns"):
911
+ if field in given:
912
+ scope[field] = payload[field]
913
+ _check_component_fields(scope)
914
+ included: list[str] = []
915
+ current = scope.get("include")
916
+ if include and isinstance(current, list) and current:
917
+ for path in include:
918
+ if path not in current and path not in included:
919
+ included.append(path)
920
+ scope["include"] = [*current, *included]
921
+ errors = _added_errors(before, brief)
922
+ if errors:
923
+ _die(f"amend: the amendment breaks skill-brief.v1.json: {errors[0].get('message')}",
924
+ field=errors[0].get("field"))
925
+
926
+ backup = target.with_name(target.name + ".bak")
927
+ try:
928
+ backup.write_bytes(original)
929
+ except OSError as e:
930
+ _die(f"amend: cannot write {backup}: {e}", code=2)
931
+ bytes_written = atomic_write(target, render_yaml(brief))
932
+ print(json.dumps({
933
+ "status": "ok",
934
+ "brief_path": str(target.resolve()),
935
+ "backup": str(backup.resolve()),
936
+ "set": [field for field in given if field in ("registry_path", "demo_patterns")],
937
+ "included": included,
938
+ "appended": len(entries),
939
+ "bytes": bytes_written,
940
+ }))
941
+ return 0
942
+
943
+
634
944
  def main() -> int:
635
945
  parser = argparse.ArgumentParser(
636
946
  prog="skf-write-skill-brief",
@@ -650,12 +960,50 @@ def main() -> int:
650
960
  "assembly site in step 5."
651
961
  ),
652
962
  )
963
+ p_write.add_argument("--base-brief", type=Path, metavar="FILE",
964
+ help="start from the brief in FILE (a skill-brief.yaml) instead of reading stdin")
965
+ p_write.add_argument("--doc-urls-file", type=Path, metavar="FILE",
966
+ help="with --base-brief: replace doc_urls with the doc_urls of FILE "
967
+ "(skf-merge-doc-urls.py output)")
968
+ p_write.add_argument("--patch-file", type=Path, metavar="FILE",
969
+ help="with --base-brief: lay the JSON object in FILE over the brief "
970
+ "(objects merge, null removes, other values replace)")
971
+
972
+ p_amend = sub.add_parser(
973
+ "amend",
974
+ help=(
975
+ "Apply answers (JSON on stdin) to an existing brief: set scope.registry_path and "
976
+ "scope.demo_patterns, append paths to scope.include and entries to scope.amendments, "
977
+ "check the brief schema, keep a .bak copy, atomic write"
978
+ ),
979
+ )
980
+ p_amend.add_argument("--target", type=Path, required=True, help="Path to the skill-brief.yaml to amend")
653
981
 
654
982
  args = parser.parse_args()
655
983
  if args.cmd == "write":
656
- return cmd_write(args.target, from_flat=args.from_flat)
984
+ return cmd_write(args.target, from_flat=args.from_flat, base_brief=args.base_brief,
985
+ doc_urls_file=args.doc_urls_file, patch_file=args.patch_file)
986
+ if args.cmd == "amend":
987
+ return cmd_amend(args.target)
657
988
  return 2
658
989
 
659
990
 
991
+ def _force_utf8(*streams) -> None:
992
+ """Reconfigure stdin, stdout and stderr to UTF-8, keeping each stream's error handler.
993
+
994
+ A Windows console pipes them as cp1252, which would garble a payload's
995
+ non-ASCII text (a description, an amendment's reason) read from stdin
996
+ (reconfigured here, before the first read).
997
+ """
998
+ for stream in streams:
999
+ if hasattr(stream, "reconfigure"):
1000
+ errors = getattr(stream, "errors", None)
1001
+ if errors is None:
1002
+ stream.reconfigure(encoding="utf-8")
1003
+ else:
1004
+ stream.reconfigure(encoding="utf-8", errors=errors)
1005
+
1006
+
660
1007
  if __name__ == "__main__":
1008
+ _force_utf8(sys.stdin, sys.stdout, sys.stderr)
661
1009
  sys.exit(main())