bmad-module-skill-forge 2.2.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (437) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +49 -49
  3. package/docs/404.md +10 -8
  4. package/docs/_data/pinned.yaml +22 -13
  5. package/docs/_internal/STABILITY.md +58 -35
  6. package/docs/agents.md +39 -53
  7. package/docs/architecture.md +65 -51
  8. package/docs/bmad-synergy.md +53 -51
  9. package/docs/campaign.md +127 -75
  10. package/docs/concepts.md +34 -28
  11. package/docs/examples.md +70 -68
  12. package/docs/forge-auto.md +34 -31
  13. package/docs/getting-started.md +118 -57
  14. package/docs/how-it-works.md +25 -21
  15. package/docs/index.md +15 -15
  16. package/docs/skill-model.md +70 -59
  17. package/docs/troubleshooting.md +143 -18
  18. package/docs/verifying-a-skill.md +87 -57
  19. package/docs/why-skf.md +24 -22
  20. package/docs/workflows.md +274 -110
  21. package/package.json +18 -10
  22. package/src/forger/preferences.yaml +8 -1
  23. package/src/knowledge/agentskills-spec.md +2 -0
  24. package/src/knowledge/ccc-bridge.md +40 -24
  25. package/src/knowledge/confidence-tiers.md +9 -5
  26. package/src/knowledge/overview.md +3 -3
  27. package/src/knowledge/progressive-capability.md +8 -8
  28. package/src/knowledge/provenance-tracking.md +9 -5
  29. package/src/knowledge/qmd-registry.md +3 -3
  30. package/src/knowledge/skf-knowledge-index.csv +2 -2
  31. package/src/knowledge/skill-lifecycle.md +18 -11
  32. package/src/knowledge/split-body-strategy.md +0 -3
  33. package/src/knowledge/tool-resolution.md +4 -2
  34. package/src/knowledge/version-paths.md +101 -15
  35. package/src/module-help.csv +3 -3
  36. package/src/module.yaml +10 -0
  37. package/src/shared/data/ast-grep-recipes.yaml +608 -0
  38. package/src/shared/data/ide-context-files.json +30 -0
  39. package/src/shared/health-check.md +31 -59
  40. package/src/shared/references/description-guard-protocol.md +12 -9
  41. package/src/shared/references/feasibility-report-schema.md +19 -13
  42. package/src/shared/references/headless-gate-convention.md +55 -3
  43. package/src/shared/references/output-contract-schema.md +19 -8
  44. package/src/shared/references/pipeline-contracts.md +94 -29
  45. package/src/shared/references/tessl-review.md +77 -0
  46. package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
  47. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
  48. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
  49. package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
  50. package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
  51. package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
  52. package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
  53. package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
  54. package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
  55. package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
  56. package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
  57. package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
  58. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
  59. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
  60. package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
  61. package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
  62. package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
  63. package/src/shared/scripts/skf-atomic-write.py +233 -64
  64. package/src/shared/scripts/skf-build-change-manifest.py +2753 -28
  65. package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
  66. package/src/shared/scripts/skf-check-unit-records.py +184 -0
  67. package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
  68. package/src/shared/scripts/skf-classify-changed-files.py +772 -0
  69. package/src/shared/scripts/skf-comention-pairs.py +1333 -123
  70. package/src/shared/scripts/skf-compare-file-hashes.py +267 -105
  71. package/src/shared/scripts/skf-count-imports.py +1867 -0
  72. package/src/shared/scripts/skf-count-tokens.py +1 -1
  73. package/src/shared/scripts/skf-derive-assembly-shape.py +21 -3
  74. package/src/shared/scripts/skf-description-guard.py +99 -11
  75. package/src/shared/scripts/skf-detect-docs.py +694 -2
  76. package/src/shared/scripts/skf-detect-language.py +486 -107
  77. package/src/shared/scripts/skf-detect-registry.py +887 -0
  78. package/src/shared/scripts/skf-detect-scripts-assets.py +197 -55
  79. package/src/shared/scripts/skf-detect-tools.py +209 -27
  80. package/src/shared/scripts/skf-detect-workspaces.py +506 -30
  81. package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
  82. package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
  83. package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
  84. package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
  85. package/src/shared/scripts/skf-extract-public-api.py +5247 -96
  86. package/src/shared/scripts/skf-extraction-inventory.py +1280 -0
  87. package/src/shared/scripts/skf-extraction-snapshot.py +664 -0
  88. package/src/shared/scripts/skf-fetch-temporal.py +674 -0
  89. package/src/shared/scripts/skf-find-cycles.py +97 -29
  90. package/src/shared/scripts/skf-find-test-report.py +476 -0
  91. package/src/shared/scripts/skf-forge-tier-rw.py +469 -205
  92. package/src/shared/scripts/skf-github-fetch.py +365 -0
  93. package/src/shared/scripts/skf-github-probe.py +700 -0
  94. package/src/shared/scripts/skf-hash-content.py +180 -20
  95. package/src/shared/scripts/skf-language-corpora.py +1 -1
  96. package/src/shared/scripts/skf-load-provenance.py +161 -27
  97. package/src/shared/scripts/skf-manifest-ops.py +1 -1
  98. package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
  99. package/src/shared/scripts/skf-merge-doc-urls.py +143 -20
  100. package/src/shared/scripts/skf-names-present.py +221 -0
  101. package/src/shared/scripts/skf-pair-intersect.py +119 -28
  102. package/src/shared/scripts/skf-parse-gaps.py +1384 -0
  103. package/src/shared/scripts/skf-preapply.py +1 -1
  104. package/src/shared/scripts/skf-preflight.py +176 -28
  105. package/src/shared/scripts/skf-promote-staged.py +430 -0
  106. package/src/shared/scripts/skf-provenance-gap-dispatch.py +64 -8
  107. package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
  108. package/src/shared/scripts/skf-quick-batch.py +774 -0
  109. package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
  110. package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
  111. package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
  112. package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
  113. package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
  114. package/src/shared/scripts/skf-resolve-authoritative-files.py +155 -14
  115. package/src/shared/scripts/skf-resolve-package.py +523 -106
  116. package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
  117. package/src/shared/scripts/skf-run-lock.py +577 -0
  118. package/src/shared/scripts/skf-scan-manifests.py +998 -57
  119. package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
  120. package/src/shared/scripts/skf-severity-classify.py +712 -108
  121. package/src/shared/scripts/skf-shape-detect.py +298 -204
  122. package/src/shared/scripts/skf-shard-body.py +9 -8
  123. package/src/shared/scripts/skf-skill-inventory.py +2412 -89
  124. package/src/shared/scripts/skf-skills-module.py +472 -0
  125. package/src/shared/scripts/skf-source-tree.py +1796 -0
  126. package/src/shared/scripts/skf-structural-diff.py +787 -121
  127. package/src/shared/scripts/skf-tessl-review.py +706 -0
  128. package/src/shared/scripts/skf-update-active-symlink.py +120 -67
  129. package/src/shared/scripts/skf-update-run-state.py +624 -0
  130. package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
  131. package/src/shared/scripts/skf-validate-brief-schema.py +23 -2
  132. package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
  133. package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
  134. package/src/shared/scripts/skf-validate-output.py +712 -64
  135. package/src/shared/scripts/skf-validate-pins.py +1 -1
  136. package/src/shared/scripts/skf-verify-no-trace.py +116 -10
  137. package/src/shared/scripts/skf-verify-provenance-completeness.py +3945 -152
  138. package/src/shared/scripts/skf-write-skill-brief.py +406 -25
  139. package/src/shared/tool-requirements.yaml +201 -0
  140. package/src/skf-analyze-source/SKILL.md +41 -36
  141. package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
  142. package/src/skf-analyze-source/customize.toml +36 -24
  143. package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
  144. package/src/skf-analyze-source/references/continue.md +2 -13
  145. package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
  146. package/src/skf-analyze-source/references/generate-briefs.md +121 -55
  147. package/src/skf-analyze-source/references/headless-contract.md +42 -15
  148. package/src/skf-analyze-source/references/identify-units.md +96 -110
  149. package/src/skf-analyze-source/references/init.md +73 -71
  150. package/src/skf-analyze-source/references/map-and-detect.md +123 -105
  151. package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
  152. package/src/skf-analyze-source/references/recommend.md +51 -70
  153. package/src/skf-analyze-source/references/scan-project.md +60 -84
  154. package/src/skf-analyze-source/references/scan-root.md +39 -0
  155. package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
  156. package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
  157. package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
  158. package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
  159. package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
  160. package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
  161. package/src/skf-audit-skill/SKILL.md +43 -51
  162. package/src/skf-audit-skill/assets/drift-report-template.md +28 -3
  163. package/src/skf-audit-skill/customize.toml +41 -20
  164. package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
  165. package/src/skf-audit-skill/references/doc-drift.md +141 -0
  166. package/src/skf-audit-skill/references/headless-contract.md +29 -0
  167. package/src/skf-audit-skill/references/health-check.md +2 -0
  168. package/src/skf-audit-skill/references/init.md +149 -210
  169. package/src/skf-audit-skill/references/re-index.md +103 -110
  170. package/src/skf-audit-skill/references/report.md +96 -33
  171. package/src/skf-audit-skill/references/semantic-diff.md +34 -25
  172. package/src/skf-audit-skill/references/severity-classify.md +79 -65
  173. package/src/skf-audit-skill/references/severity-rules.md +15 -6
  174. package/src/skf-audit-skill/references/structural-diff.md +80 -90
  175. package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
  176. package/src/skf-audit-skill/scripts/render-drift-tables.py +712 -0
  177. package/src/skf-brief-skill/SKILL.md +37 -26
  178. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  179. package/src/skf-brief-skill/assets/scope-templates.md +18 -25
  180. package/src/skf-brief-skill/assets/skill-brief-schema.md +30 -23
  181. package/src/skf-brief-skill/customize.toml +48 -20
  182. package/src/skf-brief-skill/references/analyze-target.md +144 -127
  183. package/src/skf-brief-skill/references/confirm-brief.md +11 -9
  184. package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
  185. package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
  186. package/src/skf-brief-skill/references/gather-intent.md +87 -182
  187. package/src/skf-brief-skill/references/headless-args.md +84 -16
  188. package/src/skf-brief-skill/references/health-check.md +1 -1
  189. package/src/skf-brief-skill/references/invocation-contract.md +18 -13
  190. package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
  191. package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
  192. package/src/skf-brief-skill/references/scope-definition.md +123 -47
  193. package/src/skf-brief-skill/references/step-auto-brief.md +26 -100
  194. package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
  195. package/src/skf-brief-skill/references/version-resolution.md +7 -10
  196. package/src/skf-brief-skill/references/write-brief.md +66 -57
  197. package/src/skf-campaign/SKILL.md +31 -34
  198. package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
  199. package/src/skf-campaign/customize.toml +47 -16
  200. package/src/skf-campaign/references/campaign-contracts.md +39 -29
  201. package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
  202. package/src/skf-campaign/references/health-check.md +7 -16
  203. package/src/skf-campaign/references/step-01-setup.md +46 -84
  204. package/src/skf-campaign/references/step-02-strategy.md +31 -30
  205. package/src/skf-campaign/references/step-03-pins.md +26 -18
  206. package/src/skf-campaign/references/step-04-provenance.md +24 -23
  207. package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
  208. package/src/skf-campaign/references/step-06-batch.md +41 -34
  209. package/src/skf-campaign/references/step-07-capstone.md +40 -25
  210. package/src/skf-campaign/references/step-08-verify.md +28 -29
  211. package/src/skf-campaign/references/step-09-refine.md +29 -30
  212. package/src/skf-campaign/references/step-10-export.md +48 -36
  213. package/src/skf-campaign/references/step-11-maintenance.md +31 -34
  214. package/src/skf-campaign/references/step-resume.md +37 -68
  215. package/src/skf-campaign/scripts/campaign-deps.py +57 -18
  216. package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
  217. package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
  218. package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
  219. package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
  220. package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
  221. package/src/skf-campaign/scripts/campaign-report.py +204 -37
  222. package/src/skf-campaign/scripts/campaign-state.py +1128 -0
  223. package/src/skf-campaign/scripts/campaign-status.py +10 -74
  224. package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
  225. package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
  226. package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
  227. package/src/skf-campaign/templates/campaign-report-template.md +5 -1
  228. package/src/skf-campaign/templates/kickoff-template.md +0 -14
  229. package/src/skf-create-skill/SKILL.md +27 -20
  230. package/src/skf-create-skill/assets/compile-assembly-rules.md +11 -8
  231. package/src/skf-create-skill/assets/skill-sections.md +21 -11
  232. package/src/skf-create-skill/customize.toml +20 -8
  233. package/src/skf-create-skill/references/authoritative-files-protocol.md +36 -58
  234. package/src/skf-create-skill/references/batch-mode.md +81 -0
  235. package/src/skf-create-skill/references/ccc-index-check.md +27 -0
  236. package/src/skf-create-skill/references/compile.md +119 -70
  237. package/src/skf-create-skill/references/component-extraction.md +113 -91
  238. package/src/skf-create-skill/references/enrich.md +41 -23
  239. package/src/skf-create-skill/references/entry-points-by-hand.md +37 -0
  240. package/src/skf-create-skill/references/extract.md +204 -162
  241. package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
  242. package/src/skf-create-skill/references/extraction-patterns-tracing.md +11 -11
  243. package/src/skf-create-skill/references/extraction-patterns.md +588 -201
  244. package/src/skf-create-skill/references/generate-artifacts.md +145 -93
  245. package/src/skf-create-skill/references/health-check.md +1 -1
  246. package/src/skf-create-skill/references/load-brief.md +53 -36
  247. package/src/skf-create-skill/references/relabel-rule.md +12 -0
  248. package/src/skf-create-skill/references/report.md +37 -57
  249. package/src/skf-create-skill/references/source-resolution-protocols.md +58 -169
  250. package/src/skf-create-skill/references/step-auto-shard.md +20 -93
  251. package/src/skf-create-skill/references/step-doc-rot.md +44 -60
  252. package/src/skf-create-skill/references/step-doc-sources.md +28 -19
  253. package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
  254. package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
  255. package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
  256. package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
  257. package/src/skf-create-skill/references/validate.md +173 -99
  258. package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
  259. package/src/skf-create-stack-skill/SKILL.md +29 -56
  260. package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
  261. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
  262. package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
  263. package/src/skf-create-stack-skill/customize.toml +23 -12
  264. package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
  265. package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
  266. package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
  267. package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
  268. package/src/skf-create-stack-skill/references/generate-output.md +138 -128
  269. package/src/skf-create-stack-skill/references/init.md +90 -32
  270. package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
  271. package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
  272. package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
  273. package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
  274. package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
  275. package/src/skf-create-stack-skill/references/report.md +39 -33
  276. package/src/skf-create-stack-skill/references/validate.md +20 -39
  277. package/src/skf-drop-skill/SKILL.md +28 -51
  278. package/src/skf-drop-skill/customize.toml +44 -30
  279. package/src/skf-drop-skill/references/execute.md +83 -146
  280. package/src/skf-drop-skill/references/health-check.md +2 -0
  281. package/src/skf-drop-skill/references/invocation-contract.md +64 -0
  282. package/src/skf-drop-skill/references/report.md +46 -30
  283. package/src/skf-drop-skill/references/select.md +122 -124
  284. package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
  285. package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
  286. package/src/skf-export-skill/SKILL.md +40 -62
  287. package/src/skf-export-skill/assets/managed-section-format.md +14 -50
  288. package/src/skf-export-skill/assets/snippet-format.md +3 -1
  289. package/src/skf-export-skill/customize.toml +42 -13
  290. package/src/skf-export-skill/references/generate-snippet.md +54 -46
  291. package/src/skf-export-skill/references/health-check.md +2 -0
  292. package/src/skf-export-skill/references/invocation-contract.md +15 -0
  293. package/src/skf-export-skill/references/load-skill.md +88 -62
  294. package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
  295. package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
  296. package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
  297. package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
  298. package/src/skf-export-skill/references/package.md +15 -73
  299. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
  300. package/src/skf-export-skill/references/result-envelope.md +31 -16
  301. package/src/skf-export-skill/references/summary.md +41 -17
  302. package/src/skf-export-skill/references/token-report.md +4 -12
  303. package/src/skf-export-skill/references/update-context.md +118 -237
  304. package/src/skf-forger/SKILL.md +61 -37
  305. package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
  306. package/src/skf-forger/customize.toml +43 -0
  307. package/src/skf-forger/references/pipeline-mode.md +73 -18
  308. package/src/skf-forger/scripts/forge-status.py +299 -0
  309. package/src/skf-forger/scripts/parse-pipeline.py +322 -54
  310. package/src/skf-forger/scripts/pipeline-gate.py +428 -0
  311. package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
  312. package/src/skf-quick-skill/SKILL.md +54 -53
  313. package/src/skf-quick-skill/assets/skill-template.md +4 -2
  314. package/src/skf-quick-skill/customize.toml +51 -20
  315. package/src/skf-quick-skill/references/batch-mode.md +74 -21
  316. package/src/skf-quick-skill/references/compile.md +30 -31
  317. package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
  318. package/src/skf-quick-skill/references/finalize.md +69 -18
  319. package/src/skf-quick-skill/references/halt-contract.md +41 -29
  320. package/src/skf-quick-skill/references/health-check.md +5 -3
  321. package/src/skf-quick-skill/references/quick-extract.md +114 -45
  322. package/src/skf-quick-skill/references/registry-resolution.md +9 -64
  323. package/src/skf-quick-skill/references/resolve-target.md +116 -66
  324. package/src/skf-quick-skill/references/write-and-validate.md +79 -40
  325. package/src/skf-refine-architecture/SKILL.md +43 -53
  326. package/src/skf-refine-architecture/customize.toml +49 -20
  327. package/src/skf-refine-architecture/references/compile.md +97 -54
  328. package/src/skf-refine-architecture/references/exit-codes.md +30 -0
  329. package/src/skf-refine-architecture/references/finding-storage.md +21 -0
  330. package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
  331. package/src/skf-refine-architecture/references/improvements.md +14 -20
  332. package/src/skf-refine-architecture/references/init.md +98 -57
  333. package/src/skf-refine-architecture/references/issue-detection.md +32 -39
  334. package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
  335. package/src/skf-refine-architecture/references/report.md +65 -54
  336. package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
  337. package/src/skf-rename-skill/SKILL.md +19 -54
  338. package/src/skf-rename-skill/customize.toml +32 -26
  339. package/src/skf-rename-skill/references/execute.md +230 -184
  340. package/src/skf-rename-skill/references/exit-codes.md +7 -7
  341. package/src/skf-rename-skill/references/health-check.md +1 -7
  342. package/src/skf-rename-skill/references/invocation-contract.md +25 -0
  343. package/src/skf-rename-skill/references/report.md +63 -52
  344. package/src/skf-rename-skill/references/select.md +197 -115
  345. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
  346. package/src/skf-setup/SKILL.md +26 -26
  347. package/src/skf-setup/customize.toml +14 -10
  348. package/src/skf-setup/references/auto-index.md +50 -63
  349. package/src/skf-setup/references/ccc-index.md +29 -90
  350. package/src/skf-setup/references/detect-and-tier.md +58 -67
  351. package/src/skf-setup/references/health-check.md +2 -0
  352. package/src/skf-setup/references/invocation-contract.md +14 -0
  353. package/src/skf-setup/references/report.md +43 -166
  354. package/src/skf-setup/references/tier-rules.md +5 -5
  355. package/src/skf-setup/references/write-config.md +34 -57
  356. package/src/skf-test-skill/SKILL.md +17 -57
  357. package/src/skf-test-skill/assets/output-section-formats.md +33 -34
  358. package/src/skf-test-skill/customize.toml +27 -13
  359. package/src/skf-test-skill/references/coherence-check.md +134 -62
  360. package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
  361. package/src/skf-test-skill/references/coverage-check.md +209 -264
  362. package/src/skf-test-skill/references/detect-mode.md +5 -5
  363. package/src/skf-test-skill/references/external-validators.md +94 -82
  364. package/src/skf-test-skill/references/health-check.md +6 -7
  365. package/src/skf-test-skill/references/init.md +164 -91
  366. package/src/skf-test-skill/references/invocation-contract.md +66 -0
  367. package/src/skf-test-skill/references/migration-section-rules.md +5 -2
  368. package/src/skf-test-skill/references/report.md +164 -142
  369. package/src/skf-test-skill/references/score.md +80 -107
  370. package/src/skf-test-skill/references/scoring-rules.md +42 -155
  371. package/src/skf-test-skill/references/source-access-protocol.md +18 -94
  372. package/src/skf-test-skill/references/step-hard-gate.md +51 -33
  373. package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
  374. package/src/skf-test-skill/scripts/build-result-context.py +380 -0
  375. package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
  376. package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
  377. package/src/skf-test-skill/scripts/compute-score.py +223 -45
  378. package/src/skf-test-skill/scripts/gap-ledger.py +1666 -0
  379. package/src/skf-test-skill/scripts/hard-gate.py +203 -0
  380. package/src/skf-test-skill/scripts/load-coverage-inputs.py +1198 -0
  381. package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
  382. package/src/skf-test-skill/scripts/reconcile-coverage.py +547 -79
  383. package/src/skf-test-skill/scripts/score-signatures.py +479 -0
  384. package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
  385. package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
  386. package/src/skf-test-skill/scripts/verify-declared-numerator.py +133 -56
  387. package/src/skf-test-skill/templates/test-report-template.md +21 -15
  388. package/src/skf-update-skill/SKILL.md +38 -25
  389. package/src/skf-update-skill/customize.toml +27 -12
  390. package/src/skf-update-skill/references/detect-changes.md +253 -223
  391. package/src/skf-update-skill/references/gap-driven.md +336 -0
  392. package/src/skf-update-skill/references/health-check.md +31 -5
  393. package/src/skf-update-skill/references/init.md +254 -84
  394. package/src/skf-update-skill/references/invocation-contract.md +12 -0
  395. package/src/skf-update-skill/references/manual-section-rules.md +6 -23
  396. package/src/skf-update-skill/references/merge.md +182 -49
  397. package/src/skf-update-skill/references/re-extract.md +53 -262
  398. package/src/skf-update-skill/references/report.md +102 -35
  399. package/src/skf-update-skill/references/write.md +357 -155
  400. package/src/skf-update-skill/scripts/skf-new-file-diff.py +183 -46
  401. package/src/skf-verify-stack/SKILL.md +23 -42
  402. package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
  403. package/src/skf-verify-stack/customize.toml +55 -22
  404. package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
  405. package/src/skf-verify-stack/references/coverage.md +67 -64
  406. package/src/skf-verify-stack/references/exit-codes.md +21 -8
  407. package/src/skf-verify-stack/references/init.md +107 -62
  408. package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
  409. package/src/skf-verify-stack/references/integrations.md +106 -65
  410. package/src/skf-verify-stack/references/report.md +59 -42
  411. package/src/skf-verify-stack/references/requirements.md +32 -9
  412. package/src/skf-verify-stack/references/synthesize.md +53 -39
  413. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
  414. package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
  415. package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
  416. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
  417. package/tools/cli/commands/install.js +11 -4
  418. package/tools/cli/commands/status.js +15 -3
  419. package/tools/cli/commands/update.js +11 -4
  420. package/tools/cli/lib/tool-check.js +320 -0
  421. package/tools/cli/lib/version-check.js +22 -9
  422. package/tools/cli/skf-cli.js +4 -1
  423. package/.gitattributes +0 -16
  424. package/.nvmrc +0 -1
  425. package/docs/_internal/RELEASING.md +0 -679
  426. package/release-audits/v1.0.0-launch-audit.md +0 -728
  427. package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
  428. package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
  429. package/src/skf-campaign/manifest.yaml +0 -15
  430. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
  431. package/src/skf-create-skill/references/ecosystem-check.md +0 -74
  432. package/src/skf-drop-skill/references/headless-contract.md +0 -18
  433. package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
  434. package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
  435. package/src/skf-update-skill/references/validate.md +0 -124
  436. package/tools/validate-docs-drift.js +0 -233
  437. package/tools/validate-docs-links.js +0 -665
