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,162 @@
1
+ # /// script
2
+ # requires-python = ">=3.9"
3
+ # dependencies = ["pyyaml"]
4
+ # ///
5
+ """Campaign Status — deterministic state summary + optional backup-drift verdict.
6
+
7
+ Collapses two in-prompt determinism leaks into one helper the LLM calls instead
8
+ of hand-deriving:
9
+
10
+ - `campaign status` and the resume summary block tallied `skills[]` by status
11
+ ("N completed / M total, K pending, ...") in-prompt over a 15+ skill array.
12
+ - the `.bak` recovery path compared `primary` vs `backup` timestamps and stage
13
+ numbers by hand (an ISO-8601 + int compare across two YAML files).
14
+
15
+ Both are mechanical, one-correct-answer computations. This script owns them so
16
+ identical state always yields identical output.
17
+
18
+ CLI:
19
+ uv run campaign-status.py --state-file <path>
20
+ uv run campaign-status.py --state-file <path> --backup-file <path>
21
+
22
+ Output (JSON on stdout):
23
+ {
24
+ "campaign_name": "...",
25
+ "current_stage": 0,
26
+ "last_updated": "...",
27
+ "total": 0, "completed": 0, "pending": 0,
28
+ "active": 0, "failed": 0, "skipped": 0,
29
+ "backup_comparison": null | {
30
+ "primary_behind": bool,
31
+ "primary_last_updated": "...", "backup_last_updated": "...",
32
+ "primary_stage": 0, "backup_stage": 0
33
+ }
34
+ }
35
+
36
+ Exit codes:
37
+ 0 ok
38
+ 2 error (state missing / unreadable / not a mapping)
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import argparse
44
+ import json
45
+ import sys
46
+ from datetime import datetime
47
+ from pathlib import Path
48
+ from typing import Any, Dict, Optional
49
+
50
+ import yaml
51
+
52
+ STATUSES = ("pending", "active", "completed", "failed", "skipped")
53
+
54
+
55
+ def _err(message: str, code: str) -> int:
56
+ json.dump({"error": message, "code": code}, sys.stderr)
57
+ sys.stderr.write("\n")
58
+ return 2
59
+
60
+
61
+ def _load_state(path: Path) -> Optional[Dict[str, Any]]:
62
+ if not path.is_file():
63
+ return None
64
+ try:
65
+ data = yaml.safe_load(path.read_text(encoding="utf-8"))
66
+ except (yaml.YAMLError, OSError):
67
+ return None
68
+ return data if isinstance(data, dict) else None
69
+
70
+
71
+ def _parse_iso(value: Any) -> Optional[datetime]:
72
+ if not isinstance(value, str) or not value:
73
+ return None
74
+ text = value[:-1] + "+00:00" if value.endswith("Z") else value
75
+ try:
76
+ return datetime.fromisoformat(text)
77
+ except (ValueError, TypeError):
78
+ return None
79
+
80
+
81
+ def _counts(state: Dict[str, Any]) -> Dict[str, int]:
82
+ skills = state.get("skills", []) or []
83
+ tally = {s: 0 for s in STATUSES}
84
+ for skill in skills:
85
+ status = skill.get("status")
86
+ if status in tally:
87
+ tally[status] += 1
88
+ tally["total"] = len(skills)
89
+ return tally
90
+
91
+
92
+ def _compare_backup(primary: Dict[str, Any], backup: Dict[str, Any]) -> Dict[str, Any]:
93
+ p_c = primary.get("campaign", {}) or {}
94
+ b_c = backup.get("campaign", {}) or {}
95
+ p_updated = p_c.get("last_updated")
96
+ b_updated = b_c.get("last_updated")
97
+ p_stage = p_c.get("current_stage")
98
+ b_stage = b_c.get("current_stage")
99
+
100
+ stage_behind = (
101
+ isinstance(p_stage, int) and isinstance(b_stage, int) and p_stage < b_stage
102
+ )
103
+
104
+ ts_behind = False
105
+ p_ts = _parse_iso(p_updated)
106
+ b_ts = _parse_iso(b_updated)
107
+ if p_ts is not None and b_ts is not None:
108
+ try:
109
+ ts_behind = p_ts < b_ts
110
+ except TypeError:
111
+ # naive vs aware datetime — cannot order; defer to stage compare.
112
+ ts_behind = False
113
+
114
+ return {
115
+ "primary_behind": bool(stage_behind or ts_behind),
116
+ "primary_last_updated": p_updated,
117
+ "backup_last_updated": b_updated,
118
+ "primary_stage": p_stage,
119
+ "backup_stage": b_stage,
120
+ }
121
+
122
+
123
+ def run(state_file: str, backup_file: Optional[str]) -> int:
124
+ state = _load_state(Path(state_file))
125
+ if state is None:
126
+ return _err(f"State not readable as a mapping: {state_file}", "STATE_UNREADABLE")
127
+
128
+ campaign = state.get("campaign", {}) or {}
129
+ result: Dict[str, Any] = {
130
+ "campaign_name": campaign.get("name", ""),
131
+ "current_stage": campaign.get("current_stage"),
132
+ "last_updated": campaign.get("last_updated"),
133
+ **_counts(state),
134
+ "backup_comparison": None,
135
+ }
136
+
137
+ if backup_file:
138
+ backup = _load_state(Path(backup_file))
139
+ if backup is not None:
140
+ result["backup_comparison"] = _compare_backup(state, backup)
141
+
142
+ json.dump(result, sys.stdout, separators=(",", ":"), default=str)
143
+ sys.stdout.write("\n")
144
+ return 0
145
+
146
+
147
+ def main(argv: Optional[list] = None) -> int:
148
+ parser = argparse.ArgumentParser(
149
+ prog="campaign-status",
150
+ description="Summarize campaign state and (optionally) compare it to its backup.",
151
+ )
152
+ parser.add_argument("--state-file", required=True, help="Path to _campaign-state.yaml")
153
+ parser.add_argument(
154
+ "--backup-file",
155
+ help="Path to _campaign-state.yaml.bak; when given, emit backup_comparison",
156
+ )
157
+ args = parser.parse_args(argv)
158
+ return run(args.state_file, args.backup_file)
159
+
160
+
161
+ if __name__ == "__main__":
162
+ raise SystemExit(main())
@@ -0,0 +1,174 @@
1
+ # /// script
2
+ # requires-python = ">=3.9"
3
+ # dependencies = ["pyyaml"]
4
+ # ///
5
+ """Campaign Validate Pins — validate all version pins in a campaign state file.
6
+
7
+ Campaign-specific wrapper around the shared skf-validate-pins.py module.
8
+ Reads the campaign state and brief, validates every skill's pin against
9
+ real GitHub releases/tags, and outputs consolidated JSON.
10
+
11
+ CLI:
12
+ uv run src/skf-campaign/scripts/campaign-validate-pins.py \
13
+ --state-file <path> --brief-file <path>
14
+
15
+ Input:
16
+ --state-file Path to _campaign-state.yaml
17
+ --brief-file Path to campaign-brief.yaml
18
+
19
+ Output (JSON on stdout):
20
+ {
21
+ "results": [
22
+ {
23
+ "name": "skill-name",
24
+ "status": "valid|invalid|resolved",
25
+ "pin": "input-pin-or-null",
26
+ "resolved_ref": "actual-tag-or-branch",
27
+ "ref_type": "tag|branch|null",
28
+ "version": "semver-or-null",
29
+ "suggestions": []
30
+ }
31
+ ],
32
+ "all_valid": true,
33
+ "invalid_count": 0,
34
+ "resolved_count": 0
35
+ }
36
+
37
+ Exit codes:
38
+ 0 all valid/resolved
39
+ 1 one or more invalid pins
40
+ 2 error (missing files, bad YAML, gh unavailable)
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ import argparse
46
+ import importlib.util
47
+ import json
48
+ import sys
49
+ from pathlib import Path
50
+ from typing import Any, Dict, List
51
+
52
+ import yaml
53
+
54
+ SHARED_SCRIPTS = Path(__file__).parent.parent.parent / "shared" / "scripts"
55
+ VALIDATE_PINS_PATH = SHARED_SCRIPTS / "skf-validate-pins.py"
56
+
57
+
58
+ def _load_validate_pin():
59
+ spec = importlib.util.spec_from_file_location("skf_validate_pins", VALIDATE_PINS_PATH)
60
+ mod = importlib.util.module_from_spec(spec)
61
+ spec.loader.exec_module(mod)
62
+ return mod.validate_pin
63
+
64
+
65
+ def _emit_error(message: str, code: str) -> None:
66
+ json.dump({"error": message, "code": code}, sys.stderr)
67
+ sys.stderr.write("\n")
68
+
69
+
70
+ def _load_yaml(path: Path) -> Any:
71
+ with open(path, encoding="utf-8") as f:
72
+ return yaml.safe_load(f)
73
+
74
+
75
+ def run(state_file: str, brief_file: str) -> int:
76
+ state_path = Path(state_file)
77
+ brief_path = Path(brief_file)
78
+
79
+ if not state_path.is_file():
80
+ _emit_error(f"State file not found: {state_file}", "STATE_NOT_FOUND")
81
+ return 2
82
+
83
+ if not brief_path.is_file():
84
+ _emit_error(f"Brief file not found: {brief_file}", "BRIEF_NOT_FOUND")
85
+ return 2
86
+
87
+ try:
88
+ state = _load_yaml(state_path)
89
+ except Exception as exc:
90
+ _emit_error(f"Failed to parse state file: {exc}", "STATE_PARSE_ERROR")
91
+ return 2
92
+
93
+ try:
94
+ brief = _load_yaml(brief_path)
95
+ except Exception as exc:
96
+ _emit_error(f"Failed to parse brief file: {exc}", "BRIEF_PARSE_ERROR")
97
+ return 2
98
+
99
+ skills = state.get("skills", [])
100
+ if not isinstance(skills, list):
101
+ _emit_error("State file 'skills' is not an array", "INVALID_STATE")
102
+ return 2
103
+
104
+ targets = brief.get("targets", [])
105
+ if not isinstance(targets, list):
106
+ _emit_error("Brief file 'targets' is not an array", "INVALID_BRIEF")
107
+ return 2
108
+
109
+ name_to_repo: Dict[str, str] = {}
110
+ for target in targets:
111
+ name_to_repo[target["name"]] = target["repo_url"]
112
+
113
+ if not VALIDATE_PINS_PATH.is_file():
114
+ _emit_error(
115
+ f"Shared module not found: {VALIDATE_PINS_PATH.as_posix()}",
116
+ "SHARED_MODULE_NOT_FOUND",
117
+ )
118
+ return 2
119
+
120
+ validate_pin = _load_validate_pin()
121
+
122
+ results: List[Dict[str, Any]] = []
123
+ invalid_count = 0
124
+ resolved_count = 0
125
+
126
+ for skill in skills:
127
+ skill_name = skill["name"]
128
+ repo_url = name_to_repo.get(skill_name)
129
+ if repo_url is None:
130
+ _emit_error(
131
+ f"Skill '{skill_name}' not found in brief targets",
132
+ "SKILL_NOT_IN_BRIEF",
133
+ )
134
+ return 2
135
+
136
+ pin = skill.get("pin")
137
+ result = validate_pin(repo_url, pin=pin)
138
+ result["name"] = skill_name
139
+
140
+ if result["status"] == "invalid":
141
+ invalid_count += 1
142
+ elif result["status"] == "resolved":
143
+ resolved_count += 1
144
+
145
+ results.append(result)
146
+
147
+ all_valid = invalid_count == 0
148
+ output = {
149
+ "results": results,
150
+ "all_valid": all_valid,
151
+ "invalid_count": invalid_count,
152
+ "resolved_count": resolved_count,
153
+ }
154
+
155
+ json.dump(output, sys.stdout, separators=(",", ":"))
156
+ sys.stdout.write("\n")
157
+
158
+ if not all_valid:
159
+ return 1
160
+ return 0
161
+
162
+
163
+ def main() -> int:
164
+ parser = argparse.ArgumentParser(
165
+ description="Validate all version pins in a campaign state file.",
166
+ )
167
+ parser.add_argument("--state-file", required=True, help="Path to _campaign-state.yaml")
168
+ parser.add_argument("--brief-file", required=True, help="Path to campaign-brief.yaml")
169
+ args = parser.parse_args()
170
+ return run(args.state_file, args.brief_file)
171
+
172
+
173
+ if __name__ == "__main__":
174
+ raise SystemExit(main())
@@ -0,0 +1,207 @@
1
+ # /// script
2
+ # requires-python = ">=3.9"
3
+ # dependencies = ["pyyaml", "jsonschema>=4.0"]
4
+ # ///
5
+ """Campaign Validate State — schema check for _campaign-state.yaml on disk.
6
+
7
+ Replaces the per-step "mentally validate the loaded state against the schema"
8
+ prose with a deterministic check. Every campaign step that loads state runs
9
+ this once on entry instead of asking the LLM to validate a draft-07 schema
10
+ (nested objects, enums, additionalProperties:false, date-time) by hand — the
11
+ exact check an LLM does unreliably and which, when wrong, silently corrupts a
12
+ multi-session campaign.
13
+
14
+ Loads the campaign state YAML, validates it against
15
+ `assets/campaign-state-schema.json` (resolved relative to this script unless
16
+ `--schema-file` overrides), and emits skill-friendly error records the calling
17
+ step can forward verbatim.
18
+
19
+ CLI:
20
+ uv run campaign-validate-state.py --state-file <path>
21
+ uv run campaign-validate-state.py --state-file <path> --schema-file <path>
22
+
23
+ Output (JSON on stdout):
24
+ {
25
+ "valid": bool,
26
+ "errors": [{"field": "campaign.current_stage", "message": "..."}, ...],
27
+ "halt_reason": "state-missing" | "state-malformed" | "state-invalid" | null
28
+ }
29
+
30
+ Exit codes:
31
+ 0 valid (errors empty)
32
+ 1 invalid (schema violations) OR file/yaml load failed
33
+ 2 configuration error (schema file missing or unreadable)
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import argparse
39
+ import datetime
40
+ import json
41
+ import sys
42
+ from pathlib import Path
43
+ from typing import Any
44
+
45
+ import yaml
46
+ from jsonschema import Draft7Validator
47
+
48
+ DEFAULT_SCHEMA_PATH = Path(__file__).resolve().parent.parent / "assets" / "campaign-state-schema.json"
49
+
50
+
51
+ def _emit(envelope: dict) -> None:
52
+ json.dump(envelope, sys.stdout, separators=(",", ":"), default=str)
53
+ sys.stdout.write("\n")
54
+
55
+
56
+ def _load_yaml_text(text: str) -> tuple[Any, str | None]:
57
+ try:
58
+ data = yaml.safe_load(text)
59
+ except yaml.YAMLError as exc:
60
+ return None, f"State is not valid YAML: {exc}"
61
+ if data is None:
62
+ return None, "State file is empty"
63
+ if not isinstance(data, dict):
64
+ return None, f"State root must be a YAML mapping; got {type(data).__name__}"
65
+ return data, None
66
+
67
+
68
+ def _field_path(error_path) -> str:
69
+ parts: list[str] = []
70
+ for p in error_path:
71
+ if isinstance(p, int):
72
+ parts.append(f"[{p}]")
73
+ else:
74
+ parts.append(f".{p}" if parts else str(p))
75
+ return "".join(parts) or "(root)"
76
+
77
+
78
+ def _translate(err) -> dict:
79
+ field = _field_path(err.absolute_path)
80
+ validator = err.validator
81
+ inst = err.instance
82
+
83
+ if validator == "required":
84
+ missing = err.message.split("'")[1] if "'" in err.message else "(unknown)"
85
+ return {
86
+ "field": missing,
87
+ "message": f"State validation failed: missing required field `{missing}`.",
88
+ }
89
+ if validator == "enum":
90
+ return {
91
+ "field": field,
92
+ "message": (
93
+ f"State validation failed: `{field}` value `{inst}` is not one of "
94
+ f"{err.validator_value}."
95
+ ),
96
+ }
97
+ if validator == "type":
98
+ if isinstance(inst, datetime.date) and "string" in str(err.validator_value):
99
+ return {
100
+ "field": field,
101
+ "message": (
102
+ f"State validation failed: `{field}` parsed as a YAML date, not a "
103
+ f"string. Quote it (e.g. `'2026-05-01T00:00:00Z'`)."
104
+ ),
105
+ }
106
+ return {
107
+ "field": field,
108
+ "message": (
109
+ f"State validation failed: `{field}` has type `{type(inst).__name__}`, "
110
+ f"expected `{err.validator_value}`."
111
+ ),
112
+ }
113
+ if validator == "additionalProperties":
114
+ return {
115
+ "field": field,
116
+ "message": f"State validation failed: `{field}` — {err.message}",
117
+ }
118
+ if validator in ("minimum", "maximum"):
119
+ return {
120
+ "field": field,
121
+ "message": f"State validation failed: `{field}` value `{inst}` violates {validator} `{err.validator_value}`.",
122
+ }
123
+ return {
124
+ "field": field,
125
+ "message": f"State validation failed: `{field}` — {err.message}",
126
+ }
127
+
128
+
129
+ def validate_state(state: dict, schema: dict) -> dict:
130
+ validator = Draft7Validator(schema)
131
+ errors = [
132
+ _translate(err)
133
+ for err in sorted(validator.iter_errors(state), key=lambda e: list(e.absolute_path))
134
+ ]
135
+ return {"valid": not errors, "errors": errors}
136
+
137
+
138
+ def run(state_file: str, schema_file: str | None = None) -> int:
139
+ schema_path = Path(schema_file) if schema_file else DEFAULT_SCHEMA_PATH
140
+ if not schema_path.is_file():
141
+ _emit(
142
+ {
143
+ "valid": False,
144
+ "errors": [{"field": "(schema)", "message": f"Schema not found at `{schema_path}`."}],
145
+ "halt_reason": "state-invalid",
146
+ }
147
+ )
148
+ return 2
149
+ try:
150
+ schema = json.loads(schema_path.read_text(encoding="utf-8"))
151
+ except (OSError, ValueError) as exc:
152
+ _emit(
153
+ {
154
+ "valid": False,
155
+ "errors": [{"field": "(schema)", "message": f"Schema unreadable: {exc}"}],
156
+ "halt_reason": "state-invalid",
157
+ }
158
+ )
159
+ return 2
160
+
161
+ state_path = Path(state_file)
162
+ if not state_path.is_file():
163
+ _emit(
164
+ {
165
+ "valid": False,
166
+ "errors": [{"field": "(file)", "message": f"State not found at `{state_path}`."}],
167
+ "halt_reason": "state-missing",
168
+ }
169
+ )
170
+ return 1
171
+
172
+ state, load_err = _load_yaml_text(state_path.read_text(encoding="utf-8"))
173
+ if load_err is not None:
174
+ _emit(
175
+ {
176
+ "valid": False,
177
+ "errors": [{"field": "(file)", "message": load_err}],
178
+ "halt_reason": "state-malformed",
179
+ }
180
+ )
181
+ return 1
182
+
183
+ result = validate_state(state, schema)
184
+ if not result["valid"]:
185
+ _emit({**result, "halt_reason": "state-invalid"})
186
+ return 1
187
+
188
+ _emit({**result, "halt_reason": None})
189
+ return 0
190
+
191
+
192
+ def main(argv: list[str] | None = None) -> int:
193
+ parser = argparse.ArgumentParser(
194
+ prog="campaign-validate-state",
195
+ description="Validate _campaign-state.yaml against the campaign state schema.",
196
+ )
197
+ parser.add_argument("--state-file", required=True, help="Path to _campaign-state.yaml")
198
+ parser.add_argument(
199
+ "--schema-file",
200
+ help="Path to campaign-state-schema.json (defaults to the bundled schema)",
201
+ )
202
+ args = parser.parse_args(argv)
203
+ return run(args.state_file, args.schema_file)
204
+
205
+
206
+ if __name__ == "__main__":
207
+ raise SystemExit(main())
@@ -0,0 +1,34 @@
1
+ # Machine-generated campaign brief — resume context for fresh sessions
2
+ # Populated by step-01-setup.md from operator inputs
3
+
4
+ # Campaign identifier
5
+ campaign_name: ""
6
+
7
+ # ISO-8601 timestamp of campaign creation
8
+ created_at: ""
9
+
10
+ # Target skills for this campaign run
11
+ targets: []
12
+ # Each target entry:
13
+ # name: ""
14
+ # repo_url: ""
15
+ # tier: "A" # A (full pipeline) or B (batch)
16
+ # pin: null # version pin, or null for latest
17
+ # depends_on: [] # list of skill names this target depends on
18
+
19
+ # Quality thresholds applied to all skills in this campaign
20
+ quality_gate:
21
+ hard: "zero-critical-high"
22
+ soft_target: 90
23
+ soft_fallback: 80
24
+
25
+ # Where health findings are routed: "local" (project-local) or "improvement" (shared queue)
26
+ health_findings_queue: "local"
27
+
28
+ # Optional path to the architecture document the verify (Stage 7) and refine
29
+ # (Stage 8) stages consume. Empty string means those stages discover it at
30
+ # runtime (docs/architecture.md, then _bmad-output/planning-artifacts/architecture.md).
31
+ architecture_doc_path: ""
32
+
33
+ # Operator-provided context or directives for this campaign
34
+ notes: ""
@@ -0,0 +1,54 @@
1
+ # Campaign Report — {{campaign_name}}
2
+
3
+ ## Campaign Summary
4
+
5
+ | Field | Value |
6
+ |-------|-------|
7
+ | **Campaign** | {{campaign_name}} |
8
+ | **Started** | {{started_at}} |
9
+ | **Completed** | {{completed_at}} |
10
+ | **Duration** | {{duration}} |
11
+ | **Quality Gate (Hard)** | {{quality_gate_hard}} |
12
+ | **Quality Gate (Soft Target)** | {{quality_gate_soft_target}} |
13
+ | **Quality Gate (Soft Fallback)** | {{quality_gate_soft_fallback}} |
14
+ | **Skills Completed** | {{skills_completed}} |
15
+ | **Skills Failed** | {{skills_failed}} |
16
+ | **Skills Skipped** | {{skills_skipped}} |
17
+
18
+ ## Skills Overview
19
+
20
+ | Name | Tier | Status | Quality Score | Pin | Workarounds |
21
+ |------|------|--------|---------------|-----|-------------|
22
+ {{skills_table}}
23
+
24
+ ## Quality Scores
25
+
26
+ | Metric | Value |
27
+ |--------|-------|
28
+ | **Minimum** | {{quality_min}} |
29
+ | **Maximum** | {{quality_max}} |
30
+ | **Average** | {{quality_avg}} |
31
+
32
+ ### Per-Skill Breakdown
33
+
34
+ {{quality_breakdown}}
35
+
36
+ ## Findings Summary
37
+
38
+ - **Total workarounds applied:** {{total_workarounds}}
39
+ - **Skills with workarounds:** {{skills_with_workarounds}}
40
+ - **Doc-rot corrections:** tracked per-skill in health-check findings (not aggregated in campaign state)
41
+
42
+ ## Workarounds Applied
43
+
44
+ {{workarounds_list}}
45
+
46
+ ## Duration Breakdown
47
+
48
+ | Skill | Started | Completed | Duration |
49
+ |-------|---------|-----------|----------|
50
+ {{duration_table}}
51
+
52
+ ## Failed / Skipped Skills
53
+
54
+ {{failed_skipped_section}}
@@ -0,0 +1,48 @@
1
+ # Skill Kickoff — {{skill_name}}
2
+
3
+ ## Campaign Context
4
+
5
+ - **Campaign:** {{campaign_name}}
6
+ - **Current Stage:** {{current_stage}}
7
+ - **Quality Gate:** {{quality_gate_summary}}
8
+
9
+ ## Skill Identity
10
+
11
+ - **Skill:** {{skill_name}}
12
+ - **Tier:** {{skill_tier}}
13
+ - **Repository:** {{repo_url}}
14
+ - **Pin:** {{pin}}
15
+ - **Commit:** {{commit_sha}}
16
+
17
+ ## Brief Summary
18
+
19
+ {{brief_summary}}
20
+
21
+ ## Campaign Facts
22
+
23
+ {{persistent_facts}}
24
+
25
+ ## Dependency State
26
+
27
+ {{dependency_status_table}}
28
+
29
+ ## Standing Directive
30
+
31
+ {{directive_content}}
32
+
33
+ ## Workarounds Applied
34
+
35
+ {{workarounds_list}}
36
+
37
+ ## Pipeline Instructions
38
+
39
+ Execute the standard forge pipeline for **{{skill_name}}**:
40
+
41
+ 1. **AN** (Analyze) — scope and source intelligence
42
+ 2. **BS** (Brief Synthesis) — generate or validate the skill brief
43
+ 3. **CS** (Compile Skill) — compile the SKILL.md artifact
44
+ 4. **TS** (Test Skill) — run health-check validation
45
+
46
+ **Parameters:**
47
+ - Pin: {{pin}}
48
+ - Quality target: {{quality_gate_summary}}