bmad-module-skill-forge 2.2.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (437) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +49 -49
  3. package/docs/404.md +10 -8
  4. package/docs/_data/pinned.yaml +22 -13
  5. package/docs/_internal/STABILITY.md +58 -35
  6. package/docs/agents.md +39 -53
  7. package/docs/architecture.md +65 -51
  8. package/docs/bmad-synergy.md +53 -51
  9. package/docs/campaign.md +127 -75
  10. package/docs/concepts.md +34 -28
  11. package/docs/examples.md +70 -68
  12. package/docs/forge-auto.md +34 -31
  13. package/docs/getting-started.md +118 -57
  14. package/docs/how-it-works.md +25 -21
  15. package/docs/index.md +15 -15
  16. package/docs/skill-model.md +70 -59
  17. package/docs/troubleshooting.md +143 -18
  18. package/docs/verifying-a-skill.md +87 -57
  19. package/docs/why-skf.md +24 -22
  20. package/docs/workflows.md +274 -110
  21. package/package.json +18 -10
  22. package/src/forger/preferences.yaml +8 -1
  23. package/src/knowledge/agentskills-spec.md +2 -0
  24. package/src/knowledge/ccc-bridge.md +40 -24
  25. package/src/knowledge/confidence-tiers.md +9 -5
  26. package/src/knowledge/overview.md +3 -3
  27. package/src/knowledge/progressive-capability.md +8 -8
  28. package/src/knowledge/provenance-tracking.md +9 -5
  29. package/src/knowledge/qmd-registry.md +3 -3
  30. package/src/knowledge/skf-knowledge-index.csv +2 -2
  31. package/src/knowledge/skill-lifecycle.md +18 -11
  32. package/src/knowledge/split-body-strategy.md +0 -3
  33. package/src/knowledge/tool-resolution.md +4 -2
  34. package/src/knowledge/version-paths.md +101 -15
  35. package/src/module-help.csv +3 -3
  36. package/src/module.yaml +10 -0
  37. package/src/shared/data/ast-grep-recipes.yaml +608 -0
  38. package/src/shared/data/ide-context-files.json +30 -0
  39. package/src/shared/health-check.md +31 -59
  40. package/src/shared/references/description-guard-protocol.md +12 -9
  41. package/src/shared/references/feasibility-report-schema.md +19 -13
  42. package/src/shared/references/headless-gate-convention.md +55 -3
  43. package/src/shared/references/output-contract-schema.md +19 -8
  44. package/src/shared/references/pipeline-contracts.md +94 -29
  45. package/src/shared/references/tessl-review.md +77 -0
  46. package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
  47. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
  48. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
  49. package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
  50. package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
  51. package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
  52. package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
  53. package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
  54. package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
  55. package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
  56. package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
  57. package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
  58. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
  59. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
  60. package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
  61. package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
  62. package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
  63. package/src/shared/scripts/skf-atomic-write.py +233 -64
  64. package/src/shared/scripts/skf-build-change-manifest.py +2753 -28
  65. package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
  66. package/src/shared/scripts/skf-check-unit-records.py +184 -0
  67. package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
  68. package/src/shared/scripts/skf-classify-changed-files.py +772 -0
  69. package/src/shared/scripts/skf-comention-pairs.py +1333 -123
  70. package/src/shared/scripts/skf-compare-file-hashes.py +267 -105
  71. package/src/shared/scripts/skf-count-imports.py +1867 -0
  72. package/src/shared/scripts/skf-count-tokens.py +1 -1
  73. package/src/shared/scripts/skf-derive-assembly-shape.py +21 -3
  74. package/src/shared/scripts/skf-description-guard.py +99 -11
  75. package/src/shared/scripts/skf-detect-docs.py +694 -2
  76. package/src/shared/scripts/skf-detect-language.py +486 -107
  77. package/src/shared/scripts/skf-detect-registry.py +887 -0
  78. package/src/shared/scripts/skf-detect-scripts-assets.py +197 -55
  79. package/src/shared/scripts/skf-detect-tools.py +209 -27
  80. package/src/shared/scripts/skf-detect-workspaces.py +506 -30
  81. package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
  82. package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
  83. package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
  84. package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
  85. package/src/shared/scripts/skf-extract-public-api.py +5247 -96
  86. package/src/shared/scripts/skf-extraction-inventory.py +1280 -0
  87. package/src/shared/scripts/skf-extraction-snapshot.py +664 -0
  88. package/src/shared/scripts/skf-fetch-temporal.py +674 -0
  89. package/src/shared/scripts/skf-find-cycles.py +97 -29
  90. package/src/shared/scripts/skf-find-test-report.py +476 -0
  91. package/src/shared/scripts/skf-forge-tier-rw.py +469 -205
  92. package/src/shared/scripts/skf-github-fetch.py +365 -0
  93. package/src/shared/scripts/skf-github-probe.py +700 -0
  94. package/src/shared/scripts/skf-hash-content.py +180 -20
  95. package/src/shared/scripts/skf-language-corpora.py +1 -1
  96. package/src/shared/scripts/skf-load-provenance.py +161 -27
  97. package/src/shared/scripts/skf-manifest-ops.py +1 -1
  98. package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
  99. package/src/shared/scripts/skf-merge-doc-urls.py +143 -20
  100. package/src/shared/scripts/skf-names-present.py +221 -0
  101. package/src/shared/scripts/skf-pair-intersect.py +119 -28
  102. package/src/shared/scripts/skf-parse-gaps.py +1384 -0
  103. package/src/shared/scripts/skf-preapply.py +1 -1
  104. package/src/shared/scripts/skf-preflight.py +176 -28
  105. package/src/shared/scripts/skf-promote-staged.py +430 -0
  106. package/src/shared/scripts/skf-provenance-gap-dispatch.py +64 -8
  107. package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
  108. package/src/shared/scripts/skf-quick-batch.py +774 -0
  109. package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
  110. package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
  111. package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
  112. package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
  113. package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
  114. package/src/shared/scripts/skf-resolve-authoritative-files.py +155 -14
  115. package/src/shared/scripts/skf-resolve-package.py +523 -106
  116. package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
  117. package/src/shared/scripts/skf-run-lock.py +577 -0
  118. package/src/shared/scripts/skf-scan-manifests.py +998 -57
  119. package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
  120. package/src/shared/scripts/skf-severity-classify.py +712 -108
  121. package/src/shared/scripts/skf-shape-detect.py +298 -204
  122. package/src/shared/scripts/skf-shard-body.py +9 -8
  123. package/src/shared/scripts/skf-skill-inventory.py +2412 -89
  124. package/src/shared/scripts/skf-skills-module.py +472 -0
  125. package/src/shared/scripts/skf-source-tree.py +1796 -0
  126. package/src/shared/scripts/skf-structural-diff.py +787 -121
  127. package/src/shared/scripts/skf-tessl-review.py +706 -0
  128. package/src/shared/scripts/skf-update-active-symlink.py +120 -67
  129. package/src/shared/scripts/skf-update-run-state.py +624 -0
  130. package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
  131. package/src/shared/scripts/skf-validate-brief-schema.py +23 -2
  132. package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
  133. package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
  134. package/src/shared/scripts/skf-validate-output.py +712 -64
  135. package/src/shared/scripts/skf-validate-pins.py +1 -1
  136. package/src/shared/scripts/skf-verify-no-trace.py +116 -10
  137. package/src/shared/scripts/skf-verify-provenance-completeness.py +3945 -152
  138. package/src/shared/scripts/skf-write-skill-brief.py +406 -25
  139. package/src/shared/tool-requirements.yaml +201 -0
  140. package/src/skf-analyze-source/SKILL.md +41 -36
  141. package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
  142. package/src/skf-analyze-source/customize.toml +36 -24
  143. package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
  144. package/src/skf-analyze-source/references/continue.md +2 -13
  145. package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
  146. package/src/skf-analyze-source/references/generate-briefs.md +121 -55
  147. package/src/skf-analyze-source/references/headless-contract.md +42 -15
  148. package/src/skf-analyze-source/references/identify-units.md +96 -110
  149. package/src/skf-analyze-source/references/init.md +73 -71
  150. package/src/skf-analyze-source/references/map-and-detect.md +123 -105
  151. package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
  152. package/src/skf-analyze-source/references/recommend.md +51 -70
  153. package/src/skf-analyze-source/references/scan-project.md +60 -84
  154. package/src/skf-analyze-source/references/scan-root.md +39 -0
  155. package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
  156. package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
  157. package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
  158. package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
  159. package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
  160. package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
  161. package/src/skf-audit-skill/SKILL.md +43 -51
  162. package/src/skf-audit-skill/assets/drift-report-template.md +28 -3
  163. package/src/skf-audit-skill/customize.toml +41 -20
  164. package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
  165. package/src/skf-audit-skill/references/doc-drift.md +141 -0
  166. package/src/skf-audit-skill/references/headless-contract.md +29 -0
  167. package/src/skf-audit-skill/references/health-check.md +2 -0
  168. package/src/skf-audit-skill/references/init.md +149 -210
  169. package/src/skf-audit-skill/references/re-index.md +103 -110
  170. package/src/skf-audit-skill/references/report.md +96 -33
  171. package/src/skf-audit-skill/references/semantic-diff.md +34 -25
  172. package/src/skf-audit-skill/references/severity-classify.md +79 -65
  173. package/src/skf-audit-skill/references/severity-rules.md +15 -6
  174. package/src/skf-audit-skill/references/structural-diff.md +80 -90
  175. package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
  176. package/src/skf-audit-skill/scripts/render-drift-tables.py +712 -0
  177. package/src/skf-brief-skill/SKILL.md +37 -26
  178. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  179. package/src/skf-brief-skill/assets/scope-templates.md +18 -25
  180. package/src/skf-brief-skill/assets/skill-brief-schema.md +30 -23
  181. package/src/skf-brief-skill/customize.toml +48 -20
  182. package/src/skf-brief-skill/references/analyze-target.md +144 -127
  183. package/src/skf-brief-skill/references/confirm-brief.md +11 -9
  184. package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
  185. package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
  186. package/src/skf-brief-skill/references/gather-intent.md +87 -182
  187. package/src/skf-brief-skill/references/headless-args.md +84 -16
  188. package/src/skf-brief-skill/references/health-check.md +1 -1
  189. package/src/skf-brief-skill/references/invocation-contract.md +18 -13
  190. package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
  191. package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
  192. package/src/skf-brief-skill/references/scope-definition.md +123 -47
  193. package/src/skf-brief-skill/references/step-auto-brief.md +26 -100
  194. package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
  195. package/src/skf-brief-skill/references/version-resolution.md +7 -10
  196. package/src/skf-brief-skill/references/write-brief.md +66 -57
  197. package/src/skf-campaign/SKILL.md +31 -34
  198. package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
  199. package/src/skf-campaign/customize.toml +47 -16
  200. package/src/skf-campaign/references/campaign-contracts.md +39 -29
  201. package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
  202. package/src/skf-campaign/references/health-check.md +7 -16
  203. package/src/skf-campaign/references/step-01-setup.md +46 -84
  204. package/src/skf-campaign/references/step-02-strategy.md +31 -30
  205. package/src/skf-campaign/references/step-03-pins.md +26 -18
  206. package/src/skf-campaign/references/step-04-provenance.md +24 -23
  207. package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
  208. package/src/skf-campaign/references/step-06-batch.md +41 -34
  209. package/src/skf-campaign/references/step-07-capstone.md +40 -25
  210. package/src/skf-campaign/references/step-08-verify.md +28 -29
  211. package/src/skf-campaign/references/step-09-refine.md +29 -30
  212. package/src/skf-campaign/references/step-10-export.md +48 -36
  213. package/src/skf-campaign/references/step-11-maintenance.md +31 -34
  214. package/src/skf-campaign/references/step-resume.md +37 -68
  215. package/src/skf-campaign/scripts/campaign-deps.py +57 -18
  216. package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
  217. package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
  218. package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
  219. package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
  220. package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
  221. package/src/skf-campaign/scripts/campaign-report.py +204 -37
  222. package/src/skf-campaign/scripts/campaign-state.py +1128 -0
  223. package/src/skf-campaign/scripts/campaign-status.py +10 -74
  224. package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
  225. package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
  226. package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
  227. package/src/skf-campaign/templates/campaign-report-template.md +5 -1
  228. package/src/skf-campaign/templates/kickoff-template.md +0 -14
  229. package/src/skf-create-skill/SKILL.md +27 -20
  230. package/src/skf-create-skill/assets/compile-assembly-rules.md +11 -8
  231. package/src/skf-create-skill/assets/skill-sections.md +21 -11
  232. package/src/skf-create-skill/customize.toml +20 -8
  233. package/src/skf-create-skill/references/authoritative-files-protocol.md +36 -58
  234. package/src/skf-create-skill/references/batch-mode.md +81 -0
  235. package/src/skf-create-skill/references/ccc-index-check.md +27 -0
  236. package/src/skf-create-skill/references/compile.md +119 -70
  237. package/src/skf-create-skill/references/component-extraction.md +113 -91
  238. package/src/skf-create-skill/references/enrich.md +41 -23
  239. package/src/skf-create-skill/references/entry-points-by-hand.md +37 -0
  240. package/src/skf-create-skill/references/extract.md +204 -162
  241. package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
  242. package/src/skf-create-skill/references/extraction-patterns-tracing.md +11 -11
  243. package/src/skf-create-skill/references/extraction-patterns.md +588 -201
  244. package/src/skf-create-skill/references/generate-artifacts.md +145 -93
  245. package/src/skf-create-skill/references/health-check.md +1 -1
  246. package/src/skf-create-skill/references/load-brief.md +53 -36
  247. package/src/skf-create-skill/references/relabel-rule.md +12 -0
  248. package/src/skf-create-skill/references/report.md +37 -57
  249. package/src/skf-create-skill/references/source-resolution-protocols.md +58 -169
  250. package/src/skf-create-skill/references/step-auto-shard.md +20 -93
  251. package/src/skf-create-skill/references/step-doc-rot.md +44 -60
  252. package/src/skf-create-skill/references/step-doc-sources.md +28 -19
  253. package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
  254. package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
  255. package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
  256. package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
  257. package/src/skf-create-skill/references/validate.md +173 -99
  258. package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
  259. package/src/skf-create-stack-skill/SKILL.md +29 -56
  260. package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
  261. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
  262. package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
  263. package/src/skf-create-stack-skill/customize.toml +23 -12
  264. package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
  265. package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
  266. package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
  267. package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
  268. package/src/skf-create-stack-skill/references/generate-output.md +138 -128
  269. package/src/skf-create-stack-skill/references/init.md +90 -32
  270. package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
  271. package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
  272. package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
  273. package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
  274. package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
  275. package/src/skf-create-stack-skill/references/report.md +39 -33
  276. package/src/skf-create-stack-skill/references/validate.md +20 -39
  277. package/src/skf-drop-skill/SKILL.md +28 -51
  278. package/src/skf-drop-skill/customize.toml +44 -30
  279. package/src/skf-drop-skill/references/execute.md +83 -146
  280. package/src/skf-drop-skill/references/health-check.md +2 -0
  281. package/src/skf-drop-skill/references/invocation-contract.md +64 -0
  282. package/src/skf-drop-skill/references/report.md +46 -30
  283. package/src/skf-drop-skill/references/select.md +122 -124
  284. package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
  285. package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
  286. package/src/skf-export-skill/SKILL.md +40 -62
  287. package/src/skf-export-skill/assets/managed-section-format.md +14 -50
  288. package/src/skf-export-skill/assets/snippet-format.md +3 -1
  289. package/src/skf-export-skill/customize.toml +42 -13
  290. package/src/skf-export-skill/references/generate-snippet.md +54 -46
  291. package/src/skf-export-skill/references/health-check.md +2 -0
  292. package/src/skf-export-skill/references/invocation-contract.md +15 -0
  293. package/src/skf-export-skill/references/load-skill.md +88 -62
  294. package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
  295. package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
  296. package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
  297. package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
  298. package/src/skf-export-skill/references/package.md +15 -73
  299. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
  300. package/src/skf-export-skill/references/result-envelope.md +31 -16
  301. package/src/skf-export-skill/references/summary.md +41 -17
  302. package/src/skf-export-skill/references/token-report.md +4 -12
  303. package/src/skf-export-skill/references/update-context.md +118 -237
  304. package/src/skf-forger/SKILL.md +61 -37
  305. package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
  306. package/src/skf-forger/customize.toml +43 -0
  307. package/src/skf-forger/references/pipeline-mode.md +73 -18
  308. package/src/skf-forger/scripts/forge-status.py +299 -0
  309. package/src/skf-forger/scripts/parse-pipeline.py +322 -54
  310. package/src/skf-forger/scripts/pipeline-gate.py +428 -0
  311. package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
  312. package/src/skf-quick-skill/SKILL.md +54 -53
  313. package/src/skf-quick-skill/assets/skill-template.md +4 -2
  314. package/src/skf-quick-skill/customize.toml +51 -20
  315. package/src/skf-quick-skill/references/batch-mode.md +74 -21
  316. package/src/skf-quick-skill/references/compile.md +30 -31
  317. package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
  318. package/src/skf-quick-skill/references/finalize.md +69 -18
  319. package/src/skf-quick-skill/references/halt-contract.md +41 -29
  320. package/src/skf-quick-skill/references/health-check.md +5 -3
  321. package/src/skf-quick-skill/references/quick-extract.md +114 -45
  322. package/src/skf-quick-skill/references/registry-resolution.md +9 -64
  323. package/src/skf-quick-skill/references/resolve-target.md +116 -66
  324. package/src/skf-quick-skill/references/write-and-validate.md +79 -40
  325. package/src/skf-refine-architecture/SKILL.md +43 -53
  326. package/src/skf-refine-architecture/customize.toml +49 -20
  327. package/src/skf-refine-architecture/references/compile.md +97 -54
  328. package/src/skf-refine-architecture/references/exit-codes.md +30 -0
  329. package/src/skf-refine-architecture/references/finding-storage.md +21 -0
  330. package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
  331. package/src/skf-refine-architecture/references/improvements.md +14 -20
  332. package/src/skf-refine-architecture/references/init.md +98 -57
  333. package/src/skf-refine-architecture/references/issue-detection.md +32 -39
  334. package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
  335. package/src/skf-refine-architecture/references/report.md +65 -54
  336. package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
  337. package/src/skf-rename-skill/SKILL.md +19 -54
  338. package/src/skf-rename-skill/customize.toml +32 -26
  339. package/src/skf-rename-skill/references/execute.md +230 -184
  340. package/src/skf-rename-skill/references/exit-codes.md +7 -7
  341. package/src/skf-rename-skill/references/health-check.md +1 -7
  342. package/src/skf-rename-skill/references/invocation-contract.md +25 -0
  343. package/src/skf-rename-skill/references/report.md +63 -52
  344. package/src/skf-rename-skill/references/select.md +197 -115
  345. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
  346. package/src/skf-setup/SKILL.md +26 -26
  347. package/src/skf-setup/customize.toml +14 -10
  348. package/src/skf-setup/references/auto-index.md +50 -63
  349. package/src/skf-setup/references/ccc-index.md +29 -90
  350. package/src/skf-setup/references/detect-and-tier.md +58 -67
  351. package/src/skf-setup/references/health-check.md +2 -0
  352. package/src/skf-setup/references/invocation-contract.md +14 -0
  353. package/src/skf-setup/references/report.md +43 -166
  354. package/src/skf-setup/references/tier-rules.md +5 -5
  355. package/src/skf-setup/references/write-config.md +34 -57
  356. package/src/skf-test-skill/SKILL.md +17 -57
  357. package/src/skf-test-skill/assets/output-section-formats.md +33 -34
  358. package/src/skf-test-skill/customize.toml +27 -13
  359. package/src/skf-test-skill/references/coherence-check.md +134 -62
  360. package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
  361. package/src/skf-test-skill/references/coverage-check.md +209 -264
  362. package/src/skf-test-skill/references/detect-mode.md +5 -5
  363. package/src/skf-test-skill/references/external-validators.md +94 -82
  364. package/src/skf-test-skill/references/health-check.md +6 -7
  365. package/src/skf-test-skill/references/init.md +164 -91
  366. package/src/skf-test-skill/references/invocation-contract.md +66 -0
  367. package/src/skf-test-skill/references/migration-section-rules.md +5 -2
  368. package/src/skf-test-skill/references/report.md +164 -142
  369. package/src/skf-test-skill/references/score.md +80 -107
  370. package/src/skf-test-skill/references/scoring-rules.md +42 -155
  371. package/src/skf-test-skill/references/source-access-protocol.md +18 -94
  372. package/src/skf-test-skill/references/step-hard-gate.md +51 -33
  373. package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
  374. package/src/skf-test-skill/scripts/build-result-context.py +380 -0
  375. package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
  376. package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
  377. package/src/skf-test-skill/scripts/compute-score.py +223 -45
  378. package/src/skf-test-skill/scripts/gap-ledger.py +1666 -0
  379. package/src/skf-test-skill/scripts/hard-gate.py +203 -0
  380. package/src/skf-test-skill/scripts/load-coverage-inputs.py +1198 -0
  381. package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
  382. package/src/skf-test-skill/scripts/reconcile-coverage.py +547 -79
  383. package/src/skf-test-skill/scripts/score-signatures.py +479 -0
  384. package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
  385. package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
  386. package/src/skf-test-skill/scripts/verify-declared-numerator.py +133 -56
  387. package/src/skf-test-skill/templates/test-report-template.md +21 -15
  388. package/src/skf-update-skill/SKILL.md +38 -25
  389. package/src/skf-update-skill/customize.toml +27 -12
  390. package/src/skf-update-skill/references/detect-changes.md +253 -223
  391. package/src/skf-update-skill/references/gap-driven.md +336 -0
  392. package/src/skf-update-skill/references/health-check.md +31 -5
  393. package/src/skf-update-skill/references/init.md +254 -84
  394. package/src/skf-update-skill/references/invocation-contract.md +12 -0
  395. package/src/skf-update-skill/references/manual-section-rules.md +6 -23
  396. package/src/skf-update-skill/references/merge.md +182 -49
  397. package/src/skf-update-skill/references/re-extract.md +53 -262
  398. package/src/skf-update-skill/references/report.md +102 -35
  399. package/src/skf-update-skill/references/write.md +357 -155
  400. package/src/skf-update-skill/scripts/skf-new-file-diff.py +183 -46
  401. package/src/skf-verify-stack/SKILL.md +23 -42
  402. package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
  403. package/src/skf-verify-stack/customize.toml +55 -22
  404. package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
  405. package/src/skf-verify-stack/references/coverage.md +67 -64
  406. package/src/skf-verify-stack/references/exit-codes.md +21 -8
  407. package/src/skf-verify-stack/references/init.md +107 -62
  408. package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
  409. package/src/skf-verify-stack/references/integrations.md +106 -65
  410. package/src/skf-verify-stack/references/report.md +59 -42
  411. package/src/skf-verify-stack/references/requirements.md +32 -9
  412. package/src/skf-verify-stack/references/synthesize.md +53 -39
  413. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
  414. package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
  415. package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
  416. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
  417. package/tools/cli/commands/install.js +11 -4
  418. package/tools/cli/commands/status.js +15 -3
  419. package/tools/cli/commands/update.js +11 -4
  420. package/tools/cli/lib/tool-check.js +320 -0
  421. package/tools/cli/lib/version-check.js +22 -9
  422. package/tools/cli/skf-cli.js +4 -1
  423. package/.gitattributes +0 -16
  424. package/.nvmrc +0 -1
  425. package/docs/_internal/RELEASING.md +0 -679
  426. package/release-audits/v1.0.0-launch-audit.md +0 -728
  427. package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
  428. package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
  429. package/src/skf-campaign/manifest.yaml +0 -15
  430. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
  431. package/src/skf-create-skill/references/ecosystem-check.md +0 -74
  432. package/src/skf-drop-skill/references/headless-contract.md +0 -18
  433. package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
  434. package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
  435. package/src/skf-update-skill/references/validate.md +0 -124
  436. package/tools/validate-docs-drift.js +0 -233
  437. package/tools/validate-docs-links.js +0 -665
