bmad-module-skill-forge 1.9.0 → 2.0.1

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 (281) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.nvmrc +1 -1
  3. package/README.md +10 -5
  4. package/docs/_data/pinned.yaml +2 -2
  5. package/docs/agents.md +11 -2
  6. package/docs/architecture.md +5 -4
  7. package/docs/bmad-synergy.md +30 -7
  8. package/docs/campaign.md +172 -0
  9. package/docs/examples.md +7 -3
  10. package/docs/forge-auto.md +90 -0
  11. package/docs/getting-started.md +9 -0
  12. package/docs/how-it-works.md +6 -4
  13. package/docs/index.md +4 -3
  14. package/docs/skill-model.md +1 -1
  15. package/docs/troubleshooting.md +17 -1
  16. package/docs/verifying-a-skill.md +2 -2
  17. package/docs/why-skf.md +1 -1
  18. package/docs/workflows.md +64 -24
  19. package/package.json +2 -2
  20. package/src/knowledge/skill-lifecycle.md +1 -1
  21. package/src/module-help.csv +4 -3
  22. package/src/shared/data/language-corpora.json +32 -0
  23. package/src/shared/references/pipeline-contracts.md +6 -3
  24. package/src/shared/scripts/schemas/skill-brief.v1.json +6 -1
  25. package/src/shared/scripts/skf-comention-pairs.py +350 -0
  26. package/src/shared/scripts/skf-count-tokens.py +222 -0
  27. package/src/shared/scripts/skf-derive-assembly-shape.py +107 -0
  28. package/src/shared/scripts/skf-detect-docs.py +240 -4
  29. package/src/shared/scripts/skf-detect-language.py +82 -3
  30. package/src/shared/scripts/skf-detect-tools.py +105 -2
  31. package/src/shared/scripts/skf-emit-result-envelope.py +17 -0
  32. package/src/shared/scripts/skf-enumerate-stack-skills.py +111 -2
  33. package/src/shared/scripts/skf-extract-public-api.py +53 -0
  34. package/src/shared/scripts/skf-find-cycles.py +246 -0
  35. package/src/shared/scripts/skf-hash-content.py +487 -2
  36. package/src/shared/scripts/skf-language-corpora.py +100 -0
  37. package/src/shared/scripts/skf-manifest-ops.py +67 -1
  38. package/src/shared/scripts/skf-merge-doc-urls.py +244 -0
  39. package/src/shared/scripts/skf-preapply.py +1 -1
  40. package/src/shared/scripts/skf-rebuild-managed-sections.py +180 -0
  41. package/src/shared/scripts/skf-recommend-scope-type.py +1 -2
  42. package/src/shared/scripts/skf-render-metadata-stats.py +499 -0
  43. package/src/shared/scripts/skf-rewrite-skill-name.py +308 -0
  44. package/src/shared/scripts/skf-shape-detect.py +576 -25
  45. package/src/shared/scripts/skf-shard-body.py +461 -0
  46. package/src/shared/scripts/skf-skill-inventory.py +139 -3
  47. package/src/shared/scripts/skf-structural-diff.py +307 -67
  48. package/src/shared/scripts/skf-validate-brief-schema.py +41 -6
  49. package/src/shared/scripts/skf-validate-feasibility-report.py +234 -0
  50. package/src/shared/scripts/skf-validate-frontmatter.py +17 -12
  51. package/src/shared/scripts/skf-validate-output.py +443 -10
  52. package/src/shared/scripts/skf-validate-pins.py +3 -3
  53. package/src/shared/scripts/skf-verify-no-trace.py +214 -0
  54. package/src/shared/scripts/skf-verify-provenance-completeness.py +439 -0
  55. package/src/shared/scripts/skf-write-skill-brief.py +21 -5
  56. package/src/skf-analyze-source/SKILL.md +17 -32
  57. package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
  58. package/src/skf-analyze-source/customize.toml +2 -0
  59. package/src/skf-analyze-source/references/auto-docs-only.md +116 -0
  60. package/src/skf-analyze-source/references/continue.md +8 -13
  61. package/src/skf-analyze-source/references/generate-briefs.md +28 -12
  62. package/src/skf-analyze-source/references/headless-contract.md +33 -0
  63. package/src/skf-analyze-source/references/health-check.md +1 -2
  64. package/src/skf-analyze-source/references/identify-units.md +25 -36
  65. package/src/skf-analyze-source/references/init.md +12 -25
  66. package/src/skf-analyze-source/references/map-and-detect.md +8 -20
  67. package/src/skf-analyze-source/references/recommend.md +4 -14
  68. package/src/skf-analyze-source/references/scan-project.md +3 -11
  69. package/src/skf-analyze-source/references/step-auto-scope.md +211 -197
  70. package/src/skf-analyze-source/references/step-shape-detect.md +20 -13
  71. package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -2
  72. package/src/skf-audit-skill/SKILL.md +9 -6
  73. package/src/skf-audit-skill/customize.toml +2 -0
  74. package/src/skf-audit-skill/references/init.md +58 -32
  75. package/src/skf-audit-skill/references/re-index.md +15 -34
  76. package/src/skf-audit-skill/references/report.md +11 -58
  77. package/src/skf-audit-skill/references/semantic-diff.md +4 -22
  78. package/src/skf-audit-skill/references/severity-classify.md +53 -87
  79. package/src/skf-audit-skill/references/step-doc-drift.md +49 -19
  80. package/src/skf-audit-skill/references/structural-diff.md +43 -68
  81. package/src/skf-brief-skill/SKILL.md +7 -36
  82. package/src/skf-brief-skill/assets/scope-templates.md +1 -1
  83. package/src/skf-brief-skill/assets/skill-brief-schema.md +1 -1
  84. package/src/skf-brief-skill/customize.toml +14 -0
  85. package/src/skf-brief-skill/references/analyze-target.md +13 -18
  86. package/src/skf-brief-skill/references/confirm-brief.md +2 -15
  87. package/src/skf-brief-skill/references/draft-checkpoint.md +8 -8
  88. package/src/skf-brief-skill/references/gather-intent.md +27 -24
  89. package/src/skf-brief-skill/references/headless-args.md +1 -1
  90. package/src/skf-brief-skill/references/health-check.md +1 -22
  91. package/src/skf-brief-skill/references/invocation-contract.md +37 -0
  92. package/src/skf-brief-skill/references/scope-definition.md +6 -15
  93. package/src/skf-brief-skill/references/step-auto-brief.md +22 -9
  94. package/src/skf-brief-skill/references/step-auto-validate.md +3 -7
  95. package/src/skf-brief-skill/references/version-resolution.md +1 -1
  96. package/src/skf-brief-skill/references/write-brief.md +19 -13
  97. package/src/skf-campaign/SKILL.md +121 -0
  98. package/src/skf-campaign/assets/campaign-state-schema.json +191 -0
  99. package/src/skf-campaign/customize.toml +75 -0
  100. package/src/skf-campaign/manifest.yaml +15 -0
  101. package/src/skf-campaign/references/campaign-contracts.md +67 -0
  102. package/src/skf-campaign/references/campaign-directive-spec.md +32 -0
  103. package/src/skf-campaign/references/health-check.md +35 -0
  104. package/src/skf-campaign/references/step-01-setup.md +121 -0
  105. package/src/skf-campaign/references/step-02-strategy.md +102 -0
  106. package/src/skf-campaign/references/step-03-pins.md +56 -0
  107. package/src/skf-campaign/references/step-04-provenance.md +63 -0
  108. package/src/skf-campaign/references/step-05-skill-loop.md +103 -0
  109. package/src/skf-campaign/references/step-06-batch.md +88 -0
  110. package/src/skf-campaign/references/step-07-capstone.md +63 -0
  111. package/src/skf-campaign/references/step-08-verify.md +75 -0
  112. package/src/skf-campaign/references/step-09-refine.md +83 -0
  113. package/src/skf-campaign/references/step-10-export.md +106 -0
  114. package/src/skf-campaign/references/step-11-maintenance.md +84 -0
  115. package/src/skf-campaign/references/step-resume.md +115 -0
  116. package/src/skf-campaign/scripts/.gitkeep +0 -0
  117. package/src/skf-campaign/scripts/campaign-deps.py +244 -0
  118. package/src/skf-campaign/scripts/campaign-parse-manifest.py +119 -0
  119. package/src/skf-campaign/scripts/campaign-provenance.py +247 -0
  120. package/src/skf-campaign/scripts/campaign-render-batch.py +199 -0
  121. package/src/skf-campaign/scripts/campaign-render-kickoff.py +158 -0
  122. package/src/skf-campaign/scripts/campaign-report.py +249 -0
  123. package/src/skf-campaign/scripts/campaign-status.py +162 -0
  124. package/src/skf-campaign/scripts/campaign-validate-pins.py +174 -0
  125. package/src/skf-campaign/scripts/campaign-validate-state.py +207 -0
  126. package/src/skf-campaign/templates/campaign-brief-template.yaml +34 -0
  127. package/src/skf-campaign/templates/campaign-report-template.md +54 -0
  128. package/src/skf-campaign/templates/kickoff-template.md +48 -0
  129. package/src/skf-create-skill/SKILL.md +15 -4
  130. package/src/skf-create-skill/assets/compile-assembly-rules.md +35 -34
  131. package/src/skf-create-skill/assets/skill-sections.md +9 -100
  132. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +4 -13
  133. package/src/skf-create-skill/customize.toml +13 -0
  134. package/src/skf-create-skill/references/authoritative-files-protocol.md +7 -7
  135. package/src/skf-create-skill/references/compile.md +37 -47
  136. package/src/skf-create-skill/references/component-extraction.md +8 -35
  137. package/src/skf-create-skill/references/ecosystem-check.md +9 -25
  138. package/src/skf-create-skill/references/enrich.md +4 -17
  139. package/src/skf-create-skill/references/extract.md +20 -27
  140. package/src/skf-create-skill/references/extraction-patterns-tracing.md +1 -1
  141. package/src/skf-create-skill/references/extraction-patterns.md +10 -8
  142. package/src/skf-create-skill/references/generate-artifacts.md +20 -25
  143. package/src/skf-create-skill/references/health-check.md +2 -2
  144. package/src/skf-create-skill/references/load-brief.md +16 -18
  145. package/src/skf-create-skill/references/report.md +27 -14
  146. package/src/skf-create-skill/references/source-resolution-protocols.md +4 -4
  147. package/src/skf-create-skill/references/step-auto-shard.md +52 -25
  148. package/src/skf-create-skill/references/step-doc-rot.md +33 -15
  149. package/src/skf-create-skill/references/step-doc-sources.md +13 -4
  150. package/src/skf-create-skill/references/sub/ccc-discover.md +2 -13
  151. package/src/skf-create-skill/references/sub/fetch-docs.md +33 -46
  152. package/src/skf-create-skill/references/sub/fetch-temporal.md +5 -20
  153. package/src/skf-create-skill/references/tier-degradation-rules.md +6 -8
  154. package/src/skf-create-skill/references/validate.md +69 -29
  155. package/src/skf-create-skill/scripts/scan-doc-rot.py +245 -0
  156. package/src/skf-create-stack-skill/SKILL.md +15 -9
  157. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +1 -1
  158. package/src/skf-create-stack-skill/customize.toml +10 -0
  159. package/src/skf-create-stack-skill/references/compile-stack.md +7 -8
  160. package/src/skf-create-stack-skill/references/compose-mode-rules.md +2 -13
  161. package/src/skf-create-stack-skill/references/detect-integrations.md +49 -54
  162. package/src/skf-create-stack-skill/references/detect-manifests.md +37 -30
  163. package/src/skf-create-stack-skill/references/generate-output.md +24 -75
  164. package/src/skf-create-stack-skill/references/init.md +9 -81
  165. package/src/skf-create-stack-skill/references/manifest-patterns.md +5 -35
  166. package/src/skf-create-stack-skill/references/parallel-extract.md +12 -27
  167. package/src/skf-create-stack-skill/references/rank-and-confirm.md +6 -6
  168. package/src/skf-create-stack-skill/references/report.md +27 -62
  169. package/src/skf-create-stack-skill/references/validate.md +35 -31
  170. package/src/skf-drop-skill/SKILL.md +20 -23
  171. package/src/skf-drop-skill/customize.toml +10 -0
  172. package/src/skf-drop-skill/references/execute.md +67 -36
  173. package/src/skf-drop-skill/references/headless-contract.md +18 -0
  174. package/src/skf-drop-skill/references/health-check.md +1 -13
  175. package/src/skf-drop-skill/references/report.md +44 -10
  176. package/src/skf-drop-skill/references/select.md +93 -48
  177. package/src/skf-drop-skill/scripts/dir-sizes.py +116 -0
  178. package/src/skf-export-skill/SKILL.md +17 -14
  179. package/src/skf-export-skill/assets/managed-section-format.md +2 -5
  180. package/src/skf-export-skill/customize.toml +10 -6
  181. package/src/skf-export-skill/references/generate-snippet.md +21 -35
  182. package/src/skf-export-skill/references/load-skill.md +44 -42
  183. package/src/skf-export-skill/references/multi-skill-mode.md +1 -1
  184. package/src/skf-export-skill/references/orphan-context-detection.md +11 -3
  185. package/src/skf-export-skill/references/package.md +27 -33
  186. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +24 -5
  187. package/src/skf-export-skill/references/result-envelope.md +28 -0
  188. package/src/skf-export-skill/references/summary.md +11 -5
  189. package/src/skf-export-skill/references/token-report.md +42 -24
  190. package/src/skf-export-skill/references/update-context.md +32 -33
  191. package/src/skf-forger/SKILL.md +32 -39
  192. package/src/skf-forger/references/pipeline-mode.md +34 -0
  193. package/src/skf-forger/scripts/parse-pipeline.py +270 -0
  194. package/src/skf-quick-skill/SKILL.md +10 -60
  195. package/src/skf-quick-skill/customize.toml +9 -0
  196. package/src/skf-quick-skill/references/batch-mode.md +6 -4
  197. package/src/skf-quick-skill/references/compile.md +15 -52
  198. package/src/skf-quick-skill/references/ecosystem-check.md +5 -20
  199. package/src/skf-quick-skill/references/finalize.md +12 -6
  200. package/src/skf-quick-skill/references/halt-contract.md +54 -0
  201. package/src/skf-quick-skill/references/health-check.md +3 -4
  202. package/src/skf-quick-skill/references/quick-extract.md +7 -18
  203. package/src/skf-quick-skill/references/registry-resolution.md +1 -23
  204. package/src/skf-quick-skill/references/resolve-target.md +27 -34
  205. package/src/skf-quick-skill/references/write-and-validate.md +10 -25
  206. package/src/skf-refine-architecture/SKILL.md +16 -8
  207. package/src/skf-refine-architecture/customize.toml +9 -0
  208. package/src/skf-refine-architecture/references/compile.md +10 -8
  209. package/src/skf-refine-architecture/references/gap-analysis.md +21 -50
  210. package/src/skf-refine-architecture/references/health-check.md +1 -1
  211. package/src/skf-refine-architecture/references/improvements.md +9 -25
  212. package/src/skf-refine-architecture/references/init.md +9 -16
  213. package/src/skf-refine-architecture/references/issue-detection.md +10 -25
  214. package/src/skf-refine-architecture/references/refinement-rules.md +4 -16
  215. package/src/skf-refine-architecture/references/report.md +14 -11
  216. package/src/skf-rename-skill/SKILL.md +11 -21
  217. package/src/skf-rename-skill/customize.toml +10 -0
  218. package/src/skf-rename-skill/references/execute.md +112 -82
  219. package/src/skf-rename-skill/references/exit-codes.md +12 -0
  220. package/src/skf-rename-skill/references/report.md +16 -8
  221. package/src/skf-rename-skill/references/select.md +69 -42
  222. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -0
  223. package/src/skf-setup/SKILL.md +21 -13
  224. package/src/skf-setup/customize.toml +10 -0
  225. package/src/skf-setup/references/auto-index.md +11 -10
  226. package/src/skf-setup/references/ccc-index.md +14 -15
  227. package/src/skf-setup/references/detect-and-tier.md +10 -8
  228. package/src/skf-setup/references/report.md +26 -32
  229. package/src/skf-setup/references/tier-rules.md +2 -32
  230. package/src/skf-setup/references/write-config.md +9 -10
  231. package/src/skf-test-skill/SKILL.md +10 -8
  232. package/src/skf-test-skill/customize.toml +2 -0
  233. package/src/skf-test-skill/references/coherence-check.md +19 -31
  234. package/src/skf-test-skill/references/coverage-check.md +111 -67
  235. package/src/skf-test-skill/references/detect-mode.md +10 -41
  236. package/src/skf-test-skill/references/external-validators.md +11 -19
  237. package/src/skf-test-skill/references/init.md +39 -21
  238. package/src/skf-test-skill/references/migration-section-rules.md +4 -4
  239. package/src/skf-test-skill/references/report.md +60 -40
  240. package/src/skf-test-skill/references/score.md +32 -64
  241. package/src/skf-test-skill/references/scoring-rules.md +6 -62
  242. package/src/skf-test-skill/references/source-access-protocol.md +15 -5
  243. package/src/skf-test-skill/references/step-hard-gate.md +1 -3
  244. package/src/skf-test-skill/scripts/aggregate-coherence.py +276 -0
  245. package/src/skf-test-skill/scripts/check-metadata-coherence.py +359 -0
  246. package/src/skf-test-skill/scripts/combine-external-scores.py +190 -0
  247. package/src/skf-test-skill/scripts/compute-score.py +135 -4
  248. package/src/skf-test-skill/scripts/reconcile-coverage.py +327 -0
  249. package/src/skf-test-skill/scripts/validate-inventory.py +251 -0
  250. package/src/skf-test-skill/scripts/verify-declared-numerator.py +192 -0
  251. package/src/skf-update-skill/SKILL.md +17 -5
  252. package/src/skf-update-skill/customize.toml +11 -6
  253. package/src/skf-update-skill/references/detect-changes.md +48 -36
  254. package/src/skf-update-skill/references/health-check.md +5 -11
  255. package/src/skf-update-skill/references/init.md +40 -32
  256. package/src/skf-update-skill/references/manual-section-rules.md +1 -2
  257. package/src/skf-update-skill/references/merge-conflict-rules.md +1 -52
  258. package/src/skf-update-skill/references/merge.md +13 -57
  259. package/src/skf-update-skill/references/re-extract.md +27 -25
  260. package/src/skf-update-skill/references/report.md +14 -7
  261. package/src/skf-update-skill/references/validate.md +32 -30
  262. package/src/skf-update-skill/references/write.md +86 -51
  263. package/src/skf-update-skill/scripts/skf-new-file-diff.py +167 -0
  264. package/src/skf-verify-stack/SKILL.md +11 -21
  265. package/src/skf-verify-stack/customize.toml +12 -3
  266. package/src/skf-verify-stack/references/coverage-patterns.md +1 -29
  267. package/src/skf-verify-stack/references/coverage.md +21 -10
  268. package/src/skf-verify-stack/references/exit-codes.md +14 -0
  269. package/src/skf-verify-stack/references/health-check.md +1 -1
  270. package/src/skf-verify-stack/references/init.md +20 -25
  271. package/src/skf-verify-stack/references/integration-verification-rules.md +10 -19
  272. package/src/skf-verify-stack/references/integrations.md +44 -49
  273. package/src/skf-verify-stack/references/report.md +28 -34
  274. package/src/skf-verify-stack/references/requirements.md +3 -3
  275. package/src/skf-verify-stack/references/synthesize.md +36 -39
  276. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +196 -0
  277. package/src/skf-verify-stack/scripts/skf-report-delta.py +256 -0
  278. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +258 -0
  279. package/tools/cli/lib/ui.js +3 -2
  280. package/docs/deepwiki.md +0 -89
  281. package/src/skf-rename-skill/references/rebuild-context.md +0 -110
