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,774 @@
1
+ # /// script
2
+ # requires-python = ">=3.11"
3
+ # dependencies = ["pyyaml", "jsonschema>=4.0"]
4
+ # ///
5
+ """SKF Quick Batch: the state of a --batch run, kept on disk.
6
+
7
+ A --batch run drives every target through its workflow's pipeline, one
8
+ after another, in one long session: each target of a skf-quick-skill batch
9
+ file, or each skill-brief.yaml of a skf-create-skill batch. This helper
10
+ keeps the batch's place and every target's outcome in the batch run
11
+ folder, not in the conversation, so a context compaction mid-batch loses
12
+ neither, and it computes the counts, the status and the exit code the
13
+ batch ends with.
14
+
15
+ CLI (--run-dir is the batch run folder: for skf-quick-skill the one SKILL.md
16
+ On Activation creates, {project-root}/_bmad-output/.skf-run/skf-quick-skill-<id>/;
17
+ for skf-create-skill the folder start --briefs names after its briefs,
18
+ <run-root>/skf-create-skill-batch-<hash>/, so a batch of the same briefs an
19
+ ended session left is found again, and a batch of other briefs is never
20
+ touched):
21
+
22
+ uv run skf-quick-batch.py parse <file>
23
+ uv run skf-quick-batch.py start <file> --run-dir <dir> [--fail-fast]
24
+ uv run skf-quick-batch.py start --briefs <path>... --run-root <dir> [--fail-fast]
25
+ uv run skf-quick-batch.py next --run-dir <dir>
26
+ uv run skf-quick-batch.py record --run-dir <dir> --batch <n> [--target stderr]
27
+ uv run skf-quick-batch.py summarize --run-dir <dir> --output-dir <dir> [--target stderr]
28
+
29
+ The batch file (UTF-8): one target per line. A blank line, and a line whose
30
+ first character after any spaces is `#`, is skipped. The first word of a
31
+ line is the target, kept whole: `cognee@0.5.0`, `requests==2.31.0` or
32
+ `https://github.com/foo/bar@2.1.0-beta` reach quick-skill's own target
33
+ parser (skf-resolve-package.py parse-target) as written, and it splits off
34
+ the version. Each later word is a modifier: `language=<lang>` sets the
35
+ target's language_hint and `scope=<path>` its scope_hint (the key in any
36
+ letter case, the value not empty, each at most once). A line with any other
37
+ word after its target is taken whole as the target, so step 1 cannot parse
38
+ it and that target fails on its own while the rest of the batch runs.
39
+
40
+ parse
41
+ Prints {"targets": [{"batch", "target", "language_hint", "scope_hint"}]}:
42
+ the file's targets numbered from 1, a hint null when the line sets none.
43
+
44
+ start
45
+ Parses the file and writes the batch file, <run-dir>/batch.jsonl: a start
46
+ line (status running, the workflow, the input file's absolute path,
47
+ --fail-fast) and one line per target, status pending. Prints {"status":
48
+ "running", "input_file", "fail_fast", "targets_total", "recorded",
49
+ "resumed"}. When the run folder already holds a batch of the same input
50
+ file, start changes nothing and reports it (resumed true, and the targets
51
+ already recorded), so a start run twice, from any working folder, never
52
+ loses an outcome. A batch of another file is refused. A file with no
53
+ target starts a batch that summarize ends at once, status success.
54
+
55
+ start --briefs (skf-create-skill)
56
+ The targets are skill-brief.yaml files: each path given is a brief, or a
57
+ folder whose skill-brief.yaml files (under it at any depth, hidden folders
58
+ left out, in path order) are briefs, each once, by its absolute path.
59
+ The batch run folder is <run-root>/skf-create-skill-batch-<hash>, <hash>
60
+ the first 12 hex digits of the SHA-256 of the brief paths, one per line:
61
+ each set of briefs has its own. Every brief is checked with
62
+ skf-validate-brief-schema.py, beside this script, before the first one
63
+ compiles: a brief it refuses (brief-missing, brief-malformed or
64
+ brief-invalid), and a brief whose `name` an earlier brief of the batch
65
+ already has (brief-invalid: both would compile into one skill folder), is
66
+ recorded at once as failed, with the exit code the skf-create-skill
67
+ envelope schema maps its halt_reason to, and next never hands it out.
68
+ Prints {"status": "running", "workflow", "batch_dir", "input_briefs",
69
+ "fail_fast", "targets_total", "recorded", "resumed", "rejected":
70
+ [{"batch", "target", "error_code", "message"}]}. When the folder holds an
71
+ unfinished batch of the same briefs, start changes nothing and reports it
72
+ (resumed true), so a batch a session ended mid-way goes on where it
73
+ stopped; a batch that ended (summarized) gives way to the new one, its
74
+ folder emptied first. No brief under the paths is refused. The pyyaml and
75
+ jsonschema the header names are the brief validator's.
76
+
77
+ next
78
+ Prints the next pending target, {"status": "next", "batch", "target",
79
+ "language_hint", "scope_hint", "run_dir"}, where run_dir is the target's
80
+ own run folder, created empty (a folder an earlier try of the target left
81
+ is emptied first): <run-dir>/<run-dir name>-<batch>, and for a brief
82
+ <run-dir>/skf-create-skill-batch-<YYYYMMDD-HHmmss>-<batch>, named after
83
+ the batch's start, so the run_id the emitter stamps from it is the
84
+ brief's own in every batch. It prints
85
+ the target's start event on stderr, one line:
86
+ {"batch":<n>,"target":"<target>","status":"start"}
87
+ Prints {"status": "record", "batch"} when that folder already staged
88
+ halt.json or result-context.json: the target ended but was never
89
+ recorded (a compaction between its envelope and record), so record it
90
+ instead of running it again. Prints {"status": "done",
91
+ "fail_fast_triggered"} when no target is pending, or when --fail-fast was
92
+ given and a target failed.
93
+
94
+ record
95
+ Records how target <n> ended, read from what its run folder staged for
96
+ the shared emitter: halt.json (a HARD HALT: status error, error_code its
97
+ halt_reason, exit_code the code the workflow's envelope schema maps
98
+ that reason to) or result-context.json (status success, exit_code 0,
99
+ quality_score from its summary). The outcome line {"batch", "target",
100
+ "status", "exit_code", "skill_package", "error_code", "quality_score"} is
101
+ appended to batch.jsonl, and a target that succeeded has its run folder
102
+ removed. Prints the target's end event, one line (on stderr under
103
+ --target stderr, on stdout otherwise):
104
+ {"batch":<n>,"target":"<target>","status":"done","exit":0}
105
+ {"batch":<n>,"target":"<target>","status":"fail","exit":<code>,"error_code":"<reason>"}
106
+ A target already recorded prints its event again and changes nothing.
107
+
108
+ summarize
109
+ Ends the batch: every target recorded, or --fail-fast stopped it after a
110
+ failure. It counts targets_total (the targets that ran), succeeded and
111
+ failed; sets status (success when none failed, a batch with no target
112
+ included; partial when some failed and some succeeded; failed when some
113
+ failed and none succeeded), fail_fast_triggered (--fail-fast stopped the
114
+ batch with targets left) and the exit code (0, else the highest exit
115
+ code of a failed target); writes the summary, with the batch's
116
+ input_file (skf-quick-skill) or input_briefs (skf-create-skill), to
117
+ <output-dir>/<stem>-<YYYYMMDD-HHmmss>.json, <stem> quick-skill-batch or
118
+ create-skill-batch (`-2`, `-3`, ... appended when another batch took
119
+ that second), and its copy <stem>-latest.json; appends the final status
120
+ line to batch.jsonl; and removes the batch run folder when no target failed and
121
+ the summary was written (a halted target's run folder stays for a look).
122
+ Prints the batch_summary event, one line (on stderr under --target
123
+ stderr, on stdout otherwise): {"batch_summary": true, "targets_total",
124
+ "succeeded", "failed", "status", "fail_fast_triggered", "exit_code",
125
+ "summary_path"}, with "warnings" when a summary file could not be
126
+ written. A batch already summarized prints its event again.
127
+
128
+ The batch file is one JSON object per line, so a write cut short spoils at
129
+ most its own line, which is skipped when read: the next write ends that
130
+ line first. A later outcome line for a target replaces an earlier one.
131
+
132
+ Every result and event is one line of ASCII JSON, with no space after a
133
+ `,` or `:`, so an event line is printed exactly as its format reads.
134
+
135
+ Exit codes (1 prints one JSON line on stderr, {"status": "error",
136
+ "message": ...}, and nothing on stdout; a refused start adds the
137
+ "halt_reason" the batch halts with, in its workflow's words: write-failure
138
+ (skf-create-skill: write-failed) when the batch file cannot be written, an
139
+ ended batch's folder cannot be emptied, or the briefs' folder holds an
140
+ unfinished batch of other briefs; for --briefs, brief-missing when
141
+ no brief is found and helper-missing when the validator cannot be loaded;
142
+ input-invalid otherwise):
143
+ 0 the result was printed
144
+ 1 a batch file that cannot be read or is not UTF-8, a run folder with no
145
+ batch (start first) or with a batch of another file or of other
146
+ briefs, a batch file that cannot be written, a --batch number the
147
+ batch does not hold, a target whose run folder staged neither halt.json
148
+ nor result-context.json, an unreadable staged payload, or a summarize
149
+ while targets are still pending
150
+ 2 usage error (argparse)
151
+ """
152
+
153
+ from __future__ import annotations
154
+
155
+ import argparse
156
+ import hashlib
157
+ import importlib.util
158
+ import json
159
+ import os
160
+ import shutil
161
+ import sys
162
+ import time
163
+ from pathlib import Path
164
+
165
+ WORKFLOW = "skf-quick-skill"
166
+ CREATE_WORKFLOW = "skf-create-skill"
167
+ BATCH_FILE = "batch.jsonl"
168
+ HALT_PAYLOAD = "halt.json"
169
+ RESULT_PAYLOAD = "result-context.json"
170
+ SUMMARY_STEM = "quick-skill-batch"
171
+ SUMMARY_STEMS = {WORKFLOW: SUMMARY_STEM, CREATE_WORKFLOW: "create-skill-batch"}
172
+ MODIFIERS = {"language": "language_hint", "scope": "scope_hint"}
173
+ BRIEF_FILE = "skill-brief.yaml"
174
+ SCRIPTS = Path(__file__).resolve().parent
175
+ # Each workflow's envelope schema, whose emitter settings map each halt_reason
176
+ # to its exit code; the emitter derives the envelope's exit_code from it too.
177
+ SCHEMA_FILE = SCRIPTS / "schemas" / "skf-quick-skill-result-envelope.v1.json"
178
+ SCHEMA_FILES = {WORKFLOW: SCHEMA_FILE, CREATE_WORKFLOW: SCRIPTS / "schemas" / "skf-create-skill-result-envelope.v1.json"}
179
+ # The halt_reason a refused start gives, in each workflow's envelope words.
180
+ WRITE_FAILURE = {WORKFLOW: "write-failure", CREATE_WORKFLOW: "write-failed"}
181
+ VALIDATOR = SCRIPTS / "skf-validate-brief-schema.py"
182
+
183
+
184
+ class BatchError(Exception):
185
+ """A refusal: exit 1, the message on stderr, with the halt_reason of a refused start."""
186
+
187
+ def __init__(self, message: str, halt_reason: str | None = None) -> None:
188
+ super().__init__(message)
189
+ self.halt_reason = halt_reason
190
+
191
+
192
+ # ─── the batch file grammar ─────────────────────────────────────────────────
193
+
194
+
195
+ def _modifier(word: str) -> tuple[str, str] | None:
196
+ """(hint key, value) of a `language=` or `scope=` word, else None."""
197
+ key, sep, value = word.partition("=")
198
+ field = MODIFIERS.get(key.lower()) if sep else None
199
+ return (field, value) if field and value else None
200
+
201
+
202
+ def parse_line(line: str) -> dict | None:
203
+ """The target a batch-file line names, or None for a blank or `#` line."""
204
+ text = line.strip()
205
+ if not text or text.startswith("#"):
206
+ return None
207
+ words = text.split()
208
+ hints = {"language_hint": None, "scope_hint": None}
209
+ for word in words[1:]:
210
+ found = _modifier(word)
211
+ if found is None or hints[found[0]] is not None:
212
+ # Not a modifier, or one given twice: the line is a target of its own.
213
+ return {"target": text, "language_hint": None, "scope_hint": None}
214
+ hints[found[0]] = found[1]
215
+ return {"target": words[0], **hints}
216
+
217
+
218
+ def parse_text(text: str) -> list[dict]:
219
+ """The numbered targets of a batch file's text."""
220
+ targets = []
221
+ for line in text.splitlines():
222
+ entry = parse_line(line)
223
+ if entry is not None:
224
+ targets.append({"batch": len(targets) + 1, **entry})
225
+ return targets
226
+
227
+
228
+ def read_batch_file(path: str) -> list[dict]:
229
+ try:
230
+ # utf-8-sig: a byte-order mark an editor wrote is no part of the first target.
231
+ text = Path(path).read_text(encoding="utf-8-sig")
232
+ except UnicodeDecodeError:
233
+ raise BatchError(f"the batch file {path} is not UTF-8 text") from None
234
+ except OSError as e:
235
+ raise BatchError(f"the batch file {path} cannot be read: {e.strerror or e}") from None
236
+ return parse_text(text)
237
+
238
+
239
+ # ─── the batch state ────────────────────────────────────────────────────────
240
+
241
+
242
+ def _utc(seconds: float) -> tuple[str, str]:
243
+ """(ISO-8601 UTC timestamp, YYYYMMDD-HHmmss file stamp) of a Unix time."""
244
+ parts = time.gmtime(seconds)
245
+ return time.strftime("%Y-%m-%dT%H:%M:%SZ", parts), time.strftime("%Y%m%d-%H%M%S", parts)
246
+
247
+
248
+ def _append(run_dir: Path, entry: dict) -> None:
249
+ """Append one line to batch.jsonl, first ending a last line a write cut short."""
250
+ line = (json.dumps(entry, separators=(",", ":"), ensure_ascii=True) + "\n").encode("ascii")
251
+ with open(run_dir / BATCH_FILE, "ab+") as fh:
252
+ end = fh.seek(0, os.SEEK_END)
253
+ if end:
254
+ fh.seek(end - 1)
255
+ if fh.read(1) != b"\n":
256
+ line = b"\n" + line # the torn line stays a line of its own, which load skips
257
+ fh.write(line)
258
+
259
+
260
+ def load(run_dir: Path) -> dict:
261
+ """The batch run folder's state: its start line, targets, outcomes and summary."""
262
+ try:
263
+ text = (run_dir / BATCH_FILE).read_text(encoding="utf-8")
264
+ except FileNotFoundError:
265
+ raise BatchError(f"no batch in {run_dir.as_posix()}: run start first") from None
266
+ except (OSError, UnicodeDecodeError) as e:
267
+ raise BatchError(f"the batch in {run_dir.as_posix()} cannot be read: {e}") from None
268
+ state = {"start": None, "targets": {}, "results": {}, "summary": None}
269
+ for line in text.split("\n"):
270
+ try:
271
+ entry = json.loads(line) if line.strip() else None
272
+ except json.JSONDecodeError:
273
+ entry = None # a write cut short
274
+ if not isinstance(entry, dict):
275
+ continue
276
+ event = entry.get("event")
277
+ if event == "start":
278
+ state["start"] = entry
279
+ elif event == "target" and isinstance(entry.get("batch"), int):
280
+ state["targets"][entry["batch"]] = entry
281
+ elif event == "result" and isinstance(entry.get("batch"), int):
282
+ state["results"][entry["batch"]] = entry
283
+ elif event == "summary":
284
+ state["summary"] = entry
285
+ if state["start"] is None:
286
+ raise BatchError(f"the batch in {run_dir.as_posix()} has no start line: run start first")
287
+ return state
288
+
289
+
290
+ def workflow(state: dict) -> str:
291
+ """The workflow a batch runs: its start line's, skf-quick-skill for a batch older than the field."""
292
+ name = state["start"].get("workflow")
293
+ return name if name in SCHEMA_FILES else WORKFLOW
294
+
295
+
296
+ def pending(state: dict) -> list[dict]:
297
+ return [state["targets"][n] for n in sorted(state["targets"]) if n not in state["results"]]
298
+
299
+
300
+ def _failed(state: dict) -> list[dict]:
301
+ return [r for r in state["results"].values() if r.get("status") != "success"]
302
+
303
+
304
+ def _target_dir(run_dir: Path, number: int, started: dict | None = None) -> Path:
305
+ """Target `number`'s run folder: named after the batch's start for a brief
306
+ batch, whose folder name repeats for the same briefs."""
307
+ batch_id = (started or {}).get("batch_id")
308
+ if isinstance(batch_id, str) and batch_id:
309
+ return run_dir / f"{CREATE_WORKFLOW}-{batch_id}-{number}"
310
+ return run_dir / f"{run_dir.name}-{number}"
311
+
312
+
313
+ def brief_batch_dir(run_root: Path, briefs: list[str]) -> Path:
314
+ """The batch run folder of a set of briefs: one per set, found again for the same set."""
315
+ digest = hashlib.sha256("\n".join(briefs).encode("utf-8")).hexdigest()[:12]
316
+ return run_root / f"{CREATE_WORKFLOW}-batch-{digest}"
317
+
318
+
319
+ def _remove_tree(path: Path) -> None:
320
+ shutil.rmtree(path, ignore_errors=True)
321
+
322
+
323
+ # ─── subcommands ────────────────────────────────────────────────────────────
324
+
325
+
326
+ def cmd_parse(file: str) -> dict:
327
+ return {"targets": read_batch_file(file)}
328
+
329
+
330
+ def expand_briefs(paths: list[str]) -> list[str]:
331
+ """The briefs `paths` name, by absolute path with `/`, each once: a
332
+ folder gives its skill-brief.yaml files at any depth (hidden folders
333
+ left out) in path order, any other path is a brief itself."""
334
+ found: list[str] = []
335
+ for given in paths:
336
+ root = Path(given)
337
+ if root.is_dir():
338
+ briefs = sorted((p for p in root.rglob(BRIEF_FILE) if p.is_file()
339
+ and not any(part.startswith(".") for part in p.relative_to(root).parts)),
340
+ key=lambda p: p.relative_to(root).as_posix())
341
+ else:
342
+ briefs = [root]
343
+ for brief in briefs:
344
+ absolute = Path(os.path.abspath(brief)).as_posix()
345
+ if not any(_same_file(absolute, seen) for seen in found):
346
+ found.append(absolute)
347
+ return found
348
+
349
+
350
+ def _validator():
351
+ """skf-validate-brief-schema.py, the sibling that checks a brief."""
352
+ try:
353
+ spec = importlib.util.spec_from_file_location("skf_validate_brief_schema", VALIDATOR)
354
+ if spec is None or spec.loader is None:
355
+ raise ImportError(VALIDATOR.as_posix())
356
+ module = importlib.util.module_from_spec(spec)
357
+ spec.loader.exec_module(module)
358
+ except (ImportError, OSError) as e:
359
+ raise BatchError(f"the brief validator cannot be loaded ({VALIDATOR.name} beside {Path(__file__).name}): "
360
+ f"{e}", "helper-missing") from None
361
+ return module
362
+
363
+
364
+ def check_brief(validator, path: str) -> tuple[str | None, str | None, str | None]:
365
+ """(halt_reason, message, None) of a brief the validator refuses, else (None, None, its name)."""
366
+ file = Path(path)
367
+ if not file.is_file():
368
+ return "brief-missing", (f"Brief not found at `{path}`. Run [BS] Brief Skill to create one, or use [QS] "
369
+ "Quick Skill for brief-less generation."), None
370
+ try:
371
+ text = file.read_text(encoding="utf-8")
372
+ except (OSError, UnicodeDecodeError) as e:
373
+ return "brief-malformed", f"Brief validation failed: `{path}` cannot be read as UTF-8 text: {e}", None
374
+ brief, error = validator.load_brief_text(text)
375
+ if error is not None:
376
+ return "brief-malformed", error, None
377
+ result = validator.validate_brief(brief)
378
+ if not result["valid"]:
379
+ return "brief-invalid", result["errors"][0]["message"], None
380
+ return None, None, brief.get("name")
381
+
382
+
383
+ def check_briefs(validator, briefs: list[str]) -> list[tuple[int, str, str | None, str | None]]:
384
+ """(batch, brief, halt_reason, message) of every brief: refused by the
385
+ validator, or by a name an earlier valid brief already has."""
386
+ checked = []
387
+ first: dict[str, str] = {}
388
+ for n, brief in enumerate(briefs, start=1):
389
+ reason, message, name = check_brief(validator, brief)
390
+ if reason is None and isinstance(name, str) and name in first:
391
+ reason, message = "brief-invalid", (f"Brief validation failed: `{brief}` has the name `{name}` of "
392
+ f"`{first[name]}`, and both would compile into one skill folder. "
393
+ "Give each brief its own name.")
394
+ elif reason is None and isinstance(name, str):
395
+ first[name] = brief
396
+ checked.append((n, brief, reason, message))
397
+ return checked
398
+
399
+
400
+ def _same_file(a, b) -> bool:
401
+ if not isinstance(a, str) or not isinstance(b, str):
402
+ return False
403
+ return os.path.normcase(os.path.abspath(a)) == os.path.normcase(os.path.abspath(b))
404
+
405
+
406
+ def cmd_start(file: str, run_dir: Path, fail_fast: bool) -> dict:
407
+ # The absolute path, so a start run again from another working folder finds the same batch.
408
+ path = os.path.abspath(file)
409
+ try:
410
+ if (run_dir / BATCH_FILE).exists():
411
+ state = load(run_dir)
412
+ started = state["start"]
413
+ if not _same_file(started.get("input_file"), path):
414
+ raise BatchError(f"{run_dir.as_posix()} holds the batch of {started.get('input_file')}, "
415
+ f"not {path}: give this batch a run folder of its own")
416
+ return {"status": "running", "input_file": started.get("input_file"),
417
+ "fail_fast": bool(started.get("fail_fast")), "targets_total": len(state["targets"]),
418
+ "recorded": len(state["results"]), "resumed": True}
419
+ targets = read_batch_file(path)
420
+ except BatchError as e:
421
+ raise BatchError(str(e), "input-invalid") from None
422
+ try:
423
+ run_dir.mkdir(parents=True, exist_ok=True)
424
+ _append(run_dir, {"event": "start", "status": "running", "input_file": path,
425
+ "fail_fast": fail_fast, "started_at": _utc(time.time())[0]})
426
+ for target in targets:
427
+ _append(run_dir, {"event": "target", **target, "status": "pending"})
428
+ except OSError as e:
429
+ raise BatchError(f"the batch file cannot be written in {run_dir.as_posix()}: {e.strerror or e}",
430
+ "write-failure") from None
431
+ return {"status": "running", "input_file": path, "fail_fast": fail_fast,
432
+ "targets_total": len(targets), "recorded": 0, "resumed": False}
433
+
434
+
435
+ def cmd_start_briefs(paths: list[str], run_root: Path, fail_fast: bool) -> dict:
436
+ briefs = expand_briefs(paths)
437
+ if not briefs:
438
+ raise BatchError(f"no {BRIEF_FILE} under {', '.join(paths)}", "brief-missing")
439
+ run_dir = brief_batch_dir(run_root, briefs)
440
+ if (run_dir / BATCH_FILE).exists():
441
+ try:
442
+ state = load(run_dir)
443
+ except BatchError:
444
+ state = None # a batch file with no start line: nothing to resume
445
+ if state is not None and state["summary"] is None:
446
+ started = state["start"]
447
+ if workflow(state) != CREATE_WORKFLOW or started.get("input_briefs") != briefs:
448
+ raise BatchError(f"{run_dir.as_posix()} holds an unfinished batch of other briefs: "
449
+ "finish or remove it first", WRITE_FAILURE[CREATE_WORKFLOW])
450
+ return {"status": "running", "workflow": CREATE_WORKFLOW, "batch_dir": run_dir.as_posix(),
451
+ "input_briefs": briefs, "fail_fast": bool(started.get("fail_fast")),
452
+ "targets_total": len(state["targets"]), "recorded": len(state["results"]), "resumed": True,
453
+ "rejected": [{"batch": r["batch"], "target": r["target"], "error_code": r["error_code"],
454
+ "message": r.get("message")}
455
+ for _, r in sorted(state["results"].items()) if r.get("rejected")]}
456
+ validator = _validator()
457
+ codes = exit_codes(CREATE_WORKFLOW)
458
+ checked = check_briefs(validator, briefs)
459
+ timestamp, stamp = _utc(time.time())
460
+ try:
461
+ if run_dir.exists():
462
+ _remove_tree(run_dir) # an ended batch of these briefs gives way to this one
463
+ if (run_dir / BATCH_FILE).exists():
464
+ raise OSError(f"the ended batch's {BATCH_FILE} could not be removed")
465
+ run_dir.mkdir(parents=True, exist_ok=True)
466
+ _append(run_dir, {"event": "start", "status": "running", "workflow": CREATE_WORKFLOW,
467
+ "input_file": None, "input_briefs": briefs, "fail_fast": fail_fast,
468
+ "batch_id": f"batch-{stamp}", "started_at": timestamp})
469
+ for n, brief, _, _ in checked:
470
+ _append(run_dir, {"event": "target", "batch": n, "target": brief, "language_hint": None,
471
+ "scope_hint": None, "status": "pending"})
472
+ rejected = []
473
+ for n, brief, reason, message in checked:
474
+ if reason is None:
475
+ continue
476
+ _append(run_dir, {"event": "result", "batch": n, "target": brief, "status": "error",
477
+ "exit_code": codes.get(reason, 2), "skill_package": None, "error_code": reason,
478
+ "quality_score": None, "rejected": True, "message": message})
479
+ rejected.append({"batch": n, "target": brief, "error_code": reason, "message": message})
480
+ except OSError as e:
481
+ raise BatchError(f"the batch file cannot be written in {run_dir.as_posix()}: {e.strerror or e}",
482
+ WRITE_FAILURE[CREATE_WORKFLOW]) from None
483
+ return {"status": "running", "workflow": CREATE_WORKFLOW, "batch_dir": run_dir.as_posix(), "input_briefs": briefs,
484
+ "fail_fast": fail_fast, "targets_total": len(briefs), "recorded": len(rejected), "resumed": False,
485
+ "rejected": rejected}
486
+
487
+
488
+ def _stop_early(state: dict) -> bool:
489
+ return bool(state["start"].get("fail_fast")) and bool(_failed(state))
490
+
491
+
492
+ def cmd_next(run_dir: Path) -> dict:
493
+ state = load(run_dir)
494
+ left = pending(state)
495
+ if state["summary"] is not None or not left or _stop_early(state):
496
+ return {"status": "done", "fail_fast_triggered": _stop_early(state) and bool(left)}
497
+ target = left[0]
498
+ folder = _target_dir(run_dir, target["batch"], state["start"])
499
+ if (folder / HALT_PAYLOAD).exists() or (folder / RESULT_PAYLOAD).exists():
500
+ return {"status": "record", "batch": target["batch"]} # it ended: record it, never run it twice
501
+ try:
502
+ if folder.exists():
503
+ _remove_tree(folder) # an earlier try of this target left it
504
+ folder.mkdir(parents=True)
505
+ except OSError as e:
506
+ raise BatchError(f"the target's run folder {folder.as_posix()} cannot be created: "
507
+ f"{e.strerror or e}") from None
508
+ return {"status": "next", "batch": target["batch"], "target": target["target"],
509
+ "language_hint": target.get("language_hint"), "scope_hint": target.get("scope_hint"),
510
+ "run_dir": folder.as_posix()}
511
+
512
+
513
+ def exit_codes(name: str = WORKFLOW) -> dict:
514
+ """halt_reason -> exit code, from the workflow's envelope schema's emitter settings."""
515
+ try:
516
+ schema = json.loads(SCHEMA_FILES[name].read_text(encoding="utf-8"))
517
+ codes = schema["$defs"]["skf-envelope"]["const"]["exit_codes"]
518
+ except (OSError, ValueError, KeyError, TypeError):
519
+ return {}
520
+ return codes if isinstance(codes, dict) else {}
521
+
522
+
523
+ def _read_payload(path: Path) -> dict:
524
+ try:
525
+ payload = json.loads(path.read_text(encoding="utf-8-sig"))
526
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError) as e:
527
+ raise BatchError(f"{path.as_posix()} holds no JSON object: {e}") from None
528
+ if not isinstance(payload, dict):
529
+ raise BatchError(f"{path.as_posix()} holds no JSON object")
530
+ return payload
531
+
532
+
533
+ def outcome(folder: Path, name: str = WORKFLOW) -> dict:
534
+ """How the target whose run folder is `folder` ended, from its staged payload."""
535
+ halt, result = folder / HALT_PAYLOAD, folder / RESULT_PAYLOAD
536
+ if halt.exists():
537
+ payload = _read_payload(halt)
538
+ reason = payload.get("halt_reason")
539
+ code = exit_codes(name).get(reason) if isinstance(reason, str) else None
540
+ given = payload.get("exit_code")
541
+ if code is None and isinstance(given, int) and not isinstance(given, bool):
542
+ code = given
543
+ if code is None:
544
+ raise BatchError(f"{halt.as_posix()}: halt_reason {reason!r} maps to no exit code")
545
+ package = payload.get("skill_package")
546
+ return {"status": "error", "exit_code": code,
547
+ "skill_package": package if isinstance(package, str) else None,
548
+ "error_code": reason, "quality_score": None}
549
+ if result.exists():
550
+ payload = _read_payload(result)
551
+ summary = payload.get("summary") if isinstance(payload.get("summary"), dict) else {}
552
+ score = summary.get("quality_score")
553
+ package = payload.get("skill_package")
554
+ return {"status": "success", "exit_code": 0,
555
+ "skill_package": package if isinstance(package, str) else None, "error_code": None,
556
+ "quality_score": score if isinstance(score, (int, float)) and not isinstance(score, bool) else None}
557
+ raise BatchError(f"{folder.as_posix()} staged neither {HALT_PAYLOAD} nor {RESULT_PAYLOAD}: stage the "
558
+ f"payload the target ended with ({HALT_PAYLOAD} or {RESULT_PAYLOAD}) and record it again")
559
+
560
+
561
+ def start_event(handed_out: dict) -> dict:
562
+ return {"batch": handed_out["batch"], "target": handed_out["target"], "status": "start"}
563
+
564
+
565
+ def end_event(entry: dict) -> dict:
566
+ if entry["status"] == "success":
567
+ return {"batch": entry["batch"], "target": entry["target"], "status": "done", "exit": 0}
568
+ return {"batch": entry["batch"], "target": entry["target"], "status": "fail",
569
+ "exit": entry["exit_code"], "error_code": entry["error_code"]}
570
+
571
+
572
+ def cmd_record(run_dir: Path, number: int) -> dict:
573
+ state = load(run_dir)
574
+ target = state["targets"].get(number)
575
+ if target is None:
576
+ raise BatchError(f"the batch in {run_dir.as_posix()} has no target {number}")
577
+ known = state["results"].get(number)
578
+ if known is not None:
579
+ return end_event(known) # next never hands a recorded target out again
580
+ folder = _target_dir(run_dir, number, state["start"])
581
+ entry = {"event": "result", "batch": number, "target": target["target"], **outcome(folder, workflow(state))}
582
+ try:
583
+ _append(run_dir, entry)
584
+ except OSError as e:
585
+ raise BatchError(f"the outcome cannot be recorded in {run_dir.as_posix()}: {e.strerror or e}") from None
586
+ if entry["status"] == "success":
587
+ _remove_tree(folder) # a run folder is kept only for a halt
588
+ return end_event(entry)
589
+
590
+
591
+ def _claim(folder: Path, stamp: str, stem: str = SUMMARY_STEM) -> Path:
592
+ """Create the per-batch summary file, empty, under a name no batch holds yet."""
593
+ for n in range(1, 1000):
594
+ path = folder / (f"{stem}-{stamp}.json" if n == 1 else f"{stem}-{stamp}-{n}.json")
595
+ try:
596
+ os.close(os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY))
597
+ except FileExistsError:
598
+ continue
599
+ return path
600
+ raise OSError(f"no free file name for {stem}-{stamp}.json")
601
+
602
+
603
+ def _write_json(path: Path, value: dict) -> None:
604
+ """Write `value` as indented JSON through a temporary file and one rename."""
605
+ tmp = path.with_name(f".{path.name}.{os.getpid()}.tmp")
606
+ try:
607
+ with open(tmp, "w", encoding="utf-8", newline="\n") as fh:
608
+ json.dump(value, fh, indent=2, ensure_ascii=False)
609
+ fh.write("\n")
610
+ os.replace(tmp, path)
611
+ except OSError:
612
+ try:
613
+ tmp.unlink()
614
+ except OSError:
615
+ pass
616
+ raise
617
+
618
+
619
+ def summary_event(summary: dict) -> dict:
620
+ event = {"batch_summary": True}
621
+ for key in ("targets_total", "succeeded", "failed", "status", "fail_fast_triggered", "exit_code",
622
+ "summary_path"):
623
+ event[key] = summary.get(key)
624
+ if summary.get("warnings"):
625
+ event["warnings"] = summary["warnings"]
626
+ return event
627
+
628
+
629
+ def cmd_summarize(run_dir: Path, output_dir: Path) -> dict:
630
+ if not (run_dir / BATCH_FILE).exists() and not run_dir.exists():
631
+ stem = SUMMARY_STEMS[CREATE_WORKFLOW if run_dir.name.startswith(f"{CREATE_WORKFLOW}-") else WORKFLOW]
632
+ raise BatchError(f"no batch in {run_dir.as_posix()}: a batch whose targets all succeeded removes its "
633
+ f"run folder once summarized; its summary is "
634
+ f"{(output_dir / f'{stem}-latest.json').as_posix()}")
635
+ state = load(run_dir)
636
+ name = workflow(state)
637
+ if state["summary"] is not None:
638
+ return summary_event(state["summary"])
639
+ left, stopped = pending(state), _stop_early(state)
640
+ if left and not stopped:
641
+ raise BatchError(f"{len(left)} target(s) of the batch are still pending: run next and finish "
642
+ "them before summarize")
643
+ results = [state["results"][n] for n in sorted(state["results"])]
644
+ failed = [r for r in results if r.get("status") != "success"]
645
+ succeeded = len(results) - len(failed)
646
+ status = "success" if not failed else ("partial" if succeeded else "failed")
647
+ exit_code = max((r["exit_code"] for r in failed), default=0)
648
+ timestamp, stamp = _utc(time.time())
649
+ record = {
650
+ "skill": name,
651
+ "mode": "batch",
652
+ "status": status,
653
+ "timestamp": timestamp,
654
+ **({"input_briefs": state["start"].get("input_briefs")} if name == CREATE_WORKFLOW
655
+ else {"input_file": state["start"].get("input_file")}),
656
+ "targets_total": len(results),
657
+ "succeeded": succeeded,
658
+ "failed": len(failed),
659
+ "fail_fast_triggered": stopped and bool(left),
660
+ "exit_code": exit_code,
661
+ "results": [{key: r.get(key) for key in ("batch", "target", "status", "exit_code", "skill_package",
662
+ "error_code", "quality_score")} for r in results],
663
+ }
664
+ warnings, summary_path = [], None
665
+ try:
666
+ output_dir.mkdir(parents=True, exist_ok=True)
667
+ per_batch = _claim(output_dir, stamp, SUMMARY_STEMS[name])
668
+ _write_json(per_batch, record)
669
+ summary_path = per_batch.as_posix()
670
+ _write_json(output_dir / f"{SUMMARY_STEMS[name]}-latest.json", record)
671
+ except OSError as e:
672
+ warnings.append(f"summary_write_failed: {output_dir.as_posix()}: {e.strerror or e}")
673
+ final = {"event": "summary", **{k: v for k, v in record.items() if k != "results"},
674
+ "summary_path": summary_path, "warnings": warnings}
675
+ try:
676
+ _append(run_dir, final)
677
+ except OSError as e:
678
+ warnings.append(f"batch_file_write_failed: {run_dir.as_posix()}: {e.strerror or e}")
679
+ inside = output_dir.resolve().is_relative_to(run_dir.resolve())
680
+ if not failed and summary_path is not None and not inside and run_dir.name.startswith(f"{name}-"):
681
+ _remove_tree(run_dir) # kept when a target halted, or when batch.jsonl is the only record
682
+ return summary_event(final)
683
+
684
+
685
+ # ─── CLI ─────────────────────────────────────────────────────────────────────
686
+
687
+
688
+ def _error(error: BatchError) -> None:
689
+ body = {"status": "error", "message": str(error)}
690
+ if error.halt_reason:
691
+ body["halt_reason"] = error.halt_reason
692
+ print(json.dumps(body), file=sys.stderr)
693
+
694
+
695
+ def _line(value: dict) -> str:
696
+ return json.dumps(value, separators=(",", ":"), ensure_ascii=True)
697
+
698
+
699
+ def _build_parser() -> argparse.ArgumentParser:
700
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
701
+ sub = parser.add_subparsers(dest="cmd", required=True)
702
+ p_parse = sub.add_parser("parse", help="print the batch file's numbered targets")
703
+ p_parse.add_argument("file", help="the batch file")
704
+ p_start = sub.add_parser("start", help="write the batch file, every target pending")
705
+ p_start.add_argument("file", nargs="?", default=None, help="the batch file given to --batch (skf-quick-skill)")
706
+ p_start.add_argument("--briefs", nargs="+", default=None, metavar="PATH",
707
+ help="skf-create-skill: the briefs, or folders of briefs, given to --batch")
708
+ p_start.add_argument("--run-dir", default=None, help="the batch run folder (with the batch file)")
709
+ p_start.add_argument("--run-root", default=None,
710
+ help="with --briefs: the folder the briefs' batch run folder is made in")
711
+ p_start.add_argument("--fail-fast", action="store_true", help="end the batch at the first failed target")
712
+ p_next = sub.add_parser("next", help="print the next pending target, create its run folder and print its "
713
+ "start event on stderr")
714
+ p_next.add_argument("--run-dir", required=True, help="the batch run folder")
715
+ p_record = sub.add_parser("record", help="record how a target ended and print its end event")
716
+ p_record.add_argument("--run-dir", required=True, help="the batch run folder")
717
+ p_record.add_argument("--batch", type=int, required=True, help="the target's number, as next printed it")
718
+ p_summary = sub.add_parser("summarize", help="write the batch summary and print the batch_summary event")
719
+ p_summary.add_argument("--run-dir", required=True, help="the batch run folder")
720
+ p_summary.add_argument("--output-dir", required=True, help="the folder the summary files go to")
721
+ for p in (p_record, p_summary):
722
+ p.add_argument("--target", choices=["stdout", "stderr"], default="stdout",
723
+ help="the stream the event line goes to (default: stdout)")
724
+ return parser
725
+
726
+
727
+ def main(argv: list[str] | None = None) -> int:
728
+ args = _build_parser().parse_args(argv)
729
+ try:
730
+ if args.cmd == "parse":
731
+ out = cmd_parse(args.file)
732
+ elif args.cmd == "start" and args.briefs is not None:
733
+ if args.file is not None:
734
+ raise BatchError("give start a batch file or --briefs, not both", "input-invalid")
735
+ if args.run_root is None or args.run_dir is not None:
736
+ raise BatchError("start --briefs takes --run-root, not --run-dir: the briefs name their batch "
737
+ "run folder", "input-invalid")
738
+ out = cmd_start_briefs(args.briefs, Path(args.run_root), args.fail_fast)
739
+ elif args.cmd == "start":
740
+ if args.file is None:
741
+ raise BatchError("start needs the batch file, or --briefs", "input-invalid")
742
+ if args.run_dir is None or args.run_root is not None:
743
+ raise BatchError("start with a batch file takes --run-dir, not --run-root", "input-invalid")
744
+ out = cmd_start(args.file, Path(args.run_dir), args.fail_fast)
745
+ elif args.cmd == "next":
746
+ out = cmd_next(Path(args.run_dir))
747
+ elif args.cmd == "record":
748
+ out = cmd_record(Path(args.run_dir), args.batch)
749
+ else:
750
+ out = cmd_summarize(Path(args.run_dir), Path(args.output_dir))
751
+ except BatchError as e:
752
+ _error(e)
753
+ return 1
754
+ if args.cmd == "next" and out["status"] == "next":
755
+ print(_line(start_event(out)), file=sys.stderr)
756
+ stream = sys.stderr if getattr(args, "target", "stdout") == "stderr" else sys.stdout
757
+ print(_line(out), file=stream)
758
+ return 0
759
+
760
+
761
+ def _force_utf8(*streams) -> None:
762
+ """Reconfigure the JSON streams to UTF-8 (a Windows console uses cp1252)."""
763
+ for stream in streams:
764
+ if hasattr(stream, "reconfigure"):
765
+ errors = getattr(stream, "errors", None)
766
+ if errors is None:
767
+ stream.reconfigure(encoding="utf-8")
768
+ else:
769
+ stream.reconfigure(encoding="utf-8", errors=errors)
770
+
771
+
772
+ if __name__ == "__main__":
773
+ _force_utf8(sys.stdout, sys.stderr)
774
+ sys.exit(main())