bmad-module-skill-forge 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (435) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +51 -49
  3. package/docs/404.md +10 -8
  4. package/docs/_data/pinned.yaml +22 -13
  5. package/docs/_internal/STABILITY.md +58 -35
  6. package/docs/agents.md +39 -53
  7. package/docs/architecture.md +65 -51
  8. package/docs/bmad-synergy.md +53 -51
  9. package/docs/campaign.md +127 -75
  10. package/docs/concepts.md +34 -28
  11. package/docs/examples.md +70 -68
  12. package/docs/forge-auto.md +34 -31
  13. package/docs/getting-started.md +118 -57
  14. package/docs/how-it-works.md +25 -21
  15. package/docs/index.md +15 -15
  16. package/docs/skill-model.md +70 -59
  17. package/docs/troubleshooting.md +135 -18
  18. package/docs/verifying-a-skill.md +83 -57
  19. package/docs/why-skf.md +24 -22
  20. package/docs/workflows.md +274 -110
  21. package/package.json +18 -10
  22. package/src/forger/preferences.yaml +8 -1
  23. package/src/knowledge/agentskills-spec.md +2 -0
  24. package/src/knowledge/ccc-bridge.md +40 -24
  25. package/src/knowledge/confidence-tiers.md +9 -5
  26. package/src/knowledge/overview.md +3 -3
  27. package/src/knowledge/progressive-capability.md +8 -8
  28. package/src/knowledge/provenance-tracking.md +9 -5
  29. package/src/knowledge/qmd-registry.md +2 -2
  30. package/src/knowledge/skf-knowledge-index.csv +2 -2
  31. package/src/knowledge/skill-lifecycle.md +18 -11
  32. package/src/knowledge/split-body-strategy.md +0 -3
  33. package/src/knowledge/tool-resolution.md +4 -2
  34. package/src/knowledge/version-paths.md +101 -15
  35. package/src/module-help.csv +3 -3
  36. package/src/module.yaml +10 -0
  37. package/src/shared/data/ast-grep-recipes.yaml +608 -0
  38. package/src/shared/data/ide-context-files.json +30 -0
  39. package/src/shared/health-check.md +32 -12
  40. package/src/shared/references/description-guard-protocol.md +14 -9
  41. package/src/shared/references/feasibility-report-schema.md +19 -13
  42. package/src/shared/references/headless-gate-convention.md +55 -3
  43. package/src/shared/references/output-contract-schema.md +19 -8
  44. package/src/shared/references/pipeline-contracts.md +94 -29
  45. package/src/shared/references/tessl-review.md +77 -0
  46. package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
  47. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
  48. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
  49. package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
  50. package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
  51. package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
  52. package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
  53. package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
  54. package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
  55. package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
  56. package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
  57. package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
  58. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
  59. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
  60. package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
  61. package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
  62. package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
  63. package/src/shared/scripts/skf-atomic-write.py +30 -14
  64. package/src/shared/scripts/skf-build-change-manifest.py +2203 -24
  65. package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
  66. package/src/shared/scripts/skf-check-unit-records.py +184 -0
  67. package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
  68. package/src/shared/scripts/skf-classify-changed-files.py +694 -0
  69. package/src/shared/scripts/skf-comention-pairs.py +1333 -123
  70. package/src/shared/scripts/skf-compare-file-hashes.py +1 -1
  71. package/src/shared/scripts/skf-count-imports.py +1867 -0
  72. package/src/shared/scripts/skf-count-tokens.py +1 -1
  73. package/src/shared/scripts/skf-derive-assembly-shape.py +3 -2
  74. package/src/shared/scripts/skf-description-guard.py +117 -9
  75. package/src/shared/scripts/skf-detect-docs.py +892 -4
  76. package/src/shared/scripts/skf-detect-language.py +486 -107
  77. package/src/shared/scripts/skf-detect-registry.py +887 -0
  78. package/src/shared/scripts/skf-detect-scripts-assets.py +1 -1
  79. package/src/shared/scripts/skf-detect-tools.py +215 -27
  80. package/src/shared/scripts/skf-detect-workspaces.py +506 -30
  81. package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
  82. package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
  83. package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
  84. package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
  85. package/src/shared/scripts/skf-extract-public-api.py +4084 -85
  86. package/src/shared/scripts/skf-extraction-inventory.py +746 -0
  87. package/src/shared/scripts/skf-extraction-snapshot.py +539 -0
  88. package/src/shared/scripts/skf-fetch-temporal.py +674 -0
  89. package/src/shared/scripts/skf-find-cycles.py +97 -29
  90. package/src/shared/scripts/skf-find-test-report.py +476 -0
  91. package/src/shared/scripts/skf-forge-tier-rw.py +444 -203
  92. package/src/shared/scripts/skf-github-fetch.py +365 -0
  93. package/src/shared/scripts/skf-github-probe.py +661 -0
  94. package/src/shared/scripts/skf-hash-content.py +166 -12
  95. package/src/shared/scripts/skf-language-corpora.py +1 -1
  96. package/src/shared/scripts/skf-load-provenance.py +159 -27
  97. package/src/shared/scripts/skf-manifest-ops.py +1 -1
  98. package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
  99. package/src/shared/scripts/skf-merge-doc-urls.py +1 -1
  100. package/src/shared/scripts/skf-names-present.py +221 -0
  101. package/src/shared/scripts/skf-pair-intersect.py +119 -28
  102. package/src/shared/scripts/skf-parse-gaps.py +1384 -0
  103. package/src/shared/scripts/skf-preapply.py +1 -1
  104. package/src/shared/scripts/skf-preflight.py +161 -27
  105. package/src/shared/scripts/skf-promote-staged.py +430 -0
  106. package/src/shared/scripts/skf-provenance-gap-dispatch.py +39 -7
  107. package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
  108. package/src/shared/scripts/skf-quick-batch.py +774 -0
  109. package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
  110. package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
  111. package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
  112. package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
  113. package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
  114. package/src/shared/scripts/skf-resolve-authoritative-files.py +128 -13
  115. package/src/shared/scripts/skf-resolve-package.py +523 -106
  116. package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
  117. package/src/shared/scripts/skf-run-lock.py +577 -0
  118. package/src/shared/scripts/skf-scan-manifests.py +998 -57
  119. package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
  120. package/src/shared/scripts/skf-severity-classify.py +712 -108
  121. package/src/shared/scripts/skf-shape-detect.py +298 -204
  122. package/src/shared/scripts/skf-shard-body.py +9 -8
  123. package/src/shared/scripts/skf-skill-inventory.py +2412 -89
  124. package/src/shared/scripts/skf-skills-module.py +472 -0
  125. package/src/shared/scripts/skf-source-tree.py +1796 -0
  126. package/src/shared/scripts/skf-structural-diff.py +670 -119
  127. package/src/shared/scripts/skf-tessl-review.py +706 -0
  128. package/src/shared/scripts/skf-update-active-symlink.py +5 -5
  129. package/src/shared/scripts/skf-update-run-state.py +624 -0
  130. package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
  131. package/src/shared/scripts/skf-validate-brief-schema.py +4 -1
  132. package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
  133. package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
  134. package/src/shared/scripts/skf-validate-output.py +712 -64
  135. package/src/shared/scripts/skf-validate-pins.py +1 -1
  136. package/src/shared/scripts/skf-verify-no-trace.py +116 -10
  137. package/src/shared/scripts/skf-verify-provenance-completeness.py +2919 -54
  138. package/src/shared/scripts/skf-write-skill-brief.py +367 -19
  139. package/src/shared/tool-requirements.yaml +201 -0
  140. package/src/skf-analyze-source/SKILL.md +41 -36
  141. package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
  142. package/src/skf-analyze-source/customize.toml +36 -24
  143. package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
  144. package/src/skf-analyze-source/references/continue.md +2 -13
  145. package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
  146. package/src/skf-analyze-source/references/generate-briefs.md +121 -55
  147. package/src/skf-analyze-source/references/headless-contract.md +42 -15
  148. package/src/skf-analyze-source/references/identify-units.md +96 -110
  149. package/src/skf-analyze-source/references/init.md +73 -71
  150. package/src/skf-analyze-source/references/map-and-detect.md +123 -105
  151. package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
  152. package/src/skf-analyze-source/references/recommend.md +51 -70
  153. package/src/skf-analyze-source/references/scan-project.md +60 -84
  154. package/src/skf-analyze-source/references/scan-root.md +39 -0
  155. package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
  156. package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
  157. package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
  158. package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
  159. package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
  160. package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
  161. package/src/skf-audit-skill/SKILL.md +43 -51
  162. package/src/skf-audit-skill/assets/drift-report-template.md +27 -3
  163. package/src/skf-audit-skill/customize.toml +41 -20
  164. package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
  165. package/src/skf-audit-skill/references/doc-drift.md +141 -0
  166. package/src/skf-audit-skill/references/headless-contract.md +29 -0
  167. package/src/skf-audit-skill/references/init.md +148 -210
  168. package/src/skf-audit-skill/references/re-index.md +96 -108
  169. package/src/skf-audit-skill/references/report.md +96 -33
  170. package/src/skf-audit-skill/references/semantic-diff.md +27 -18
  171. package/src/skf-audit-skill/references/severity-classify.md +79 -65
  172. package/src/skf-audit-skill/references/severity-rules.md +15 -6
  173. package/src/skf-audit-skill/references/structural-diff.md +75 -90
  174. package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
  175. package/src/skf-audit-skill/scripts/render-drift-tables.py +685 -0
  176. package/src/skf-brief-skill/SKILL.md +36 -25
  177. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  178. package/src/skf-brief-skill/assets/scope-templates.md +17 -24
  179. package/src/skf-brief-skill/assets/skill-brief-schema.md +29 -15
  180. package/src/skf-brief-skill/customize.toml +48 -20
  181. package/src/skf-brief-skill/references/analyze-target.md +142 -125
  182. package/src/skf-brief-skill/references/confirm-brief.md +10 -8
  183. package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
  184. package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
  185. package/src/skf-brief-skill/references/gather-intent.md +87 -182
  186. package/src/skf-brief-skill/references/headless-args.md +84 -16
  187. package/src/skf-brief-skill/references/health-check.md +1 -1
  188. package/src/skf-brief-skill/references/invocation-contract.md +18 -13
  189. package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
  190. package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
  191. package/src/skf-brief-skill/references/scope-definition.md +123 -47
  192. package/src/skf-brief-skill/references/step-auto-brief.md +24 -80
  193. package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
  194. package/src/skf-brief-skill/references/version-resolution.md +3 -2
  195. package/src/skf-brief-skill/references/write-brief.md +66 -57
  196. package/src/skf-campaign/SKILL.md +31 -34
  197. package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
  198. package/src/skf-campaign/customize.toml +47 -16
  199. package/src/skf-campaign/references/campaign-contracts.md +39 -29
  200. package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
  201. package/src/skf-campaign/references/health-check.md +7 -16
  202. package/src/skf-campaign/references/step-01-setup.md +46 -84
  203. package/src/skf-campaign/references/step-02-strategy.md +31 -30
  204. package/src/skf-campaign/references/step-03-pins.md +26 -18
  205. package/src/skf-campaign/references/step-04-provenance.md +24 -23
  206. package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
  207. package/src/skf-campaign/references/step-06-batch.md +41 -34
  208. package/src/skf-campaign/references/step-07-capstone.md +40 -25
  209. package/src/skf-campaign/references/step-08-verify.md +28 -29
  210. package/src/skf-campaign/references/step-09-refine.md +29 -30
  211. package/src/skf-campaign/references/step-10-export.md +48 -36
  212. package/src/skf-campaign/references/step-11-maintenance.md +31 -34
  213. package/src/skf-campaign/references/step-resume.md +37 -68
  214. package/src/skf-campaign/scripts/campaign-deps.py +57 -18
  215. package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
  216. package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
  217. package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
  218. package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
  219. package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
  220. package/src/skf-campaign/scripts/campaign-report.py +204 -37
  221. package/src/skf-campaign/scripts/campaign-state.py +1128 -0
  222. package/src/skf-campaign/scripts/campaign-status.py +10 -74
  223. package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
  224. package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
  225. package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
  226. package/src/skf-campaign/templates/campaign-report-template.md +5 -1
  227. package/src/skf-campaign/templates/kickoff-template.md +0 -14
  228. package/src/skf-create-skill/SKILL.md +27 -20
  229. package/src/skf-create-skill/assets/compile-assembly-rules.md +6 -5
  230. package/src/skf-create-skill/assets/skill-sections.md +17 -6
  231. package/src/skf-create-skill/customize.toml +20 -8
  232. package/src/skf-create-skill/references/authoritative-files-protocol.md +41 -42
  233. package/src/skf-create-skill/references/batch-mode.md +81 -0
  234. package/src/skf-create-skill/references/ccc-index-check.md +27 -0
  235. package/src/skf-create-skill/references/compile.md +107 -29
  236. package/src/skf-create-skill/references/component-extraction.md +113 -91
  237. package/src/skf-create-skill/references/enrich.md +41 -23
  238. package/src/skf-create-skill/references/entry-points-by-hand.md +20 -0
  239. package/src/skf-create-skill/references/extract.md +200 -161
  240. package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
  241. package/src/skf-create-skill/references/extraction-patterns-tracing.md +2 -2
  242. package/src/skf-create-skill/references/extraction-patterns.md +584 -196
  243. package/src/skf-create-skill/references/generate-artifacts.md +145 -93
  244. package/src/skf-create-skill/references/health-check.md +1 -1
  245. package/src/skf-create-skill/references/load-brief.md +53 -36
  246. package/src/skf-create-skill/references/relabel-rule.md +12 -0
  247. package/src/skf-create-skill/references/report.md +28 -59
  248. package/src/skf-create-skill/references/source-resolution-protocols.md +49 -155
  249. package/src/skf-create-skill/references/step-auto-shard.md +20 -93
  250. package/src/skf-create-skill/references/step-doc-rot.md +44 -60
  251. package/src/skf-create-skill/references/step-doc-sources.md +55 -18
  252. package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
  253. package/src/skf-create-skill/references/sub/fetch-docs.md +106 -57
  254. package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
  255. package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
  256. package/src/skf-create-skill/references/validate.md +173 -99
  257. package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
  258. package/src/skf-create-stack-skill/SKILL.md +29 -56
  259. package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
  260. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
  261. package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
  262. package/src/skf-create-stack-skill/customize.toml +23 -12
  263. package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
  264. package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
  265. package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
  266. package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
  267. package/src/skf-create-stack-skill/references/generate-output.md +138 -128
  268. package/src/skf-create-stack-skill/references/init.md +90 -32
  269. package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
  270. package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
  271. package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
  272. package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
  273. package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
  274. package/src/skf-create-stack-skill/references/report.md +39 -33
  275. package/src/skf-create-stack-skill/references/validate.md +20 -39
  276. package/src/skf-drop-skill/SKILL.md +28 -51
  277. package/src/skf-drop-skill/customize.toml +44 -30
  278. package/src/skf-drop-skill/references/execute.md +79 -146
  279. package/src/skf-drop-skill/references/invocation-contract.md +64 -0
  280. package/src/skf-drop-skill/references/report.md +46 -30
  281. package/src/skf-drop-skill/references/select.md +118 -124
  282. package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
  283. package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
  284. package/src/skf-export-skill/SKILL.md +40 -62
  285. package/src/skf-export-skill/assets/managed-section-format.md +14 -50
  286. package/src/skf-export-skill/assets/snippet-format.md +3 -1
  287. package/src/skf-export-skill/customize.toml +42 -13
  288. package/src/skf-export-skill/references/generate-snippet.md +54 -46
  289. package/src/skf-export-skill/references/health-check.md +2 -0
  290. package/src/skf-export-skill/references/invocation-contract.md +15 -0
  291. package/src/skf-export-skill/references/load-skill.md +88 -62
  292. package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
  293. package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
  294. package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
  295. package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
  296. package/src/skf-export-skill/references/package.md +15 -73
  297. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
  298. package/src/skf-export-skill/references/result-envelope.md +31 -16
  299. package/src/skf-export-skill/references/summary.md +41 -17
  300. package/src/skf-export-skill/references/token-report.md +4 -12
  301. package/src/skf-export-skill/references/update-context.md +118 -237
  302. package/src/skf-forger/SKILL.md +61 -37
  303. package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
  304. package/src/skf-forger/customize.toml +43 -0
  305. package/src/skf-forger/references/pipeline-mode.md +73 -18
  306. package/src/skf-forger/scripts/forge-status.py +299 -0
  307. package/src/skf-forger/scripts/parse-pipeline.py +322 -54
  308. package/src/skf-forger/scripts/pipeline-gate.py +428 -0
  309. package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
  310. package/src/skf-quick-skill/SKILL.md +54 -53
  311. package/src/skf-quick-skill/assets/skill-template.md +4 -2
  312. package/src/skf-quick-skill/customize.toml +51 -20
  313. package/src/skf-quick-skill/references/batch-mode.md +74 -21
  314. package/src/skf-quick-skill/references/compile.md +30 -31
  315. package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
  316. package/src/skf-quick-skill/references/finalize.md +69 -18
  317. package/src/skf-quick-skill/references/halt-contract.md +41 -29
  318. package/src/skf-quick-skill/references/health-check.md +5 -3
  319. package/src/skf-quick-skill/references/quick-extract.md +114 -45
  320. package/src/skf-quick-skill/references/registry-resolution.md +9 -64
  321. package/src/skf-quick-skill/references/resolve-target.md +116 -66
  322. package/src/skf-quick-skill/references/write-and-validate.md +79 -40
  323. package/src/skf-refine-architecture/SKILL.md +43 -53
  324. package/src/skf-refine-architecture/customize.toml +49 -20
  325. package/src/skf-refine-architecture/references/compile.md +97 -54
  326. package/src/skf-refine-architecture/references/exit-codes.md +30 -0
  327. package/src/skf-refine-architecture/references/finding-storage.md +21 -0
  328. package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
  329. package/src/skf-refine-architecture/references/improvements.md +14 -20
  330. package/src/skf-refine-architecture/references/init.md +98 -57
  331. package/src/skf-refine-architecture/references/issue-detection.md +32 -39
  332. package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
  333. package/src/skf-refine-architecture/references/report.md +65 -54
  334. package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
  335. package/src/skf-rename-skill/SKILL.md +19 -54
  336. package/src/skf-rename-skill/customize.toml +32 -26
  337. package/src/skf-rename-skill/references/execute.md +230 -184
  338. package/src/skf-rename-skill/references/exit-codes.md +7 -7
  339. package/src/skf-rename-skill/references/health-check.md +1 -7
  340. package/src/skf-rename-skill/references/invocation-contract.md +25 -0
  341. package/src/skf-rename-skill/references/report.md +63 -52
  342. package/src/skf-rename-skill/references/select.md +197 -115
  343. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
  344. package/src/skf-setup/SKILL.md +26 -26
  345. package/src/skf-setup/customize.toml +14 -10
  346. package/src/skf-setup/references/auto-index.md +50 -63
  347. package/src/skf-setup/references/ccc-index.md +29 -89
  348. package/src/skf-setup/references/detect-and-tier.md +58 -66
  349. package/src/skf-setup/references/health-check.md +2 -0
  350. package/src/skf-setup/references/invocation-contract.md +14 -0
  351. package/src/skf-setup/references/report.md +43 -166
  352. package/src/skf-setup/references/tier-rules.md +5 -5
  353. package/src/skf-setup/references/write-config.md +34 -57
  354. package/src/skf-test-skill/SKILL.md +17 -57
  355. package/src/skf-test-skill/assets/output-section-formats.md +33 -34
  356. package/src/skf-test-skill/customize.toml +27 -13
  357. package/src/skf-test-skill/references/coherence-check.md +134 -62
  358. package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
  359. package/src/skf-test-skill/references/coverage-check.md +217 -254
  360. package/src/skf-test-skill/references/detect-mode.md +5 -5
  361. package/src/skf-test-skill/references/external-validators.md +94 -82
  362. package/src/skf-test-skill/references/health-check.md +6 -7
  363. package/src/skf-test-skill/references/init.md +163 -88
  364. package/src/skf-test-skill/references/invocation-contract.md +66 -0
  365. package/src/skf-test-skill/references/migration-section-rules.md +5 -2
  366. package/src/skf-test-skill/references/report.md +162 -142
  367. package/src/skf-test-skill/references/score.md +80 -107
  368. package/src/skf-test-skill/references/scoring-rules.md +41 -155
  369. package/src/skf-test-skill/references/source-access-protocol.md +19 -92
  370. package/src/skf-test-skill/references/step-hard-gate.md +51 -33
  371. package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
  372. package/src/skf-test-skill/scripts/build-result-context.py +287 -0
  373. package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
  374. package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
  375. package/src/skf-test-skill/scripts/compute-score.py +223 -45
  376. package/src/skf-test-skill/scripts/gap-ledger.py +1381 -0
  377. package/src/skf-test-skill/scripts/hard-gate.py +203 -0
  378. package/src/skf-test-skill/scripts/load-coverage-inputs.py +760 -0
  379. package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
  380. package/src/skf-test-skill/scripts/reconcile-coverage.py +273 -79
  381. package/src/skf-test-skill/scripts/score-signatures.py +459 -0
  382. package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
  383. package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
  384. package/src/skf-test-skill/scripts/verify-declared-numerator.py +127 -52
  385. package/src/skf-test-skill/templates/test-report-template.md +21 -15
  386. package/src/skf-update-skill/SKILL.md +38 -25
  387. package/src/skf-update-skill/customize.toml +27 -12
  388. package/src/skf-update-skill/references/detect-changes.md +233 -222
  389. package/src/skf-update-skill/references/gap-driven.md +336 -0
  390. package/src/skf-update-skill/references/health-check.md +31 -5
  391. package/src/skf-update-skill/references/init.md +250 -84
  392. package/src/skf-update-skill/references/invocation-contract.md +12 -0
  393. package/src/skf-update-skill/references/manual-section-rules.md +6 -23
  394. package/src/skf-update-skill/references/merge.md +180 -47
  395. package/src/skf-update-skill/references/re-extract.md +53 -262
  396. package/src/skf-update-skill/references/report.md +100 -35
  397. package/src/skf-update-skill/references/write.md +331 -149
  398. package/src/skf-update-skill/scripts/skf-new-file-diff.py +29 -7
  399. package/src/skf-verify-stack/SKILL.md +23 -42
  400. package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
  401. package/src/skf-verify-stack/customize.toml +55 -22
  402. package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
  403. package/src/skf-verify-stack/references/coverage.md +67 -64
  404. package/src/skf-verify-stack/references/exit-codes.md +21 -8
  405. package/src/skf-verify-stack/references/init.md +107 -62
  406. package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
  407. package/src/skf-verify-stack/references/integrations.md +106 -65
  408. package/src/skf-verify-stack/references/report.md +59 -42
  409. package/src/skf-verify-stack/references/requirements.md +32 -9
  410. package/src/skf-verify-stack/references/synthesize.md +53 -39
  411. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
  412. package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
  413. package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
  414. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
  415. package/tools/cli/commands/install.js +11 -4
  416. package/tools/cli/commands/status.js +15 -3
  417. package/tools/cli/commands/update.js +11 -4
  418. package/tools/cli/lib/tool-check.js +320 -0
  419. package/tools/cli/lib/version-check.js +22 -9
  420. package/tools/cli/skf-cli.js +4 -1
  421. package/.gitattributes +0 -16
  422. package/.nvmrc +0 -1
  423. package/docs/_internal/RELEASING.md +0 -645
  424. package/release-audits/v1.0.0-launch-audit.md +0 -728
  425. package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
  426. package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
  427. package/src/skf-campaign/manifest.yaml +0 -15
  428. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
  429. package/src/skf-create-skill/references/ecosystem-check.md +0 -74
  430. package/src/skf-drop-skill/references/headless-contract.md +0 -18
  431. package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
  432. package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
  433. package/src/skf-update-skill/references/validate.md +0 -124
  434. package/tools/validate-docs-drift.js +0 -233
  435. package/tools/validate-docs-links.js +0 -665