@@ -1,55 +1,105 @@
1
1
  # /// script
2
- # requires-python = ">=3.9"
2
+ # requires-python = ">=3.11"
3
3
  # dependencies = ["pyyaml"]
4
4
  # ///
5
- """Campaign Render Batch — build the QS `--batch` input file for Tier B skills.
5
+ """Campaign Render Batch: build the QS `--batch` input file for Tier B skills.
6
6
 
7
7
  step-06 batches every Tier B skill through `skf-quick-skill --batch`. The batch
8
8
  input file is a machine-consumed cross-tool contract: one target per line in the
9
- exact single-target shape `skf-quick-skill` parses (see
10
- src/skf-quick-skill/references/batch-mode.md). Hand-building that file in prose
11
- is filter + join + reformat-structured-data — the same mechanical, all-or-nothing
12
- work campaign-parse-manifest.py owns for the inverse direction, where a silent
13
- format slip corrupts the whole run. This script is the single source of truth for
14
- the generation side so the line format lives in one place, aligned with the
15
- consumer's contract, instead of being re-derived in the step prose.
16
-
17
- What it does:
18
- - Filters `skills[]` for `tier == "B" && status == "pending"` (skips Tier A and
19
- any already completed/failed/skipped — resume-safe).
9
+ exact single-target shape `skf-quick-skill` parses (parse_line in the shared
10
+ skf-quick-batch.py, documented in quick-skill's references/batch-mode.md).
11
+ Hand-building that file in prose is filter + join + reformat-structured-data,
12
+ the same mechanical, all-or-nothing work campaign-parse-manifest.py owns for the
13
+ inverse direction, where a silent format slip corrupts the whole run. This
14
+ script is the single source of truth for
15
+ the generation side, and for the way back: it is the only place step-06 selects
16
+ Tier B skills and the only place it joins QS results to them.
17
+
18
+ What it does (render mode):
19
+ - Selects `skills[]` with `tier == "B"` and status `pending` or `active` (an
20
+ `active` skill is one an interrupted batch left behind; completed, failed
21
+ and skipped skills are left alone, so a resume is safe).
22
+ - Leaves out each selected skill the directive's `## Skip List` names (read
23
+ through campaign-quality-gate.py) and reports it in `skipped_by_directive`.
20
24
  - Looks up each skill's `repo_url` from the brief's `targets[]` (matched by
21
25
  name); repo URLs live only in the brief, never in the state schema.