@@ -0,0 +1,199 @@
1
+ # /// script
2
+ # requires-python = ">=3.9"
3
+ # dependencies = ["pyyaml"]
4
+ # ///
5
+ """Campaign Render Batch — build the QS `--batch` input file for Tier B skills.
6
+
7
+ step-06 batches every Tier B skill through `skf-quick-skill --batch`. The batch
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).
20
+ - Looks up each skill's `repo_url` from the brief's `targets[]` (matched by
21
+ name); repo URLs live only in the brief, never in the state schema.
22
+ - 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)
25
+ with optional ` language=<lang>` / ` scope=<path>` modifiers appended when the
26
+ 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.
28
+
29
+ CLI:
30
+ uv run campaign-render-batch.py --state-file <p> --brief-file <p> [-o <batchFile>]
31
+
32
+ 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.
35
+
36
+ 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
40
+ has no matching brief target with a repo_url (unmatched-target)
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ import argparse
46
+ import json
47
+ import sys
48
+ from pathlib import Path
49
+ from typing import Any, Dict, List, Tuple
50
+
51
+ import yaml
52
+
53
+
54
+ def _err(message: str, code: str, exit_code: int, **extra: Any) -> int:
55
+ payload: Dict[str, Any] = {"error": message, "code": code}
56
+ payload.update(extra)
57
+ json.dump(payload, sys.stderr)
58
+ sys.stderr.write("\n")
59
+ return exit_code
60
+
61
+
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."""
64
+ line = repo_url
65
+ if pin:
66
+ 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
+
75
+
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
+
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.
82
+ """
83
+ skills = state.get("skills") or []
84
+ targets: Dict[str, Dict[str, Any]] = {
85
+ t.get("name"): t
86
+ for t in (brief.get("targets") or [])
87
+ if isinstance(t, dict) and t.get("name")
88
+ }
89
+
90
+ lines: List[str] = []
91
+ unmatched: List[str] = []
92
+ skipped_non_tierb = 0
93
+ skipped_non_pending = 0
94
+
95
+ for skill in skills:
96
+ if not isinstance(skill, dict):
97
+ continue
98
+ if skill.get("tier") != "B":
99
+ skipped_non_tierb += 1
100
+ continue
101
+ if skill.get("status") != "pending":
102
+ skipped_non_pending += 1
103
+ continue
104
+
105
+ name = skill.get("name")
106
+ target = targets.get(name)
107
+ repo_url = (target or {}).get("repo_url") or ""
108
+ if not repo_url:
109
+ unmatched.append(name)
110
+ continue
111
+
112
+ lines.append(_target_line(repo_url, skill.get("pin"), target))
113
+
114
+ summary = {
115
+ "count": len(lines),
116
+ "skipped_non_tierB": skipped_non_tierb,
117
+ "skipped_non_pending": skipped_non_pending,
118
+ "unmatched": unmatched,
119
+ }
120
+ return lines, summary
121
+
122
+
123
+ def _load_yaml_mapping(path: str) -> Dict[str, Any]:
124
+ data = yaml.safe_load(Path(path).read_text(encoding="utf-8"))
125
+ if data is None:
126
+ return {}
127
+ if not isinstance(data, dict):
128
+ raise ValueError("top-level document is not a mapping")
129
+ return data
130
+
131
+
132
+ def run(state_file: str, brief_file: str, output: str | None) -> int:
133
+ # State problems are plain file/parse errors (exit 2).
134
+ if not Path(state_file).is_file():
135
+ return _err(f"State file not found: {state_file}", "STATE_NOT_FOUND", 2)
136
+ try:
137
+ state = _load_yaml_mapping(state_file)
138
+ except (yaml.YAMLError, ValueError) as exc:
139
+ return _err(f"Failed to parse state YAML: {exc}", "STATE_PARSE_ERROR", 2)
140
+
141
+ # Brief problems preserve step-06 §4's missing-brief HALT (exit 8): repo_urls
142
+ # live only in the brief, so a missing/unreadable/corrupt brief is fatal here.
143
+ if not Path(brief_file).is_file():
144
+ return _err(f"Brief file not found: {brief_file}", "missing-brief", 8)
145
+ try:
146
+ brief = _load_yaml_mapping(brief_file)
147
+ except OSError as exc:
148
+ return _err(f"Brief unreadable: {exc}", "missing-brief", 8)
149
+ except (yaml.YAMLError, ValueError) as exc:
150
+ return _err(f"Brief unparseable: {exc}", "missing-brief", 8)
151
+
152
+ lines, summary = build_batch(state, brief)
153
+
154
+ unmatched = summary.pop("unmatched")
155
+ if unmatched:
156
+ return _err(
157
+ "No matching brief target (repo_url) for pending Tier B skill(s): "
158
+ + ", ".join(str(n) for n in unmatched),
159
+ "unmatched-target",
160
+ 8,
161
+ skills=unmatched,
162
+ )
163
+
164
+ batch_text = "\n".join(lines)
165
+ if batch_text:
166
+ batch_text += "\n"
167
+
168
+ if output:
169
+ Path(output).write_text(batch_text, encoding="utf-8")
170
+ summary["written"] = output
171
+ else:
172
+ sys.stdout.write(batch_text)
173
+ summary["written"] = "<stdout>"
174
+
175
+ ordered = {
176
+ "written": summary["written"],
177
+ "count": summary["count"],
178
+ "skipped_non_tierB": summary["skipped_non_tierB"],
179
+ "skipped_non_pending": summary["skipped_non_pending"],
180
+ }
181
+ json.dump(ordered, sys.stderr)
182
+ sys.stderr.write("\n")
183
+ return 0
184
+
185
+
186
+ def main(argv: list[str] | None = None) -> int:
187
+ parser = argparse.ArgumentParser(
188
+ prog="campaign-render-batch",
189
+ description="Build the QS --batch input file for pending Tier B skills.",
190
+ )
191
+ parser.add_argument("--state-file", required=True)
192
+ parser.add_argument("--brief-file", required=True)
193
+ parser.add_argument("-o", "--output", dest="output", help="batch file path (default: stdout)")
194
+ args = parser.parse_args(argv)
195
+ return run(args.state_file, args.brief_file, args.output)
196
+
197
+
198
+ if __name__ == "__main__":
199
+ raise SystemExit(main())
@@ -0,0 +1,158 @@
1
+ # /// script
2
+ # requires-python = ">=3.9"
3
+ # dependencies = ["pyyaml"]
4
+ # ///
5
+ """Campaign Render Kickoff — fill the mechanical kickoff-template placeholders.
6
+
7
+ step-05 emits a kickoff message per Tier-A skill. Most of its placeholders are
8
+ direct field copies from state + brief (campaign name, stage, quality gate,
9
+ skill identity, repo, pin, commit, the dependency-status table, the workaround
10
+ list) — mechanical substitution that an LLM should not hand-perform 15× per
11
+ campaign. This script renders those deterministically and leaves the three
12
+ judgment slots untouched for the LLM to fill in context:
13
+
14
+ {{brief_summary}} — concise summary of the brief target entry
15
+ {{persistent_facts}} — campaign-wide facts resolved in On Activation
16
+ {{directive_content}} — raw directive file content
17
+
18
+ CLI:
19
+ uv run campaign-render-kickoff.py --state-file <p> --brief-file <p> \
20
+ --skill <name> --template <p> [--workarounds '<json-list>']
21
+
22
+ Output: the rendered kickoff markdown on stdout (judgment slots preserved).
23
+
24
+ Exit codes:
25
+ 0 rendered
26
+ 2 error (missing file, bad YAML, skill/target not found, bad --workarounds)
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import argparse
32
+ import json
33
+ import sys
34
+ from pathlib import Path
35
+ from typing import Any, Dict, List, Optional
36
+
37
+ import yaml
38
+
39
+ # Placeholders this script intentionally leaves for the LLM to fill.
40
+ JUDGMENT_SLOTS = ("{{brief_summary}}", "{{persistent_facts}}", "{{directive_content}}")
41
+
42
+
43
+ def _err(message: str, code: str) -> int:
44
+ json.dump({"error": message, "code": code}, sys.stderr)
45
+ sys.stderr.write("\n")
46
+ return 2
47
+
48
+
49
+ def _quality_gate_summary(qg: Dict[str, Any]) -> str:
50
+ return (
51
+ f"Hard: {qg.get('hard', 'N/A')} | "
52
+ f"Soft: {qg.get('soft_target', 'N/A')} (fallback: {qg.get('soft_fallback', 'N/A')})"
53
+ )
54
+
55
+
56
+ def _dependency_status_table(skill: Dict[str, Any], skill_map: Dict[str, Dict[str, Any]]) -> str:
57
+ deps = skill.get("depends_on", []) or []
58
+ if not deps:
59
+ return "No dependencies."
60
+ rows = ["| Dependency | Status |", "|------------|--------|"]
61
+ for dep in deps:
62
+ status = skill_map.get(dep, {}).get("status", "unknown")
63
+ rows.append(f"| {dep} | {status} |")
64
+ return "\n".join(rows)
65
+
66
+
67
+ def _workarounds_list(workarounds: List[str]) -> str:
68
+ if not workarounds:
69
+ return "None"
70
+ return "\n".join(f"- {w}" for w in workarounds)
71
+
72
+
73
+ def render_kickoff(
74
+ state: Dict[str, Any],
75
+ brief: Dict[str, Any],
76
+ skill_name: str,
77
+ template: str,
78
+ workarounds: Optional[List[str]] = None,
79
+ ) -> str:
80
+ campaign = state.get("campaign", {})
81
+ skills = state.get("skills", [])
82
+ skill_map = {s["name"]: s for s in skills}
83
+ if skill_name not in skill_map:
84
+ raise KeyError(f"Skill '{skill_name}' not found in state")
85
+ skill = skill_map[skill_name]
86
+
87
+ targets = {t["name"]: t for t in brief.get("targets", [])}
88
+ repo_url = targets.get(skill_name, {}).get("repo_url", "")
89
+
90
+ wa = workarounds if workarounds is not None else (skill.get("workarounds_applied", []) or [])
91
+
92
+ mechanical = {
93
+ "{{campaign_name}}": str(campaign.get("name", "")),
94
+ "{{current_stage}}": str(campaign.get("current_stage", "")),
95
+ "{{quality_gate_summary}}": _quality_gate_summary(campaign.get("quality_gate", {})),
96
+ "{{skill_name}}": skill_name,
97
+ "{{skill_tier}}": str(skill.get("tier", "")),
98
+ "{{pin}}": skill.get("pin") or "latest",
99
+ "{{commit_sha}}": skill.get("commit_sha") or "unknown",
100
+ "{{repo_url}}": repo_url,
101
+ "{{workarounds_list}}": _workarounds_list(wa),
102
+ "{{dependency_status_table}}": _dependency_status_table(skill, skill_map),
103
+ }
104
+
105
+ out = template
106
+ for key, value in mechanical.items():
107
+ out = out.replace(key, value)
108
+ return out
109
+
110
+
111
+ def run(state_file: str, brief_file: str, skill: str, template_file: str, workarounds_json: Optional[str]) -> int:
112
+ for label, p in (("State", state_file), ("Brief", brief_file), ("Template", template_file)):
113
+ if not Path(p).is_file():
114
+ return _err(f"{label} file not found: {p}", f"{label.upper()}_NOT_FOUND")
115
+
116
+ try:
117
+ state = yaml.safe_load(Path(state_file).read_text(encoding="utf-8"))
118
+ brief = yaml.safe_load(Path(brief_file).read_text(encoding="utf-8"))
119
+ except yaml.YAMLError as exc:
120
+ return _err(f"Failed to parse YAML: {exc}", "PARSE_ERROR")
121
+ template = Path(template_file).read_text(encoding="utf-8")
122
+
123
+ workarounds: Optional[List[str]] = None
124
+ if workarounds_json:
125
+ try:
126
+ workarounds = json.loads(workarounds_json)
127
+ if not isinstance(workarounds, list):
128
+ raise ValueError("not a list")
129
+ except ValueError as exc:
130
+ return _err(f"--workarounds must be a JSON list: {exc}", "BAD_WORKAROUNDS")
131
+
132
+ try:
133
+ rendered = render_kickoff(state, brief, skill, template, workarounds)
134
+ except KeyError as exc:
135
+ return _err(str(exc), "SKILL_NOT_FOUND")
136
+
137
+ sys.stdout.write(rendered)
138
+ if not rendered.endswith("\n"):
139
+ sys.stdout.write("\n")
140
+ return 0
141
+
142
+
143
+ def main(argv: list[str] | None = None) -> int:
144
+ parser = argparse.ArgumentParser(
145
+ prog="campaign-render-kickoff",
146
+ description="Render the mechanical placeholders in the campaign kickoff template.",
147
+ )
148
+ parser.add_argument("--state-file", required=True)
149
+ parser.add_argument("--brief-file", required=True)
150
+ parser.add_argument("--skill", required=True, help="skill name (must exist in state)")
151
+ parser.add_argument("--template", required=True, dest="template_file")
152
+ parser.add_argument("--workarounds", dest="workarounds_json", help="JSON list of applied workarounds")
153
+ args = parser.parse_args(argv)
154
+ return run(args.state_file, args.brief_file, args.skill, args.template_file, args.workarounds_json)
155
+
156
+
157
+ if __name__ == "__main__":
158
+ raise SystemExit(main())
@@ -0,0 +1,249 @@
1
+ # /// script
2
+ # requires-python = ">=3.9"
3
+ # dependencies = ["pyyaml"]
4
+ # ///
5
+ """Campaign Report — generate a markdown report from campaign state + template.
6
+
7
+ CLI:
8
+ uv run campaign-report.py \
9
+ --state-file <path> --template-file <path> --output-file <path>
10
+
11
+ Output (JSON on stdout):
12
+ {"status":"success","report_path":"...","skills_completed":N,"skills_failed":N,
13
+ "quality_scores":{"skill":score,...},"duration":"..."}
14
+
15
+ Exit codes:
16
+ 0 success
17
+ 2 error (missing file, bad YAML, template error)
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import json
24
+ import os
25
+ import sys
26
+ from datetime import datetime, timezone
27
+ from pathlib import Path
28
+ from typing import Any, Dict, List, Optional
29
+
30
+ import yaml
31
+
32
+
33
+ def _emit_error(message: str, code: str) -> None:
34
+ json.dump({"error": message, "code": code}, sys.stderr)
35
+ sys.stderr.write("\n")
36
+
37
+
38
+ def _load_yaml(path: Path) -> Any:
39
+ with open(path, encoding="utf-8") as f:
40
+ return yaml.safe_load(f)
41
+
42
+
43
+ def _parse_iso(value: Optional[str]) -> Optional[datetime]:
44
+ if not value:
45
+ return None
46
+ try:
47
+ return datetime.fromisoformat(value)
48
+ except (ValueError, TypeError):
49
+ return None
50
+
51
+
52
+ def _format_duration(start: Optional[datetime], end: Optional[datetime]) -> str:
53
+ if not start or not end:
54
+ return "N/A"
55
+ delta = end - start
56
+ total_seconds = int(delta.total_seconds())
57
+ if total_seconds < 0:
58
+ return "N/A"
59
+ hours, remainder = divmod(total_seconds, 3600)
60
+ minutes, seconds = divmod(remainder, 60)
61
+ if hours > 0:
62
+ return f"{hours}h {minutes}m {seconds}s"
63
+ if minutes > 0:
64
+ return f"{minutes}m {seconds}s"
65
+ return f"{seconds}s"
66
+
67
+
68
+ def _compute_aggregates(state: Dict[str, Any]) -> Dict[str, Any]:
69
+ campaign = state.get("campaign", {})
70
+ skills: List[Dict[str, Any]] = state.get("skills", [])
71
+
72
+ started_at_str = campaign.get("started_at", "")
73
+ last_updated_str = campaign.get("last_updated", "")
74
+ started_at = _parse_iso(started_at_str)
75
+ last_updated = _parse_iso(last_updated_str)
76
+
77
+ completed = [s for s in skills if s.get("status") == "completed"]
78
+ failed = [s for s in skills if s.get("status") == "failed"]
79
+ skipped = [s for s in skills if s.get("status") == "skipped"]
80
+
81
+ scores = [s["quality_score"] for s in completed if s.get("quality_score") is not None]
82
+ quality_min = min(scores) if scores else 0
83
+ quality_max = max(scores) if scores else 0
84
+ quality_avg = round(sum(scores) / len(scores), 1) if scores else 0
85
+
86
+ all_workarounds: List[str] = []
87
+ skills_with_wa = 0
88
+ for s in skills:
89
+ wa = s.get("workarounds_applied", []) or []
90
+ if wa:
91
+ skills_with_wa += 1
92
+ all_workarounds.extend(wa)
93
+
94
+ skills_table_rows = []
95
+ for s in skills:
96
+ wa = s.get("workarounds_applied", []) or []
97
+ skills_table_rows.append(
98
+ f"| {s.get('name', '')} "
99
+ f"| {s.get('tier', '')} "
100
+ f"| {s.get('status', '')} "
101
+ f"| {s.get('quality_score', 'N/A')} "
102
+ f"| {s.get('pin', 'N/A')} "
103
+ f"| {len(wa)} |"
104
+ )
105
+
106
+ quality_breakdown_rows = []
107
+ for s in completed:
108
+ qs = s.get("quality_score")
109
+ quality_breakdown_rows.append(f"- **{s['name']}**: {qs if qs is not None else 'N/A'}")
110
+
111
+ if not quality_breakdown_rows:
112
+ quality_breakdown_rows.append("No completed skills with quality scores.")
113
+
114
+ if all_workarounds:
115
+ workarounds_list_items = [f"- `{fp}`" for fp in all_workarounds]
116
+ else:
117
+ workarounds_list_items = ["No workarounds applied."]
118
+
119
+ duration_table_rows = []
120
+ for s in skills:
121
+ s_start = _parse_iso(s.get("started_at"))
122
+ s_end = _parse_iso(s.get("completed_at"))
123
+ s_start_str = s.get("started_at", "N/A") or "N/A"
124
+ s_end_str = s.get("completed_at", "N/A") or "N/A"
125
+ dur = _format_duration(s_start, s_end)
126
+ duration_table_rows.append(f"| {s.get('name', '')} | {s_start_str} | {s_end_str} | {dur} |")
127
+
128
+ failed_skipped_lines = []
129
+ if failed:
130
+ failed_skipped_lines.append("### Failed Skills\n")
131
+ for s in failed:
132
+ failed_skipped_lines.append(f"- **{s['name']}** (Tier {s.get('tier', '?')})")
133
+ if skipped:
134
+ failed_skipped_lines.append("\n### Skipped Skills\n")
135
+ for s in skipped:
136
+ failed_skipped_lines.append(f"- **{s['name']}** (Tier {s.get('tier', '?')})")
137
+ if not failed and not skipped:
138
+ if skills:
139
+ failed_skipped_lines.append("All skills completed successfully.")
140
+ else:
141
+ failed_skipped_lines.append("No skills in campaign.")
142
+
143
+ quality_gate = campaign.get("quality_gate", {})
144
+
145
+ return {
146
+ "campaign_name": campaign.get("name", ""),
147
+ "started_at": started_at_str or "N/A",
148
+ "completed_at": last_updated_str or "N/A",
149
+ "duration": _format_duration(started_at, last_updated),
150
+ "quality_gate_hard": quality_gate.get("hard", "N/A"),
151
+ "quality_gate_soft_target": str(quality_gate.get("soft_target", "N/A")),
152
+ "quality_gate_soft_fallback": str(quality_gate.get("soft_fallback", "N/A")),
153
+ "skills_completed": str(len(completed)),
154
+ "skills_failed": str(len(failed)),
155
+ "skills_skipped": str(len(skipped)),
156
+ "skills_table": "\n".join(skills_table_rows),
157
+ "quality_min": str(quality_min),
158
+ "quality_max": str(quality_max),
159
+ "quality_avg": str(quality_avg),
160
+ "quality_breakdown": "\n".join(quality_breakdown_rows),
161
+ "total_workarounds": str(len(all_workarounds)),
162
+ "skills_with_workarounds": str(skills_with_wa),
163
+ "workarounds_list": "\n".join(workarounds_list_items),
164
+ "duration_table": "\n".join(duration_table_rows),
165
+ "failed_skipped_section": "\n".join(failed_skipped_lines),
166
+ }
167
+
168
+
169
+ def run(state_file: str, template_file: str, output_file: str) -> int:
170
+ state_path = Path(state_file)
171
+ template_path = Path(template_file)
172
+ output_path = Path(output_file)
173
+
174
+ if not state_path.is_file():
175
+ _emit_error(f"State file not found: {state_file}", "STATE_NOT_FOUND")
176
+ return 2
177
+
178
+ if not template_path.is_file():
179
+ _emit_error(f"Template file not found: {template_file}", "TEMPLATE_NOT_FOUND")
180
+ return 2
181
+
182
+ try:
183
+ state = _load_yaml(state_path)
184
+ except Exception as exc:
185
+ _emit_error(f"Failed to parse state file: {exc}", "STATE_PARSE_ERROR")
186
+ return 2
187
+
188
+ if not isinstance(state, dict):
189
+ _emit_error("State file root is not a mapping", "INVALID_STATE")
190
+ return 2
191
+
192
+ try:
193
+ template = template_path.read_text(encoding="utf-8")
194
+ except Exception as exc:
195
+ _emit_error(f"Failed to read template file: {exc}", "TEMPLATE_READ_ERROR")
196
+ return 2
197
+
198
+ try:
199
+ aggregates = _compute_aggregates(state)
200
+ except Exception as exc:
201
+ _emit_error(f"Failed to compute report aggregates: {exc}", "AGGREGATE_ERROR")
202
+ return 2
203
+
204
+ report = template
205
+ for key, value in aggregates.items():
206
+ report = report.replace("{{" + key + "}}", value)
207
+
208
+ output_path.parent.mkdir(parents=True, exist_ok=True)
209
+ try:
210
+ output_path.write_text(report, encoding="utf-8")
211
+ except Exception as exc:
212
+ _emit_error(f"Failed to write report: {exc}", "WRITE_ERROR")
213
+ return 2
214
+
215
+ skills = state.get("skills", [])
216
+ completed_count = sum(1 for s in skills if s.get("status") == "completed")
217
+ failed_count = sum(1 for s in skills if s.get("status") == "failed")
218
+ quality_scores = {
219
+ s["name"]: s["quality_score"]
220
+ for s in skills
221
+ if s.get("status") == "completed" and s.get("quality_score") is not None
222
+ }
223
+
224
+ result = {
225
+ "status": "success",
226
+ "report_path": output_path.as_posix(),
227
+ "skills_completed": completed_count,
228
+ "skills_failed": failed_count,
229
+ "quality_scores": quality_scores,
230
+ "duration": aggregates["duration"],
231
+ }
232
+ json.dump(result, sys.stdout, separators=(",", ":"))
233
+ sys.stdout.write("\n")
234
+ return 0
235
+
236
+
237
+ def main() -> int:
238
+ parser = argparse.ArgumentParser(
239
+ description="Generate a campaign report from state and template.",
240
+ )
241
+ parser.add_argument("--state-file", required=True, help="Path to _campaign-state.yaml")
242
+ parser.add_argument("--template-file", required=True, help="Path to campaign-report-template.md")
243
+ parser.add_argument("--output-file", required=True, help="Path to write the generated report")
244
+ args = parser.parse_args()
245
+ return run(args.state_file, args.template_file, args.output_file)
246
+
247
+
248
+ if __name__ == "__main__":
249
+ raise SystemExit(main())