@@ -0,0 +1,592 @@
1
+ # /// script
2
+ # requires-python = ">=3.11"
3
+ # dependencies = ["pyyaml"]
4
+ # ///
5
+ """Campaign Quality Gate: the campaign's quality bar, applied by one script.
6
+
7
+ The campaign holds a quality gate (`campaign.quality_gate` in state: `hard`,
8
+ `soft_target`, `soft_fallback`), and the directive's `## Quality Overrides`
9
+ and `## Skip List` sections adjust it while the campaign runs. Which gate a
10
+ skill gets, whether a test result settles it, and which completed skills
11
+ clear the gate each have one right answer, so they are computed here and the
12
+ step files read the result.
13
+
14
+ Precedence, highest first: the directive's Quality Overrides (a line for one
15
+ skill beats a campaign-wide line), then the gate in state, which `check`
16
+ settled at step-01 from a campaign brief's `quality_gate` and the
17
+ customize.toml values.
18
+
19
+ Subcommands:
20
+
21
+ check Settle and validate the gate step-01 writes to state: each of
22
+ `hard`, `soft_target` and `soft_fallback` from the brief's
23
+ `quality_gate` (--brief-file) when it gives one, else from the
24
+ customize.toml values passed in. `hard` must be
25
+ `zero-critical-high` (test-skill's own hard gate, the only one
26
+ there is); `soft_target` and `soft_fallback` are numbers from 0
27
+ to 100, the fallback not above the target.
28
+ resolve One Tier A skill's run at step-05: whether the directive's Skip
29
+ List names it, the threshold its test run gets (`threshold`, the
30
+ effective `soft_target`) and, with --brief-file, the inputs
31
+ brief-skill takes for it (`brief_skill`: `target_repo` from the
32
+ brief, `target_version` the skill's pin, the brief target's
33
+ language and scope hints), so no step reads them out of prose.
34
+ record Settle a Tier A skill from the SKF_TEST_RESULT_JSON line its
35
+ test run printed (stdin, or the file --result names). Only the
36
+ verdict PASS completes the skill; FAIL, INCONCLUSIVE,
37
+ pass-with-drift, an error envelope and a missing or unreadable
38
+ line fail it, with the reason. test-skill already applied the
39
+ threshold, its caps and its 80% floor fallback, so no score is
40
+ compared here.
41
+ classify Classify every completed skill against its effective gate for
42
+ the step-10 export: `pass` (score at or above soft_target),
43
+ `fallback` (at or above soft_fallback) or `fail` (below it, or
44
+ no score at all). Only pass and fallback skills export. A Tier A
45
+ score is test-skill's; a Tier B score is the skill-check score
46
+ quick-skill records, not a test-skill score. Each row's
47
+ `export_name` is the skill folder skf-export-skill resolves: the
48
+ last part of the `skill_path` the build recorded
49
+ (`{skills_output_folder}/{skill-name}/{version}/{skill-name}`),
50
+ which quick-skill names after the library for a Tier B skill,
51
+ else the campaign name.
52
+
53
+ Directive format (a section runs to the next heading of level 1 or 2; each
54
+ entry is one list item, backticks around a name are dropped):
55
+
56
+ ## Quality Overrides
57
+ - soft_target: 85 campaign-wide
58
+ - soft_fallback: 75 campaign-wide
59
+ - cognee: soft_target 80, soft_fallback 70 for one skill
60
+
61
+ ## Skip List
62
+ - cognee: upstream is mid-rewrite a reason after a colon, a dash
63
+ - legacy-lib or in parentheses is optional
64
+
65
+ A list item in these sections that does not read this way is returned in
66
+ `unparsed[]`, never dropped silently: a Quality Overrides item with any other
67
+ text, or that names a key twice, is unparsed and none of its numbers apply. A
68
+ directive path that names no file means no directive.
69
+
70
+ CLI:
71
+ uv run campaign-quality-gate.py check --hard <value> --soft-target <N> --soft-fallback <N> [--brief-file <p>]
72
+ uv run campaign-quality-gate.py resolve --state-file <p> --skill <name> [--directive-file <p>] [--brief-file <p>]
73
+ uv run campaign-quality-gate.py record --skill <name> [--result <p>] (stdin otherwise)
74
+ uv run campaign-quality-gate.py classify --state-file <p> [--directive-file <p>]
75
+
76
+ Output (one JSON object on stdout):
77
+ check: {"hard", "soft_target", "soft_fallback"}
78
+ resolve: {"skill", "skip", "skip_reason", "hard", "soft_target",
79
+ "soft_fallback", "threshold", "overrides": [...],
80
+ "unparsed": [...], "warnings": [...]}, and with --brief-file
81
+ "brief_skill": {"target_repo", "skill_name", "target_version",
82
+ "language_hint", "scope_hint"} (null when the skill has none)
83
+ record: {"skill", "status": "completed|failed", "verdict",
84
+ "quality_score", "threshold", "threshold_fallback", "reason"}
85
+ classify: {"gate": {...}, "skills": [{"name", "tier", "skill_path",
86
+ "export_name", "quality_score", "soft_target", "soft_fallback",
87
+ "verdict", "reason"}],
88
+ "export": [...], "excluded": [{"name", "tier", "quality_score",
89
+ "verdict", "reason"}], "counts": {"pass", "fallback", "fail"},
90
+ "unparsed": [...], "warnings": [...]}
91
+
92
+ Exit codes:
93
+ 0 result printed (for record, read `status`)
94
+ 2 error, as {"error", "code"} on stderr: STATE_NOT_FOUND,
95
+ STATE_PARSE_ERROR, SKILL_NOT_FOUND, DIRECTIVE_UNREADABLE,
96
+ RESULT_NOT_FOUND, BRIEF_NOT_FOUND, BRIEF_UNREADABLE (missing, or not a
97
+ YAML mapping), TARGET_NOT_FOUND (the brief has no target with a
98
+ repo_url for the skill), or INVALID_GATE (with `errors[]`: an
99
+ unsupported hard gate, a soft value outside 0 to 100, a fallback above
100
+ the target, from the brief, in state or after the directive's overrides)
101
+ """
102
+
103
+ from __future__ import annotations
104
+
105
+ import argparse
106
+ import json
107
+ import re
108
+ import sys
109
+ from pathlib import Path, PurePosixPath
110
+ from typing import Any
111
+
112
+ import yaml
113
+
114
+ HARD_GATES = ("zero-critical-high",)
115
+ SOFT_KEYS = ("soft_target", "soft_fallback")
116
+ GATE_KEYS = ("hard", *SOFT_KEYS)
117
+ # Each brief-skill hint and the brief target fields it is read from, in order.
118
+ HINT_FIELDS = {"language_hint": ("language_hint", "language"), "scope_hint": ("scope_hint", "scope")}
119
+
120
+ HEADING_RE = re.compile(r"^(#{1,6})\s+(.*?)\s*#*\s*$")
121
+ ITEM_RE = re.compile(r"^\s*(?:[-*+]|\d+[.)])\s+(.*\S)\s*$")
122
+ PAIR_RE = re.compile(r"\b(soft_target|soft_fallback)\b\s*[:=]?\s*(-?\d+(?:\.\d+)?)\s*%?", re.I)
123
+ # The whole override text: one or more PAIR_RE pairs separated by commas, nothing else.
124
+ PAIRS_RE = re.compile(
125
+ r"(?:soft_target|soft_fallback)\b\s*[:=]?\s*-?\d+(?:\.\d+)?\s*%?"
126
+ r"(?:\s*,\s*(?:soft_target|soft_fallback)\b\s*[:=]?\s*-?\d+(?:\.\d+)?\s*%?)*",
127
+ re.I,
128
+ )
129
+ SKILL_LINE_RE = re.compile(r"^([^\s:]+)\s*:\s*(.+)$")
130
+ SKIP_NAME_RE = re.compile(r"^([^\s:(]+)(.*)$")
131
+ TS_PREFIX = "SKF_TEST_RESULT_JSON:"
132
+ ANY_ENVELOPE_RE = re.compile(r"SKF_[A-Z_]+_RESULT_JSON:")
133
+
134
+
135
+ class GateError(Exception):
136
+ """A failure the CLI reports as {"error", "code"} on stderr with exit 2."""
137
+
138
+ def __init__(self, message: str, code: str, **extra: Any) -> None:
139
+ super().__init__(message)
140
+ self.code = code
141
+ self.extra = extra
142
+
143
+
144
+ def _number(text: str) -> int | float:
145
+ value = float(text)
146
+ return int(value) if value.is_integer() else value
147
+
148
+
149
+ def _plain(value: Any) -> Any:
150
+ """A whole float as an int, so a threshold of 90.0 prints as 90."""
151
+ if isinstance(value, float) and value.is_integer():
152
+ return int(value)
153
+ return value
154
+
155
+
156
+ # --------------------------------------------------------------------------
157
+ # Directive
158
+ # --------------------------------------------------------------------------
159
+
160
+
161
+ def _sections(text: str) -> dict[str, list[tuple[int, str]]]:
162
+ """The list items of each level-2 section, keyed by lower-case title."""
163
+ sections: dict[str, list[tuple[int, str]]] = {}
164
+ current: list[tuple[int, str]] | None = None
165
+ for lineno, line in enumerate(text.splitlines(), start=1):
166
+ heading = HEADING_RE.match(line)
167
+ if heading:
168
+ level = len(heading.group(1))
169
+ if level <= 2:
170
+ title = " ".join(heading.group(2).split()).lower()
171
+ current = sections.setdefault(title, []) if level == 2 else None
172
+ continue
173
+ if current is not None:
174
+ item = ITEM_RE.match(line)
175
+ if item:
176
+ current.append((lineno, item.group(1).replace("`", "").strip()))
177
+ return sections
178
+
179
+
180
+ def parse_directive(text: str) -> dict[str, Any]:
181
+ """Read the Skip List and Quality Overrides sections of a directive."""
182
+ sections = _sections(text)
183
+ skip: dict[str, str | None] = {}
184
+ campaign: dict[str, int | float] = {}
185
+ skills: dict[str, dict[str, int | float]] = {}
186
+ unparsed: list[dict[str, Any]] = []
187
+
188
+ for lineno, entry in sections.get("skip list", []):
189
+ match = SKIP_NAME_RE.match(entry)
190
+ if not match:
191
+ unparsed.append({"section": "Skip List", "line": lineno, "text": entry})
192
+ continue
193
+ rest = match.group(2).strip()
194
+ if rest.startswith("(") and rest.endswith(")"):
195
+ rest = rest[1:-1]
196
+ skip[match.group(1)] = re.sub(r"^\W+", "", rest).strip() or None
197
+
198
+ for lineno, entry in sections.get("quality overrides", []):
199
+ first = entry.split(None, 1)[0].rstrip(":=").lower()
200
+ per_skill = SKILL_LINE_RE.match(entry)
201
+ if first in SOFT_KEYS:
202
+ name, text = None, entry
203
+ elif per_skill:
204
+ name, text = per_skill.group(1), per_skill.group(2)
205
+ else:
206
+ name, text = None, ""
207
+ # Only the documented grammar applies: other text or a repeated key
208
+ # leaves the whole item unparsed, so no stray number becomes the gate.
209
+ pairs = PAIR_RE.findall(text) if PAIRS_RE.fullmatch(text) else []
210
+ if len({key.lower() for key, _ in pairs}) < len(pairs):
211
+ pairs = []
212
+ if not pairs:
213
+ unparsed.append({"section": "Quality Overrides", "line": lineno, "text": entry})
214
+ continue
215
+ target = campaign if name is None else skills.setdefault(name, {})
216
+ for key, value in pairs:
217
+ target[key.lower()] = _number(value)
218
+
219
+ return {"skip": skip, "campaign": campaign, "skills": skills, "unparsed": unparsed}
220
+
221
+
222
+ def read_directive(path: str | None) -> dict[str, Any]:
223
+ """The parsed directive, or an empty one when no path is given or no file is there."""
224
+ if not path or not Path(path).is_file():
225
+ return parse_directive("")
226
+ try:
227
+ text = Path(path).read_bytes().decode("utf-8", errors="replace").lstrip("\ufeff")
228
+ except OSError as exc:
229
+ raise GateError(f"Directive unreadable: {exc}", "DIRECTIVE_UNREADABLE") from exc
230
+ return parse_directive(text)
231
+
232
+
233
+ def _unknown_name_warnings(directive: dict[str, Any], names: set[str]) -> list[str]:
234
+ warnings = []
235
+ for name in directive["skip"]:
236
+ if name not in names:
237
+ warnings.append(f"Skip List names `{name}`, which is no skill in this campaign")
238
+ for name in directive["skills"]:
239
+ if name not in names:
240
+ warnings.append(f"Quality Overrides names `{name}`, which is no skill in this campaign")
241
+ return warnings
242
+
243
+
244
+ # --------------------------------------------------------------------------
245
+ # Gate
246
+ # --------------------------------------------------------------------------
247
+
248
+
249
+ def gate_errors(gate: dict[str, Any]) -> list[str]:
250
+ """Why a gate cannot be applied, or [] when it can."""
251
+ errors = []
252
+ hard = gate.get("hard")
253
+ if hard not in HARD_GATES:
254
+ errors.append(
255
+ f"hard gate `{hard}` is not supported: the only hard gate is `zero-critical-high`, "
256
+ "the Critical and High gap check test-skill runs on every skill"
257
+ )
258
+ in_range = []
259
+ for key in SOFT_KEYS:
260
+ value = gate.get(key)
261
+ if _is_number(value) and 0 <= value <= 100:
262
+ in_range.append(key)
263
+ else:
264
+ errors.append(f"`{key}` must be a number from 0 to 100, got {value!r}")
265
+ if len(in_range) == 2 and gate["soft_fallback"] > gate["soft_target"]:
266
+ errors.append(
267
+ f"`soft_fallback` {_plain(gate['soft_fallback'])} is above "
268
+ f"`soft_target` {_plain(gate['soft_target'])}"
269
+ )
270
+ return errors
271
+
272
+
273
+ def _is_number(value: Any) -> bool:
274
+ return isinstance(value, (int, float)) and not isinstance(value, bool)
275
+
276
+
277
+ def effective_gate(
278
+ base: dict[str, Any], directive: dict[str, Any], skill: str | None = None
279
+ ) -> tuple[dict[str, Any], list[str]]:
280
+ """The gate after the directive's overrides, and one line per override applied."""
281
+ gate = {"hard": base.get("hard"), **{key: base.get(key) for key in SOFT_KEYS}}
282
+ applied = []
283
+ for key, value in directive["campaign"].items():
284
+ gate[key] = value
285
+ applied.append(f"directive Quality Overrides: {key} {value}")
286
+ if skill is not None:
287
+ for key, value in directive["skills"].get(skill, {}).items():
288
+ gate[key] = value
289
+ applied.append(f"directive Quality Overrides for {skill}: {key} {value}")
290
+ return gate, applied
291
+
292
+
293
+ def _checked(gate: dict[str, Any], applied: list[str]) -> dict[str, Any]:
294
+ errors = gate_errors(gate)
295
+ if errors:
296
+ where = f" (after {'; '.join(applied)})" if applied else ""
297
+ raise GateError(f"The quality gate{where} cannot be applied: " + "; ".join(errors),
298
+ "INVALID_GATE", errors=errors)
299
+ return {key: _plain(value) for key, value in gate.items()}
300
+
301
+
302
+ def _load_brief(path: str) -> dict[str, Any]:
303
+ if not Path(path).is_file():
304
+ raise GateError(f"Brief not found: {path}", "BRIEF_NOT_FOUND")
305
+ try:
306
+ brief = yaml.safe_load(Path(path).read_text(encoding="utf-8-sig"))
307
+ except (OSError, UnicodeDecodeError, yaml.YAMLError) as exc:
308
+ raise GateError(f"Brief unreadable: {exc}", "BRIEF_UNREADABLE") from exc
309
+ if not isinstance(brief, dict):
310
+ raise GateError(f"Brief is not a YAML mapping: {path}", "BRIEF_UNREADABLE")
311
+ return brief
312
+
313
+
314
+ def _load_state(path: str) -> dict[str, Any]:
315
+ if not Path(path).is_file():
316
+ raise GateError(f"State file not found: {path}", "STATE_NOT_FOUND")
317
+ try:
318
+ state = yaml.safe_load(Path(path).read_text(encoding="utf-8"))
319
+ except (OSError, UnicodeDecodeError, yaml.YAMLError) as exc:
320
+ raise GateError(f"Failed to parse state file: {exc}", "STATE_PARSE_ERROR") from exc
321
+ if not isinstance(state, dict) or not isinstance(state.get("skills"), list):
322
+ raise GateError("State file is not a campaign state (no skills[] list)", "STATE_PARSE_ERROR")
323
+ campaign = state.get("campaign")
324
+ if not isinstance(campaign, dict) or not isinstance(campaign.get("quality_gate"), dict):
325
+ raise GateError("State file has no campaign.quality_gate", "INVALID_GATE",
326
+ errors=["campaign.quality_gate is missing"])
327
+ return state
328
+
329
+
330
+ # --------------------------------------------------------------------------
331
+ # Subcommands
332
+ # --------------------------------------------------------------------------
333
+
334
+
335
+ def check(hard: str, soft_target: str, soft_fallback: str, brief: dict[str, Any] | None = None) -> dict[str, Any]:
336
+ gate: dict[str, Any] = {"hard": hard}
337
+ for key, raw in (("soft_target", soft_target), ("soft_fallback", soft_fallback)):
338
+ try:
339
+ gate[key] = _number(raw)
340
+ except ValueError:
341
+ gate[key] = raw
342
+ applied = []
343
+ brief_gate = (brief or {}).get("quality_gate")
344
+ if brief_gate is not None and not isinstance(brief_gate, dict):
345
+ raise GateError("The brief's quality_gate is not a mapping", "INVALID_GATE",
346
+ errors=["the brief's quality_gate must be a mapping"])
347
+ for key in GATE_KEYS:
348
+ if (brief_gate or {}).get(key) is not None:
349
+ gate[key] = brief_gate[key]
350
+ applied.append(f"campaign brief: {key} {brief_gate[key]}")
351
+ return _checked(gate, applied)
352
+
353
+
354
+ def _hint(target: dict[str, Any], fields: tuple[str, ...]) -> str | None:
355
+ for field in fields:
356
+ value = target.get(field)
357
+ if isinstance(value, str) and value.strip():
358
+ return value.strip()
359
+ return None
360
+
361
+
362
+ def brief_skill_inputs(state: dict[str, Any], brief: dict[str, Any], skill: str) -> dict[str, Any]:
363
+ """The inputs brief-skill takes for one skill: its repository and hints from the
364
+ brief, its pin from state (the step-01 parser accepts only an X.Y.Z Tier A pin)."""
365
+ target = next(
366
+ (t for t in brief.get("targets") or [] if isinstance(t, dict) and t.get("name") == skill), None
367
+ )
368
+ repo_url = (target or {}).get("repo_url")
369
+ if not isinstance(repo_url, str) or not repo_url.strip():
370
+ raise GateError(f"The campaign brief has no target with a repo_url for skill '{skill}'",
371
+ "TARGET_NOT_FOUND")
372
+ entry = next(s for s in state["skills"] if isinstance(s, dict) and s.get("name") == skill)
373
+ pin = entry.get("pin")
374
+ return {
375
+ "target_repo": repo_url.strip(),
376
+ "skill_name": skill,
377
+ "target_version": str(pin) if pin not in (None, "") else None,
378
+ **{hint: _hint(target, fields) for hint, fields in HINT_FIELDS.items()},
379
+ }
380
+
381
+
382
+ def resolve(
383
+ state: dict[str, Any], directive: dict[str, Any], skill: str, brief: dict[str, Any] | None = None
384
+ ) -> dict[str, Any]:
385
+ names = {s.get("name") for s in state["skills"] if isinstance(s, dict)}
386
+ if skill not in names:
387
+ raise GateError(f"Skill '{skill}' not found in state", "SKILL_NOT_FOUND")
388
+ gate, applied = effective_gate(state["campaign"]["quality_gate"], directive, skill)
389
+ gate = _checked(gate, applied)
390
+ skipped = skill in directive["skip"]
391
+ extra = {} if brief is None else {"brief_skill": brief_skill_inputs(state, brief, skill)}
392
+ return {
393
+ "skill": skill,
394
+ "skip": skipped,
395
+ "skip_reason": directive["skip"][skill] if skipped else None,
396
+ **gate,
397
+ "threshold": gate["soft_target"],
398
+ "overrides": applied,
399
+ "unparsed": directive["unparsed"],
400
+ "warnings": _unknown_name_warnings(directive, names),
401
+ **extra,
402
+ }
403
+
404
+
405
+ def _envelope(text: str) -> tuple[dict[str, Any] | None, str | None]:
406
+ """The last test-skill envelope in `text`, or None and why there is none."""
407
+ text = text.lstrip("\ufeff")
408
+ lines = [line for line in text.splitlines() if TS_PREFIX in line]
409
+ if lines:
410
+ payload = lines[-1].split(TS_PREFIX, 1)[1].strip()
411
+ elif not text.strip():
412
+ return None, "no-result"
413
+ elif ANY_ENVELOPE_RE.search(text):
414
+ return None, "not-a-test-result"
415
+ else:
416
+ payload = text.strip()
417
+ try:
418
+ envelope = json.loads(payload)
419
+ except json.JSONDecodeError:
420
+ return None, "result-unreadable"
421
+ if not isinstance(envelope, dict):
422
+ return None, "result-unreadable"
423
+ return envelope, None
424
+
425
+
426
+ def record(skill: str, text: str) -> dict[str, Any]:
427
+ envelope, problem = _envelope(text)
428
+ out: dict[str, Any] = {
429
+ "skill": skill,
430
+ "status": "failed",
431
+ "verdict": None,
432
+ "quality_score": None,
433
+ "threshold": None,
434
+ "threshold_fallback": False,
435
+ "reason": problem,
436
+ }
437
+ if envelope is None:
438
+ return out
439
+
440
+ verdict = envelope.get("verdict")
441
+ if isinstance(verdict, str):
442
+ verdict = {"PASS_WITH_DRIFT": "pass-with-drift"}.get(verdict.upper(), verdict)
443
+ if verdict.upper() in ("PASS", "FAIL", "INCONCLUSIVE"):
444
+ verdict = verdict.upper()
445
+ score = envelope.get("score")
446
+ out.update(
447
+ verdict=verdict,
448
+ quality_score=_plain(score) if _is_number(score) else None,
449
+ threshold=_plain(envelope["threshold"]) if _is_number(envelope.get("threshold")) else None,
450
+ threshold_fallback=envelope.get("threshold_fallback") is True,
451
+ )
452
+
453
+ named = envelope.get("skill_name")
454
+ if isinstance(named, str) and named and named != skill:
455
+ out["reason"] = f"result-for-another-skill: {named}"
456
+ elif envelope.get("status") == "error":
457
+ out["reason"] = envelope.get("halt_reason") or verdict or "error"
458
+ elif verdict == "PASS":
459
+ route = envelope.get("next_workflow")
460
+ if route in (None, "export-skill"):
461
+ out.update(status="completed", reason=None)
462
+ else:
463
+ out["reason"] = f"PASS routed to {route}"
464
+ elif verdict in ("FAIL", "INCONCLUSIVE", "pass-with-drift"):
465
+ out["reason"] = verdict
466
+ else:
467
+ out["reason"] = f"unknown-verdict: {verdict}"
468
+ return out
469
+
470
+
471
+ def export_name(skill: dict[str, Any]) -> Any:
472
+ """The skill folder skf-export-skill resolves for a completed skill: the
473
+ last part of the package path its build recorded, else its campaign name."""
474
+ path = skill.get("skill_path")
475
+ if isinstance(path, str) and path.strip():
476
+ return PurePosixPath(path.strip().replace("\\", "/")).name or skill.get("name")
477
+ return skill.get("name")
478
+
479
+
480
+ def classify(state: dict[str, Any], directive: dict[str, Any]) -> dict[str, Any]:
481
+ base = state["campaign"]["quality_gate"]
482
+ names = {s.get("name") for s in state["skills"] if isinstance(s, dict)}
483
+ rows: list[dict[str, Any]] = []
484
+ for skill in state["skills"]:
485
+ if not isinstance(skill, dict) or skill.get("status") != "completed":
486
+ continue
487
+ name = skill.get("name")
488
+ gate = _checked(*effective_gate(base, directive, name))
489
+ score = skill.get("quality_score")
490
+ if not _is_number(score):
491
+ verdict, reason = "fail", "no quality score to check against the gate"
492
+ elif score >= gate["soft_target"]:
493
+ verdict, reason = "pass", None
494
+ elif score >= gate["soft_fallback"]:
495
+ verdict, reason = "fallback", f"below soft_target {gate['soft_target']}"
496
+ else:
497
+ verdict, reason = "fail", f"below soft_fallback {gate['soft_fallback']}"
498
+ rows.append({
499
+ "name": name,
500
+ "tier": skill.get("tier"),
501
+ "skill_path": skill.get("skill_path"),
502
+ "export_name": export_name(skill),
503
+ "quality_score": _plain(score) if _is_number(score) else None,
504
+ "soft_target": gate["soft_target"],
505
+ "soft_fallback": gate["soft_fallback"],
506
+ "verdict": verdict,
507
+ "reason": reason,
508
+ })
509
+ return {
510
+ "gate": _checked(*effective_gate(base, directive)),
511
+ "skills": rows,
512
+ "export": [r["name"] for r in rows if r["verdict"] in ("pass", "fallback")],
513
+ "excluded": [
514
+ {key: r[key] for key in ("name", "tier", "quality_score", "verdict", "reason")}
515
+ for r in rows if r["verdict"] == "fail"
516
+ ],
517
+ "counts": {v: sum(1 for r in rows if r["verdict"] == v) for v in ("pass", "fallback", "fail")},
518
+ "unparsed": directive["unparsed"],
519
+ "warnings": _unknown_name_warnings(directive, names),
520
+ }
521
+
522
+
523
+ # --------------------------------------------------------------------------
524
+ # CLI
525
+ # --------------------------------------------------------------------------
526
+
527
+
528
+ def _emit(payload: dict[str, Any], stream: Any) -> None:
529
+ stream.write(json.dumps(payload, separators=(",", ":")) + "\n")
530
+
531
+
532
+ def _read_result(path: str | None) -> str:
533
+ if path is None:
534
+ return sys.stdin.buffer.read().decode("utf-8", errors="replace")
535
+ if not Path(path).is_file():
536
+ raise GateError(f"Result file not found: {path}", "RESULT_NOT_FOUND")
537
+ return Path(path).read_bytes().decode("utf-8", errors="replace")
538
+
539
+
540
+ def build_parser() -> argparse.ArgumentParser:
541
+ parser = argparse.ArgumentParser(
542
+ prog="campaign-quality-gate",
543
+ description="Apply the campaign quality gate: check it, resolve it per skill, "
544
+ "settle a test result, classify completed skills for export.",
545
+ )
546
+ sub = parser.add_subparsers(dest="command", required=True)
547
+
548
+ p_check = sub.add_parser("check", help="validate a quality gate before it is written to state")
549
+ p_check.add_argument("--hard", required=True)
550
+ p_check.add_argument("--soft-target", required=True)
551
+ p_check.add_argument("--soft-fallback", required=True)
552
+ p_check.add_argument("--brief-file", help="campaign brief whose quality_gate wins field by field")
553
+
554
+ p_resolve = sub.add_parser("resolve", help="one skill's gate and Skip List entry")
555
+ p_resolve.add_argument("--state-file", required=True)
556
+ p_resolve.add_argument("--skill", required=True)
557
+ p_resolve.add_argument("--directive-file")
558
+ p_resolve.add_argument("--brief-file", help="campaign brief: adds the brief-skill inputs (brief_skill)")
559
+
560
+ p_record = sub.add_parser("record", help="settle a skill from its SKF_TEST_RESULT_JSON line")
561
+ p_record.add_argument("--skill", required=True)
562
+ p_record.add_argument("--result", help="file holding the envelope line (default: stdin)")
563
+
564
+ p_classify = sub.add_parser("classify", help="classify completed skills for export")
565
+ p_classify.add_argument("--state-file", required=True)
566
+ p_classify.add_argument("--directive-file")
567
+ return parser
568
+
569
+
570
+ def main(argv: list[str] | None = None) -> int:
571
+ args = build_parser().parse_args(argv)
572
+ try:
573
+ if args.command == "check":
574
+ brief = _load_brief(args.brief_file) if args.brief_file else None
575
+ result = check(args.hard, args.soft_target, args.soft_fallback, brief)
576
+ elif args.command == "resolve":
577
+ state = _load_state(args.state_file)
578
+ brief = _load_brief(args.brief_file) if args.brief_file else None
579
+ result = resolve(state, read_directive(args.directive_file), args.skill, brief)
580
+ elif args.command == "record":
581
+ result = record(args.skill, _read_result(args.result))
582
+ else:
583
+ result = classify(_load_state(args.state_file), read_directive(args.directive_file))
584
+ except GateError as exc:
585
+ _emit({"error": str(exc), "code": exc.code, **exc.extra}, sys.stderr)
586
+ return 2
587
+ _emit(result, sys.stdout)
588
+ return 0
589
+
590
+
591
+ if __name__ == "__main__":
592
+ sys.exit(main())