22
26
  - Emits one line per skill in the CONSUMER's single-target shape:
23
- {repo_url} (pin is null → latest)
24
- {repo_url}@{pin} (skills[].pin is non-null)
27
+ {repo_url} (pin is null: latest)
28
+ {repo_url}@{pin} (a version pin: a digit, or `v` and a
29
+ digit, first; skf-resolve-package.py's
30
+ _VERSION_RE)
31
+ {repo_url}/tree/{pin} (any other pin, a branch such as
32
+ `main`: quick-skill's parse-target
33
+ reads it as the ref to build)
25
34
  with optional ` language=<lang>` / ` scope=<path>` modifiers appended when the
26
35
  brief target carries a `language_hint`/`language` or `scope_hint`/`scope` hint.
27
- - No skill-name field, no bare pin token — that would not parse as a target.
36
+ QS reads a modifier as one word: a hint that is empty or holds whitespace
37
+ would make it read the whole line as one target, so it is left out and
38
+ reported in `dropped_hints`, and the first word of every line is the
39
+ target QS records for it.
40
+ - No skill-name field, no bare pin token: that would not parse as a target.
41
+ - With --map, writes the line-to-skill map: line n of the batch file is QS's
42
+ batch number n, so two skills whose lines read the same stay apart.
43
+
44
+ Record mode (--record): joins the QS batch summary (`summary_path` of the
45
+ `batch_summary` event) to the map by batch number. Each mapped skill gets
46
+ `completed` (QS status success, `skill_path` from `skill_package`, its
47
+ `quality_score`) or `failed` (with QS's `error_code`, or `no-batch-result`
48
+ when the summary holds no result for its line). A result whose target is not
49
+ the mapped line's target means the summary belongs to another batch file, and
50
+ nothing is joined.
28
51
 