@@ -0,0 +1,365 @@
1
+ # /// script
2
+ # requires-python = ">=3.11"
3
+ # dependencies = ["pyyaml"]
4
+ # ///
5
+ """SKF GitHub Fetch: stage files of a GitHub repository at one ref in a folder.
6
+
7
+ quick-skill reads a repository it never clones: its README, its manifest,
8
+ its entry-point files, and a skills module's SKILL.md files and
9
+ module-help.csv. This helper writes each file byte for byte under --dest,
10
+ laid out as in the repository, so the helper that reads it next
11
+ (skf-extract-public-api.py --manifest-file and --entry-file,
12
+ skf-skills-module.py extract) gets the file itself: no file text passes
13
+ through the model, a shell string or a rendered web page, and a quote or an
14
+ apostrophe in it arrives as written.
15
+
16
+ CLI:
17
+ uv run skf-github-fetch.py --repo <repo> --ref <ref> --tree-file <file> \\
18
+ --dest <folder> [--limit <n>] [--exclude <glob>]... \\
19
+ [--patterns-file <file>] [--timeout <seconds>] [<pattern>...]
20
+
21
+ --repo takes a github.com repository as skf-github-probe.py does
22
+ (`owner/repo` or a URL). --ref takes a branch, tag or commit; an empty,
23
+ `null`, `none` or `head` value (any letter case) is HEAD, the default
24
+ branch.
25
+
26
+ Which files. --tree-file is the repository's file listing at --ref:
27
+ skf-github-probe.py tree output, or any listing skf-detect-language.py's
28
+ --tree-file reads (a listing that reports a failure is refused). Each
29
+ pattern is a repo-relative path or a glob, read by
30
+ skf-resolve-authoritative-files.py's rules: `**` spans any number of path
31
+ segments, none included, and `*` and `?` stay inside one. A pattern takes
32
+ the listed files it matches, in path order, at most --limit of them (0,
33
+ the default, for all), leaving out the files an --exclude glob matches. A
34
+ pattern that matches no listed file is reported in `unmatched` and costs
35
+ no request: an optional file the repository does not have (one more
36
+ entry-point candidate, a module-help.csv) is a normal answer, not an
37
+ error. A listing GitHub cut short (`truncated`) may lack a file the
38
+ repository has, so on one a pattern with no `*` or `?` that matches no
39
+ listed file is read anyway, one request: it is `fetched` when it is read,
40
+ and stays in `unmatched` when it cannot be. --patterns-file adds one
41
+ pattern per line (blank lines skipped), such as the list
42
+ skf-skills-module.py sniff writes with --fetch-list. A file two patterns
43
+ match is fetched once.
44
+
45
+ How. Each file is read from
46
+ https://raw.githubusercontent.com/<owner>/<repo>/<ref>/<path> without
47
+ credentials, which needs neither gh nor the API's rate limit. When that
48
+ fails (a private repository answers 404 there too), `gh api -H "Accept:
49
+ application/vnd.github.raw" repos/<owner>/<repo>/contents/<path>?ref=<ref>`
50
+ reads it with the user's GitHub login; once gh has read a file the raw URL
51
+ could not, the files left go to gh first. gh runs as skf-github-probe.py
52
+ runs it (through skf-source-tree.py's runner, never a gh in the current
53
+ folder). A path the listing holds that leads outside --dest (an absolute
54
+ path, a `..` segment) is never written.
55
+
56
+ The whole call finishes within about --timeout seconds (120 by default);
57
+ one request gets at most 20 of them.
58
+
59
+ Output (one ASCII JSON line on stdout):
60
+ status "ok": every file a pattern matched was written (no pattern
61
+ matching anything is ok too); "partial": some could not be;
62
+ "unavailable": none could be
63
+ message one line naming what failed, else null
64
+ repo "owner/repo"
65
+ ref the ref read ("HEAD" for the default branch)
66
+ dest the folder written to, absolute, with `/`
67
+ via "raw" | "gh" | "raw+gh" | null: what read the files written
68
+ fetched the repo-relative paths written under --dest, in pattern order
69
+ unmatched the patterns that matched no listed file (and, on a
70
+ truncated listing, a path that could not be read either)
71
+ failed [{"path", "detail"}]: files that could not be read or written
72
+ truncated the listing was cut short, so a glob may miss a file
73
+
74
+ Exit codes:
75
+ 0 status ok
76
+ 3 status partial or unavailable (JSON on stdout; the caller decides)
77
+ 1 unexpected error: {"status": "error", "message": ...} on stderr
78
+ 2 usage error: argparse, an invalid --repo or --ref, or a --tree-file
79
+ or --patterns-file that cannot be read ({"status": "error", ...} on
80
+ stderr)
81
+ """
82
+
83
+ from __future__ import annotations
84
+
85
+ import argparse
86
+ import importlib.util
87
+ import json
88
+ import socket
89
+ import sys
90
+ import time
91
+ import urllib.error
92
+ import urllib.parse
93
+ import urllib.request
94
+ from pathlib import Path
95
+
96
+ RAW_ROOT = "https://raw.githubusercontent.com"
97
+ DEFAULT_TIMEOUT_SEC = 120.0
98
+ REQUEST_TIMEOUT_SEC = 20.0
99
+
100
+ _SIBLINGS: dict[str, object] = {}
101
+
102
+
103
+ def _load(filename: str):
104
+ """A script of this folder, loaded once."""
105
+ if filename not in _SIBLINGS:
106
+ path = Path(__file__).resolve().parent / filename
107
+ spec = importlib.util.spec_from_file_location(filename[:-3].replace("-", "_"), path)
108
+ if spec is None or spec.loader is None:
109
+ raise ImportError(f"cannot load {path}")
110
+ module = importlib.util.module_from_spec(spec)
111
+ spec.loader.exec_module(module)
112
+ _SIBLINGS[filename] = module
113
+ return _SIBLINGS[filename]
114
+
115
+
116
+ def _probe():
117
+ """skf-github-probe.py: its repository grammar, ref rule, user agent and gh error reading."""
118
+ return _load("skf-github-probe.py")
119
+
120
+
121
+ class UsageError(ValueError):
122
+ """An input the call cannot run with: exit 2."""
123
+
124
+
125
+ # --------------------------------------------------------------------------
126
+ # Which files
127
+ # --------------------------------------------------------------------------
128
+
129
+
130
+ def select(listing: list[str], patterns: list[str], limit: int = 0,
131
+ excludes: list[str] | None = None, truncated: bool = False) -> tuple[list[str], list[str]]:
132
+ """(the listed paths the patterns take, in pattern order and each once;
133
+ the patterns that matched no listed file). On a truncated listing a
134
+ pattern with no `*` or `?` that matched nothing is in both: the
135
+ repository may hold it all the same."""
136
+ glob_match = _load("skf-resolve-authoritative-files.py").glob_match
137
+ excludes = excludes or []
138
+ files = sorted(p for p in dict.fromkeys(listing) if not any(glob_match(p, x) for x in excludes))
139
+ chosen: list[str] = []
140
+ unmatched: list[str] = []
141
+ for pattern in patterns:
142
+ pattern = pattern.strip().removeprefix("./")
143
+ matched = [p for p in files if glob_match(p, pattern)]
144
+ if limit > 0:
145
+ matched = matched[:limit]
146
+ if not matched:
147
+ unmatched.append(pattern)
148
+ if truncated and pattern and not any(c in pattern for c in "*?") \
149
+ and not any(glob_match(pattern, x) for x in excludes):
150
+ matched = [pattern]
151
+ chosen.extend(p for p in matched if p not in chosen)
152
+ return chosen, unmatched
153
+
154
+
155
+ def _target(dest: Path, path: str) -> Path | None:
156
+ """Where `path` is written under `dest`, or None when it would land outside it."""
157
+ parts = path.split("/")
158
+ if path.startswith("/") or "\\" in path or any(part in ("", ".", "..") for part in parts):
159
+ return None
160
+ target = dest.joinpath(*parts)
161
+ try:
162
+ target.resolve().relative_to(dest.resolve())
163
+ except ValueError:
164
+ return None
165
+ return target
166
+
167
+
168
+ # --------------------------------------------------------------------------
169
+ # Reading a file
170
+ # --------------------------------------------------------------------------
171
+
172
+
173
+ _deadline: float | None = None
174
+
175
+
176
+ def _start_clock(seconds: float | None) -> None:
177
+ global _deadline
178
+ _deadline = None if seconds is None else time.monotonic() + max(0.0, seconds)
179
+
180
+
181
+ def _budget() -> float | None:
182
+ """Seconds the next request may take, or None when no time is left."""
183
+ if _deadline is None:
184
+ return REQUEST_TIMEOUT_SEC
185
+ left = _deadline - time.monotonic()
186
+ return min(REQUEST_TIMEOUT_SEC, left) if left > 0.5 else None
187
+
188
+
189
+ def _raw(owner: str, repo: str, ref: str, path: str) -> tuple[bytes | None, str]:
190
+ """The file from raw.githubusercontent.com, without credentials: (bytes or None, detail)."""
191
+ timeout = _budget()
192
+ if timeout is None:
193
+ return None, "no time was left to read it"
194
+ url = f"{RAW_ROOT}/{owner}/{repo}/{urllib.parse.quote(ref, safe='/')}/{urllib.parse.quote(path, safe='/')}"
195
+ req = urllib.request.Request(url, headers={"User-Agent": _probe().USER_AGENT})
196
+ try:
197
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
198
+ return resp.read(), ""
199
+ except urllib.error.HTTPError as e:
200
+ return None, f"HTTP {e.code} from {RAW_ROOT}"
201
+ except (TimeoutError, socket.timeout):
202
+ return None, f"{RAW_ROOT} did not answer within {timeout:g} seconds"
203
+ except (urllib.error.URLError, OSError, ValueError) as e:
204
+ return None, f"{RAW_ROOT}: {getattr(e, 'reason', e)}"
205
+
206
+
207
+ def _gh_raw(owner: str, repo: str, ref: str, path: str) -> tuple[bytes | None, str]:
208
+ """The file through `gh api` with the raw media type: (bytes or None, detail)."""
209
+ tree = _probe()._sibling()
210
+ exe = tree._resolve_outside_cwd("gh")
211
+ if exe is None:
212
+ return None, "gh is not installed"
213
+ timeout = _budget()
214
+ if timeout is None:
215
+ return None, "no time was left to run gh"
216
+ api_path = (f"repos/{owner}/{repo}/contents/{urllib.parse.quote(path, safe='/')}"
217
+ f"?ref={urllib.parse.quote(ref, safe='')}")
218
+ try:
219
+ rc, out, err, _killed = tree._run([exe, "api", "--hostname", "github.com", "-H",
220
+ "Accept: application/vnd.github.raw", api_path], timeout)
221
+ except (OSError, ValueError) as e:
222
+ return None, f"gh could not run: {e}"
223
+ if rc is None:
224
+ return None, f"gh did not answer within {timeout:g} seconds"
225
+ if rc == 0:
226
+ return out, ""
227
+ if rc == 4:
228
+ return None, "gh is not logged in"
229
+ return None, _probe()._last_line(err.decode("utf-8", errors="replace")) or f"gh exited {rc}"
230
+
231
+
232
+ def fetch(owner: str, repo: str, ref: str, paths: list[str], dest: Path) -> dict:
233
+ """Write each path under dest; the output's fetched, failed and via keys."""
234
+ fetched: list[str] = []
235
+ failed: list[dict] = []
236
+ used: list[str] = []
237
+ gh_first = False
238
+ for path in paths:
239
+ target = _target(dest, path)
240
+ if target is None:
241
+ failed.append({"path": path, "detail": "the path leads outside the destination folder"})
242
+ continue
243
+ readers = (("gh", _gh_raw), ("raw", _raw)) if gh_first else (("raw", _raw), ("gh", _gh_raw))
244
+ body, details, reader = None, [], None
245
+ for name, read in readers:
246
+ body, detail = read(owner, repo, ref, path)
247
+ if body is not None:
248
+ reader = name
249
+ break
250
+ details.append(f"{name}: {detail}")
251
+ if body is None:
252
+ failed.append({"path": path, "detail": "; ".join(details)})
253
+ continue
254
+ if reader == "gh" and not gh_first:
255
+ gh_first = True
256
+ try:
257
+ target.parent.mkdir(parents=True, exist_ok=True)
258
+ target.write_bytes(body)
259
+ except OSError as e:
260
+ failed.append({"path": path, "detail": f"cannot write {target.as_posix()}: {e.strerror or e}"})
261
+ continue
262
+ fetched.append(path)
263
+ if reader not in used:
264
+ used.append(reader)
265
+ via = "+".join(name for name in ("raw", "gh") if name in used) or None
266
+ return {"fetched": fetched, "failed": failed, "via": via}
267
+
268
+
269
+ def run(repo_arg: str, ref_arg: str | None, tree_file: str, dest_arg: str, patterns: list[str],
270
+ limit: int = 0, excludes: list[str] | None = None) -> dict:
271
+ probe = _probe()
272
+ parsed = probe.parse_repo(repo_arg)
273
+ if parsed is None:
274
+ raise UsageError(f"{repo_arg} is not a github.com repository (expected owner/repo or a URL).")
275
+ owner, repo = parsed
276
+ ref = probe._tree_ref(ref_arg)
277
+ if ref != "HEAD" and not probe._sibling().REF_RE.fullmatch(ref):
278
+ raise UsageError(f"{ref} is not a valid branch, tag or commit name.")
279
+ try:
280
+ listing, truncated = _load("skf-detect-language.py").read_tree_file(tree_file)
281
+ except ValueError as e:
282
+ raise UsageError(str(e)) from e
283
+ dest = Path(dest_arg).absolute()
284
+ chosen, unmatched = select(listing, patterns, limit, excludes, truncated)
285
+ result = fetch(owner, repo, ref, chosen, dest)
286
+ # A path the truncated listing lacks was a guess: read, it matched; unread, it stays unmatched.
287
+ guessed = {p for p in chosen if p in unmatched}
288
+ unmatched = [p for p in unmatched if p not in result["fetched"]]
289
+ failed = [f for f in result["failed"] if f["path"] not in guessed]
290
+ if not failed:
291
+ status, message = "ok", None
292
+ else:
293
+ status = "partial" if result["fetched"] else "unavailable"
294
+ names = ", ".join(f["path"] for f in failed[:3]) + (f" and {len(failed) - 3} more" if len(failed) > 3 else "")
295
+ message = (f"Could not fetch {names} from {owner}/{repo} at {ref}: {failed[0]['detail']}. "
296
+ "Check the network, or log in with gh auth login for a private repository.")
297
+ return {"status": status, "message": message, "repo": f"{owner}/{repo}", "ref": ref,
298
+ "dest": dest.as_posix(), "via": result["via"], "fetched": result["fetched"],
299
+ "unmatched": unmatched, "failed": failed, "truncated": truncated}
300
+
301
+
302
+ # --------------------------------------------------------------------------
303
+ # CLI
304
+ # --------------------------------------------------------------------------
305
+
306
+
307
+ def _patterns_file(value: str) -> list[str]:
308
+ try:
309
+ with open(value, encoding="utf-8-sig") as fh:
310
+ return [line.strip() for line in fh if line.strip()]
311
+ except (OSError, UnicodeDecodeError) as e:
312
+ raise UsageError(f"cannot read --patterns-file {value}: {getattr(e, 'strerror', None) or e}") from e
313
+
314
+
315
+ def _build_parser() -> argparse.ArgumentParser:
316
+ parser = argparse.ArgumentParser(
317
+ prog="skf-github-fetch",
318
+ description="Write files of a GitHub repository at one ref under a folder, picked from its file listing.",
319
+ )
320
+ parser.add_argument("patterns", nargs="*", metavar="pattern", help="a repo-relative path or glob")
321
+ parser.add_argument("--repo", required=True, help="owner/repo or a github.com URL")
322
+ parser.add_argument("--ref", default="HEAD", help="branch, tag or commit (default: the default branch)")
323
+ parser.add_argument("--tree-file", required=True,
324
+ help="the repository's file listing at --ref (skf-github-probe.py tree output)")
325
+ parser.add_argument("--dest", required=True, help="the folder the files are written under")
326
+ parser.add_argument("--limit", type=int, default=0, help="files one pattern takes at most (0: all)")
327
+ parser.add_argument("--exclude", action="append", default=[], help="a glob of listed files to leave out")
328
+ parser.add_argument("--patterns-file", help="more patterns, one per line")
329
+ parser.add_argument("--timeout", type=float, default=DEFAULT_TIMEOUT_SEC,
330
+ help="seconds the whole call may take (default 120)")
331
+ return parser
332
+
333
+
334
+ def main(argv: list[str] | None = None) -> int:
335
+ args = _build_parser().parse_args(argv)
336
+ try:
337
+ _start_clock(args.timeout)
338
+ patterns = list(args.patterns)
339
+ if args.patterns_file:
340
+ patterns.extend(_patterns_file(args.patterns_file))
341
+ out = run(args.repo, args.ref, args.tree_file, args.dest, patterns, args.limit, args.exclude)
342
+ except UsageError as e:
343
+ print(json.dumps({"status": "error", "message": str(e)}), file=sys.stderr)
344
+ return 2
345
+ except Exception as e: # noqa: BLE001 - one JSON error line, never a traceback
346
+ print(json.dumps({"status": "error", "message": f"{type(e).__name__}: {e}"}), file=sys.stderr)
347
+ return 1
348
+ print(json.dumps(out))
349
+ return 0 if out["status"] == "ok" else 3
350
+
351
+
352
+ def _force_utf8(*streams) -> None:
353
+ """Reconfigure the JSON streams to UTF-8 (a Windows console uses cp1252)."""
354
+ for stream in streams:
355
+ if hasattr(stream, "reconfigure"):
356
+ errors = getattr(stream, "errors", None)
357
+ if errors is None:
358
+ stream.reconfigure(encoding="utf-8")
359
+ else:
360
+ stream.reconfigure(encoding="utf-8", errors=errors)
361
+
362
+
363
+ if __name__ == "__main__":
364
+ _force_utf8(sys.stdout, sys.stderr)
365
+ sys.exit(main())