29
52
  CLI:
30
- uv run campaign-render-batch.py --state-file <p> --brief-file <p> [-o <batchFile>]
53
+ uv run campaign-render-batch.py --state-file <p> --brief-file <p> \
54
+ [-o <batchFile>] [--map <mapFile>] [--directive-file <p>]
55
+ uv run campaign-render-batch.py --record <summary_path> --map <mapFile>
31
56
 
32
57
  Output:
33
- - Batch text (one target per line) to {batchFile} via -o, else to stdout.
34
- - JSON summary {written, count, skipped_non_tierB, skipped_non_pending} to stderr.
58
+ render: batch text (one target per line) to {batchFile} via -o, else to
59
+ stdout; the map JSON {"batch_file", "lines": [{"batch", "skill",
60
+ "target", "line"}], "skipped_by_directive": [{"name", "reason"}]} to
61
+ --map; a JSON summary {written, count, skipped_non_tierB,
62
+ skipped_non_pending, skills, skipped_by_directive, dropped_hints:
63
+ [{"skill", "hint", "value"}]} to stderr.
64
+ record: {"results": [{"skill", "batch", "status", "quality_score",
65
+ "skill_path", "exit_code", "error_code"}], "counts": {"completed",
66
+ "failed"}} on stdout.
35
67
 
36
68
  Exit codes:
37
- 0 batch file written (0+ targets)
38
- 2 state file missing / unreadable / bad YAML
39
- 8 brief missing / unreadable (missing-brief HALT), or a pending Tier B skill
69
+ 0 batch file written (0+ targets), or results joined
70
+ 2 state file missing / unreadable / bad YAML, a directive that cannot be
71
+ read, a missing or unreadable map or summary, or a summary that does not
72
+ match the map (SUMMARY_MISMATCH)
73
+ 8 brief missing / unreadable (missing-brief HALT), or a selected Tier B skill
40
74
  has no matching brief target with a repo_url (unmatched-target)
41
75
  """
42
76
 
43
77
  from __future__ import annotations
44
78
 
45
79
  import argparse
80
+ import importlib.util
46
81
  import json
47
82
  import sys
48
83
  from pathlib import Path
49
- from typing import Any, Dict, List, Tuple
84
+ from typing import Any, Dict, List, Optional, Tuple
50
85
 
51
86
  import yaml
52
87
 
88
+ GATE_SCRIPT = Path(__file__).resolve().with_name("campaign-quality-gate.py")
89
+ RESOLVE_PACKAGE_SCRIPT = Path(__file__).resolve().parent.parent.parent / "shared" / "scripts" / "skf-resolve-package.py"
90
+ SELECTED_STATUSES = ("pending", "active")
91
+
92
+
93
+ def _version_re() -> Any:
94
+ """skf-resolve-package.py's _VERSION_RE: the pins parse-target reads after `@`."""
95
+ spec = importlib.util.spec_from_file_location("skf_resolve_package", RESOLVE_PACKAGE_SCRIPT)
96
+ module = importlib.util.module_from_spec(spec)
97
+ spec.loader.exec_module(module)
98
+ return module._VERSION_RE
99
+
100
+
101
+ VERSION_RE = _version_re()
102
+
53
103
 
54
104
  def _err(message: str, code: str, exit_code: int, **extra: Any) -> int:
55
105
  payload: Dict[str, Any] = {"error": message, "code": code}
@@ -59,27 +109,58 @@ def _err(message: str, code: str, exit_code: int, **extra: Any) -> int:
59
109
  return exit_code
60
110
 
61
111
 
62
- def _target_line(repo_url: str, pin: Any, target: Dict[str, Any]) -> str:
63
- """Render one batch line in the consumer's single-target shape."""
112
+ def _target_line(repo_url: str, pin: Any, target: Dict[str, Any]) -> Tuple[str, List[Dict[str, Any]]]:
113
+ """Render one batch line in the consumer's single-target shape.
114
+
115
+ Returns (line, dropped): each hint QS could not read as one modifier word
116
+ (empty, or holding whitespace) is left off the line and returned instead.
117
+ """
64
118
  line = repo_url
65
- if pin:
119
+ if pin and VERSION_RE.match(str(pin)):
66
120
  line += f"@{pin}"
67
- lang = target.get("language_hint") or target.get("language")
68
- scope = target.get("scope_hint") or target.get("scope")
69
- if lang:
70
- line += f" language={lang}"
71
- if scope:
72
- line += f" scope={scope}"
73
- return line
74
-
121
+ elif pin:
122
+ # parse-target reads `@main` as no target at all; /tree/<branch> is a ref.
123
+ line += f"/tree/{pin}"
124
+ dropped: List[Dict[str, Any]] = []
125
+ for modifier, fields in (("language", ("language_hint", "language")), ("scope", ("scope_hint", "scope"))):
126
+ field = next((f for f in fields if target.get(f) not in (None, "")), None)
127
+ if field is None:
128
+ continue
129
+ value = str(target[field]).strip()
130
+ if not value or any(c.isspace() for c in value):
131
+ dropped.append({"hint": field, "value": target[field]})
132
+ continue
133
+ line += f" {modifier}={value}"
134
+ return line, dropped
75
135
 
76
- def build_batch(state: Dict[str, Any], brief: Dict[str, Any]) -> Tuple[List[str], Dict[str, Any]]:
77
- """Filter Tier B pending skills and render batch lines.
78
136
 
79
- Returns (lines, summary). `summary["unmatched"]` lists any pending Tier B
80
- skill names with no brief target / repo_url; the caller HALTs (exit 8) when
81
- it is non-empty.
137
+ def _skip_list(directive_file: Optional[str]) -> Dict[str, Optional[str]]:
138
+ """The directive's Skip List, parsed by campaign-quality-gate.py (one parser)."""
139
+ if not directive_file:
140
+ return {}
141
+ spec = importlib.util.spec_from_file_location("campaign_quality_gate", GATE_SCRIPT)
142
+ gate = importlib.util.module_from_spec(spec)
143
+ spec.loader.exec_module(gate)
144
+ try:
145
+ return gate.read_directive(directive_file)["skip"]
146
+ except gate.GateError as exc:
147
+ raise OSError(str(exc)) from exc
148
+
149
+
150
+ def build_batch(
151
+ state: Dict[str, Any],
152
+ brief: Dict[str, Any],
153
+ skip: Optional[Dict[str, Optional[str]]] = None,
154
+ ) -> Tuple[List[str], Dict[str, Any]]:
155
+ """Select Tier B pending and interrupted skills and render batch lines.
156
+
157
+ Returns (lines, summary). `summary["skills"]` names the skill of each line,
158
+ in order; `summary["skipped_by_directive"]` lists the selected skills the
159
+ Skip List names; `summary["unmatched"]` lists any selected Tier B skill
160
+ with no brief target / repo_url; the caller HALTs (exit 8) when it is
161
+ non-empty.
82
162
  """
163
+ skip = skip or {}
83
164
  skills = state.get("skills") or []
84
165
  targets: Dict[str, Dict[str, Any]] = {
85
166
  t.get("name"): t
@@ -88,7 +169,10 @@ def build_batch(state: Dict[str, Any], brief: Dict[str, Any]) -> Tuple[List[str]
88
169
  }
89
170
 
90
171
  lines: List[str] = []
172
+ names: List[str] = []
91
173
  unmatched: List[str] = []
174
+ skipped_by_directive: List[Dict[str, Any]] = []
175
+ dropped_hints: List[Dict[str, Any]] = []
92
176
  skipped_non_tierb = 0
93
177
  skipped_non_pending = 0
94
178
 
@@ -98,28 +182,94 @@ def build_batch(state: Dict[str, Any], brief: Dict[str, Any]) -> Tuple[List[str]
98
182
  if skill.get("tier") != "B":
99
183
  skipped_non_tierb += 1
100
184
  continue
101
- if skill.get("status") != "pending":
185
+ if skill.get("status") not in SELECTED_STATUSES:
102
186
  skipped_non_pending += 1
103
187
  continue
104
188
 
105
189
  name = skill.get("name")
190
+ if name in skip:
191
+ skipped_by_directive.append({"name": name, "reason": skip[name]})
192
+ continue
106
193
  target = targets.get(name)
107
194
  repo_url = (target or {}).get("repo_url") or ""
108
195
  if not repo_url:
109
196
  unmatched.append(name)
110
197
  continue
111
198
 
112
- lines.append(_target_line(repo_url, skill.get("pin"), target))
199
+ line, dropped = _target_line(repo_url, skill.get("pin"), target)
200
+ lines.append(line)
201
+ names.append(name)
202
+ dropped_hints.extend({"skill": name, **d} for d in dropped)
113
203
 
114
204
  summary = {
115
205
  "count": len(lines),
116
206
  "skipped_non_tierB": skipped_non_tierb,
117
207
  "skipped_non_pending": skipped_non_pending,
208
+ "skills": names,
209
+ "skipped_by_directive": skipped_by_directive,
210
+ "dropped_hints": dropped_hints,
118
211
  "unmatched": unmatched,
119
212
  }
120
213
  return lines, summary
121
214
 
122
215
 
216
+ def build_map(lines: List[str], summary: Dict[str, Any], batch_file: str) -> Dict[str, Any]:
217
+ """The line-to-skill map: QS numbers the batch file's targets from 1, in line order.
218
+
219
+ A line's target is its first word, the target QS records for it, because
220
+ every word after it is a modifier QS reads as one (see _target_line).
221
+ """
222
+ return {
223
+ "batch_file": batch_file,
224
+ "lines": [
225
+ {"batch": n, "skill": name, "target": line.split()[0], "line": line}
226
+ for n, (line, name) in enumerate(zip(lines, summary["skills"]), start=1)
227
+ ],
228
+ "skipped_by_directive": summary["skipped_by_directive"],
229
+ }
230
+
231
+
232
+ def join_results(batch_map: Dict[str, Any], batch_summary: Dict[str, Any]) -> Dict[str, Any]:
233
+ """Join a QS batch summary's results[] to the mapped skills by batch number.
234
+
235
+ Raises ValueError when a result's target is not its mapped line's target.
236
+ """
237
+ by_batch = {
238
+ r.get("batch"): r
239
+ for r in (batch_summary.get("results") or [])
240
+ if isinstance(r, dict)
241
+ }
242
+ results: List[Dict[str, Any]] = []
243
+ for entry in batch_map.get("lines") or []:
244
+ result = by_batch.get(entry["batch"])
245
+ if result is not None and result.get("target") != entry["target"]:
246
+ raise ValueError(
247
+ f"batch {entry['batch']} is `{result.get('target')}` in the summary but "
248
+ f"`{entry['target']}` in the map: the summary belongs to another batch file"
249
+ )
250
+ row: Dict[str, Any] = {
251
+ "skill": entry["skill"],
252
+ "batch": entry["batch"],
253
+ "status": "failed",
254
+ "quality_score": None,
255
+ "skill_path": None,
256
+ "exit_code": None,
257
+ "error_code": "no-batch-result",
258
+ }
259
+ if result is not None:
260
+ ok = result.get("status") == "success"
261
+ row.update(
262
+ status="completed" if ok else "failed",
263
+ quality_score=result.get("quality_score") if ok else None,
264
+ skill_path=result.get("skill_package") if ok else None,
265
+ exit_code=result.get("exit_code"),
266
+ error_code=None if ok else result.get("error_code"),
267
+ )
268
+ results.append(row)
269
+ counts = {key: sum(1 for r in results if r["status"] == key) for key in ("completed", "failed")}
270
+ return {"results": results, "counts": counts}
271
+
272
+
123
273
  def _load_yaml_mapping(path: str) -> Dict[str, Any]:
124
274
  data = yaml.safe_load(Path(path).read_text(encoding="utf-8"))
125
275
  if data is None:
@@ -129,7 +279,13 @@ def _load_yaml_mapping(path: str) -> Dict[str, Any]:
129
279
  return data
130
280
 
131
281
 
132
- def run(state_file: str, brief_file: str, output: str | None) -> int:
282
+ def run(
283
+ state_file: str,
284
+ brief_file: str,
285
+ output: str | None,
286
+ map_file: str | None = None,
287
+ directive_file: str | None = None,
288
+ ) -> int:
133
289
  # State problems are plain file/parse errors (exit 2).
134
290
  if not Path(state_file).is_file():
135
291
  return _err(f"State file not found: {state_file}", "STATE_NOT_FOUND", 2)
@@ -138,7 +294,7 @@ def run(state_file: str, brief_file: str, output: str | None) -> int:
138
294
  except (yaml.YAMLError, ValueError) as exc:
139
295
  return _err(f"Failed to parse state YAML: {exc}", "STATE_PARSE_ERROR", 2)
140
296
 
141
- # Brief problems preserve step-06 §4's missing-brief HALT (exit 8): repo_urls
297
+ # Brief problems preserve step-06's missing-brief HALT (exit 8): repo_urls
142
298
  # live only in the brief, so a missing/unreadable/corrupt brief is fatal here.
143
299
  if not Path(brief_file).is_file():
144
300
  return _err(f"Brief file not found: {brief_file}", "missing-brief", 8)
@@ -149,12 +305,17 @@ def run(state_file: str, brief_file: str, output: str | None) -> int:
149
305
  except (yaml.YAMLError, ValueError) as exc:
150
306
  return _err(f"Brief unparseable: {exc}", "missing-brief", 8)
151
307
 
152
- lines, summary = build_batch(state, brief)
308
+ try:
309
+ skip = _skip_list(directive_file)
310
+ except OSError as exc:
311
+ return _err(str(exc), "DIRECTIVE_UNREADABLE", 2)
312
+
313
+ lines, summary = build_batch(state, brief, skip)
153
314
 
154
315
  unmatched = summary.pop("unmatched")
155
316
  if unmatched:
156
317
  return _err(
157
- "No matching brief target (repo_url) for pending Tier B skill(s): "
318
+ "No matching brief target (repo_url) for selected Tier B skill(s): "
158
319
  + ", ".join(str(n) for n in unmatched),
159
320
  "unmatched-target",
160
321
  8,
@@ -177,22 +338,69 @@ def run(state_file: str, brief_file: str, output: str | None) -> int:
177
338
  "count": summary["count"],
178
339
  "skipped_non_tierB": summary["skipped_non_tierB"],
179
340
  "skipped_non_pending": summary["skipped_non_pending"],
341
+ "skills": summary["skills"],
342
+ "skipped_by_directive": summary["skipped_by_directive"],
343
+ "dropped_hints": summary["dropped_hints"],
180
344
  }
345
+ if map_file:
346
+ Path(map_file).write_text(
347
+ json.dumps(build_map(lines, summary, summary["written"]), indent=2) + "\n",
348
+ encoding="utf-8",
349
+ )
350
+ ordered["map"] = map_file
181
351
  json.dump(ordered, sys.stderr)
182
352
  sys.stderr.write("\n")
183
353
  return 0
184
354
 
185
355
 
356
+ def _load_json(path: str, label: str) -> Tuple[Optional[Dict[str, Any]], int]:
357
+ if not Path(path).is_file():
358
+ return None, _err(f"{label.capitalize()} file not found: {path}", f"{label.upper()}_NOT_FOUND", 2)
359
+ try:
360
+ data = json.loads(Path(path).read_text(encoding="utf-8"))
361
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
362
+ return None, _err(f"{label.capitalize()} unreadable: {exc}", f"{label.upper()}_PARSE_ERROR", 2)
363
+ if not isinstance(data, dict):
364
+ return None, _err(f"{label.capitalize()} is not a JSON object: {path}", f"{label.upper()}_PARSE_ERROR", 2)
365
+ return data, 0
366
+
367
+
368
+ def record(summary_file: str, map_file: str) -> int:
369
+ batch_map, rc = _load_json(map_file, "map")
370
+ if batch_map is None:
371
+ return rc
372
+ batch_summary, rc = _load_json(summary_file, "summary")
373
+ if batch_summary is None:
374
+ return rc
375
+ try:
376
+ joined = join_results(batch_map, batch_summary)
377
+ except ValueError as exc:
378
+ return _err(str(exc), "SUMMARY_MISMATCH", 2)
379
+ json.dump(joined, sys.stdout, separators=(",", ":"))
380
+ sys.stdout.write("\n")
381
+ return 0
382
+
383
+
186
384
  def main(argv: list[str] | None = None) -> int:
187
385
  parser = argparse.ArgumentParser(
188
386
  prog="campaign-render-batch",
189
- description="Build the QS --batch input file for pending Tier B skills.",
387
+ description="Build the QS --batch input file for Tier B skills, or join a QS "
388
+ "batch summary back to them with --record.",
190
389
  )
191
- parser.add_argument("--state-file", required=True)
192
- parser.add_argument("--brief-file", required=True)
390
+ parser.add_argument("--state-file")
391
+ parser.add_argument("--brief-file")
193
392
  parser.add_argument("-o", "--output", dest="output", help="batch file path (default: stdout)")
393
+ parser.add_argument("--map", dest="map_file", help="line-to-skill map path (written, or read by --record)")
394
+ parser.add_argument("--directive-file", help="campaign directive whose Skip List is applied")
395
+ parser.add_argument("--record", dest="summary_file", help="QS batch summary to join to --map")
194
396
  args = parser.parse_args(argv)
195
- return run(args.state_file, args.brief_file, args.output)
397
+ if args.summary_file:
398
+ if not args.map_file:
399
+ parser.error("--record needs --map")
400
+ return record(args.summary_file, args.map_file)
401
+ if not args.state_file or not args.brief_file:
402
+ parser.error("--state-file and --brief-file are required to build the batch file")
403
+ return run(args.state_file, args.brief_file, args.output, args.map_file, args.directive_file)
196
404
 
197
405
 
198
406
  if __name__ == "__main__":