bmad-module-skill-forge 1.8.0 → 2.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 (136) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +10 -5
  3. package/docs/_data/pinned.yaml +2 -2
  4. package/docs/agents.md +11 -2
  5. package/docs/architecture.md +5 -4
  6. package/docs/bmad-synergy.md +42 -3
  7. package/docs/campaign.md +172 -0
  8. package/docs/examples.md +7 -3
  9. package/docs/forge-auto.md +90 -0
  10. package/docs/getting-started.md +9 -0
  11. package/docs/how-it-works.md +6 -4
  12. package/docs/index.md +4 -3
  13. package/docs/skill-model.md +1 -1
  14. package/docs/troubleshooting.md +17 -1
  15. package/docs/verifying-a-skill.md +9 -3
  16. package/docs/why-skf.md +1 -1
  17. package/docs/workflows.md +73 -27
  18. package/package.json +2 -2
  19. package/src/module-help.csv +2 -1
  20. package/src/shared/_known-workarounds.yaml +155 -0
  21. package/src/shared/data/language-corpora.json +32 -0
  22. package/src/shared/references/pipeline-contracts.md +14 -2
  23. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +6 -1
  24. package/src/shared/scripts/schemas/skill-brief.v1.json +6 -1
  25. package/src/shared/scripts/skf-derive-assembly-shape.py +107 -0
  26. package/src/shared/scripts/skf-detect-docs.py +417 -0
  27. package/src/shared/scripts/skf-detect-language.py +5 -3
  28. package/src/shared/scripts/skf-emit-brief-result-envelope.py +12 -1
  29. package/src/shared/scripts/skf-emit-result-envelope.py +17 -0
  30. package/src/shared/scripts/skf-extract-public-api.py +53 -0
  31. package/src/shared/scripts/skf-language-corpora.py +100 -0
  32. package/src/shared/scripts/skf-merge-doc-urls.py +244 -0
  33. package/src/shared/scripts/skf-preapply.py +192 -0
  34. package/src/shared/scripts/skf-shape-detect.py +1114 -0
  35. package/src/shared/scripts/skf-validate-brief-schema.py +2 -5
  36. package/src/shared/scripts/skf-validate-pins.py +368 -0
  37. package/src/shared/scripts/skf-write-skill-brief.py +21 -5
  38. package/src/skf-analyze-source/SKILL.md +26 -20
  39. package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
  40. package/src/skf-analyze-source/references/init.md +30 -0
  41. package/src/skf-analyze-source/references/step-auto-scope.md +677 -0
  42. package/src/skf-analyze-source/references/step-shape-detect.md +86 -0
  43. package/src/skf-audit-skill/SKILL.md +1 -0
  44. package/src/skf-audit-skill/assets/drift-report-template.md +6 -0
  45. package/src/skf-audit-skill/references/init.md +1 -1
  46. package/src/skf-audit-skill/references/report.md +4 -0
  47. package/src/skf-audit-skill/references/severity-classify.md +1 -1
  48. package/src/skf-audit-skill/references/step-doc-drift.md +147 -0
  49. package/src/skf-brief-skill/SKILL.md +7 -3
  50. package/src/skf-brief-skill/assets/skill-brief-schema.md +1 -1
  51. package/src/skf-brief-skill/references/analyze-target.md +7 -4
  52. package/src/skf-brief-skill/references/confirm-brief.md +0 -1
  53. package/src/skf-brief-skill/references/gather-intent.md +22 -2
  54. package/src/skf-brief-skill/references/scope-definition.md +2 -1
  55. package/src/skf-brief-skill/references/step-auto-brief.md +188 -0
  56. package/src/skf-brief-skill/references/step-auto-validate.md +209 -0
  57. package/src/skf-brief-skill/references/write-brief.md +2 -3
  58. package/src/skf-campaign/SKILL.md +190 -0
  59. package/src/skf-campaign/assets/campaign-state-schema.json +191 -0
  60. package/src/skf-campaign/customize.toml +73 -0
  61. package/src/skf-campaign/manifest.yaml +11 -0
  62. package/src/skf-campaign/references/campaign-directive-spec.md +121 -0
  63. package/src/skf-campaign/references/health-check.md +35 -0
  64. package/src/skf-campaign/references/step-01-setup.md +122 -0
  65. package/src/skf-campaign/references/step-02-strategy.md +97 -0
  66. package/src/skf-campaign/references/step-03-pins.md +56 -0
  67. package/src/skf-campaign/references/step-04-provenance.md +63 -0
  68. package/src/skf-campaign/references/step-05-skill-loop.md +103 -0
  69. package/src/skf-campaign/references/step-06-batch.md +87 -0
  70. package/src/skf-campaign/references/step-07-capstone.md +63 -0
  71. package/src/skf-campaign/references/step-08-verify.md +75 -0
  72. package/src/skf-campaign/references/step-09-refine.md +83 -0
  73. package/src/skf-campaign/references/step-10-export.md +106 -0
  74. package/src/skf-campaign/references/step-11-maintenance.md +84 -0
  75. package/src/skf-campaign/references/step-resume.md +114 -0
  76. package/src/skf-campaign/scripts/.gitkeep +0 -0
  77. package/src/skf-campaign/scripts/campaign-deps.py +235 -0
  78. package/src/skf-campaign/scripts/campaign-parse-manifest.py +119 -0
  79. package/src/skf-campaign/scripts/campaign-provenance.py +247 -0
  80. package/src/skf-campaign/scripts/campaign-render-kickoff.py +158 -0
  81. package/src/skf-campaign/scripts/campaign-report.py +249 -0
  82. package/src/skf-campaign/scripts/campaign-validate-pins.py +174 -0
  83. package/src/skf-campaign/scripts/campaign-validate-state.py +207 -0
  84. package/src/skf-campaign/templates/campaign-brief-template.yaml +34 -0
  85. package/src/skf-campaign/templates/campaign-report-template.md +54 -0
  86. package/src/skf-campaign/templates/kickoff-template.md +48 -0
  87. package/src/skf-create-skill/SKILL.md +4 -1
  88. package/src/skf-create-skill/assets/compile-assembly-rules.md +22 -0
  89. package/src/skf-create-skill/assets/skill-sections.md +2 -0
  90. package/src/skf-create-skill/references/compile.md +5 -2
  91. package/src/skf-create-skill/references/extract.md +9 -1
  92. package/src/skf-create-skill/references/extraction-patterns.md +3 -1
  93. package/src/skf-create-skill/references/generate-artifacts.md +11 -2
  94. package/src/skf-create-skill/references/health-check.md +2 -2
  95. package/src/skf-create-skill/references/report.md +17 -1
  96. package/src/skf-create-skill/references/source-resolution-protocols.md +1 -1
  97. package/src/skf-create-skill/references/step-auto-shard.md +110 -0
  98. package/src/skf-create-skill/references/step-doc-rot.md +109 -0
  99. package/src/skf-create-skill/references/step-doc-sources.md +123 -0
  100. package/src/skf-create-skill/references/sub/fetch-docs.md +29 -0
  101. package/src/skf-create-skill/references/validate.md +12 -3
  102. package/src/skf-drop-skill/SKILL.md +2 -2
  103. package/src/skf-drop-skill/references/execute.md +20 -9
  104. package/src/skf-drop-skill/references/report.md +2 -0
  105. package/src/skf-drop-skill/references/select.md +7 -2
  106. package/src/skf-forger/SKILL.md +15 -4
  107. package/src/skf-refine-architecture/SKILL.md +2 -1
  108. package/src/skf-refine-architecture/references/compile.md +1 -1
  109. package/src/skf-refine-architecture/references/report.md +1 -1
  110. package/src/skf-rename-skill/SKILL.md +2 -2
  111. package/src/skf-rename-skill/references/execute.md +5 -4
  112. package/src/skf-rename-skill/references/rebuild-context.md +2 -2
  113. package/src/skf-rename-skill/references/select.md +3 -3
  114. package/src/skf-setup/SKILL.md +1 -1
  115. package/src/skf-setup/references/auto-index.md +3 -1
  116. package/src/skf-setup/references/detect-and-tier.md +4 -0
  117. package/src/skf-setup/references/report.md +4 -1
  118. package/src/skf-setup/references/tier-rules.md +1 -1
  119. package/src/skf-test-skill/SKILL.md +8 -7
  120. package/src/skf-test-skill/customize.toml +2 -1
  121. package/src/skf-test-skill/references/coverage-check.md +10 -0
  122. package/src/skf-test-skill/references/external-validators.md +1 -1
  123. package/src/skf-test-skill/references/init.md +16 -1
  124. package/src/skf-test-skill/references/report.md +5 -1
  125. package/src/skf-test-skill/references/score.md +95 -6
  126. package/src/skf-test-skill/references/source-access-protocol.md +13 -3
  127. package/src/skf-test-skill/references/step-hard-gate.md +73 -0
  128. package/src/skf-test-skill/scripts/compute-score.py +4 -3
  129. package/src/skf-test-skill/templates/test-report-template.md +1 -0
  130. package/src/skf-update-skill/customize.toml +0 -9
  131. package/src/skf-update-skill/references/detect-changes.md +33 -3
  132. package/src/skf-update-skill/references/health-check.md +2 -2
  133. package/src/skf-update-skill/references/init.md +1 -0
  134. package/src/skf-update-skill/references/re-extract.md +15 -1
  135. package/src/skf-verify-stack/references/report.md +1 -1
  136. package/tools/cli/lib/ui.js +3 -2
@@ -0,0 +1,188 @@
1
+ ---
2
+ nextStepFile: 'step-auto-validate.md'
3
+ validateBriefSchemaProbeOrder:
4
+ - '{project-root}/_bmad/skf/shared/scripts/skf-validate-brief-schema.py'
5
+ - '{project-root}/src/shared/scripts/skf-validate-brief-schema.py'
6
+ writeSkillBriefProbeOrder:
7
+ - '{project-root}/_bmad/skf/shared/scripts/skf-write-skill-brief.py'
8
+ - '{project-root}/src/shared/scripts/skf-write-skill-brief.py'
9
+ emitBriefEnvelopeProbeOrder:
10
+ - '{project-root}/_bmad/skf/shared/scripts/skf-emit-brief-result-envelope.py'
11
+ - '{project-root}/src/shared/scripts/skf-emit-brief-result-envelope.py'
12
+ mergeDocUrlsProbeOrder:
13
+ - '{project-root}/_bmad/skf/shared/scripts/skf-merge-doc-urls.py'
14
+ - '{project-root}/src/shared/scripts/skf-merge-doc-urls.py'
15
+ detectDocsProbeOrder:
16
+ - '{project-root}/_bmad/skf/shared/scripts/skf-detect-docs.py'
17
+ - '{project-root}/src/shared/scripts/skf-detect-docs.py'
18
+ ---
19
+
20
+ <!-- Config: communicate in {communication_language}. -->
21
+
22
+ # Step 1a: Auto-Brief Generation
23
+
24
+ ## STEP GOAL:
25
+
26
+ To enrich an upstream skill brief (produced by AN auto-scope) with documentation URLs discovered via `skf-detect-docs.py`, validate the enriched brief, and write it through the canonical writer. Envelope emission is deferred to step-auto-validate.md, which presents the brief for user approval before continuing. This step replaces the interactive gather-intent → analyze-target → scope-definition → confirm-brief → write-brief chain when `[auto]` mode is active.
27
+
28
+ ## Rules
29
+
30
+ - Auto-proceed step — no user interaction required
31
+ - This step is conditional — only loaded when `[auto]` flag is present in the pipeline context
32
+ - Must produce the same output artifact as the interactive chain: a validated `skill-brief.yaml`
33
+ - Doc detection is best-effort — failures do not halt the pipeline
34
+ - Do NOT re-derive scope fields from the upstream brief — AN already set them correctly
35
+ - Do NOT render YAML or JSON envelopes in the LLM — delegate to deterministic scripts
36
+
37
+ ## MANDATORY SEQUENCE
38
+
39
+ ### 1. Load Upstream Brief
40
+
41
+ Read the upstream brief path from `{brief_path}` (passed by the forger from AN's `SKF_ANALYZE_RESULT_JSON` `brief_paths[]`).
42
+
43
+ **IF `{brief_path}` is not set or the file does not exist:**
44
+ - HARD HALT with exit code 2 (`input-missing`): "**Auto-brief requires an upstream brief — `brief_path` is missing or the file does not exist at `{brief_path}`.**"
45
+ - Emit error envelope per §6 with `halt_reason: "input-missing"`.
46
+
47
+ **Resolve `{validateBriefSchemaHelper}`** from `{validateBriefSchemaProbeOrder}`; first existing path wins. HALT if no candidate exists.
48
+
49
+ Validate the upstream brief against the schema:
50
+
51
+ ```bash
52
+ uv run {validateBriefSchemaHelper} {brief_path}
53
+ ```
54
+
55
+ The script returns JSON `{valid, errors[], warnings[], halt_reason, brief}`.
56
+
57
+ - **`valid: false`** — the upstream brief is malformed. HARD HALT with exit code 2 (`input-invalid`): "**Upstream brief at `{brief_path}` is invalid: {first error message}.**" Emit error envelope per §6 with `halt_reason: "input-invalid"`.
58
+ - **`valid: true`** — proceed with the parsed `brief` payload. Surface any non-empty `warnings[]` to the log.
59
+
60
+ Extract from the parsed brief:
61
+ - `skill_name` ← `brief.name`
62
+ - `version` ← `brief.version`
63
+ - `source_repo` ← `brief.source_repo`
64
+ - `language` ← `brief.language`
65
+ - `scope_type` ← `brief.scope.type`
66
+ - `forge_tier` ← `brief.forge_tier`
67
+ - `description` ← `brief.description`
68
+ - `created` ← `brief.created`
69
+ - `created_by` ← `brief.created_by`
70
+ - All scope fields: `scope.include`, `scope.exclude`, `scope.notes`, `scope.rationale`, `scope.amendments`, `scope.tier_a_include`
71
+ - Optional fields: `source_type`, `source_authority`, `doc_urls`, `target_version`, `target_ref`, `source_ref`, `scripts_intent`, `assets_intent`
72
+
73
+ **Docs-only check:** If `source_type` is `docs-only` in the parsed brief, skip §2 (Run Doc Detection) and §3 (Enrich Brief with Detected Docs) — the doc URL is already in the brief's `doc_urls`. Log: "Docs-only brief — skipping repo-based doc detection. Doc URLs provided by upstream." Proceed directly to §4 (Validate Enriched Brief). All brief fields (`source_type`, `source_authority`, `doc_urls`, `scope_type`) must pass through unmodified.
74
+
75
+ ### 2. Run Doc Detection
76
+
77
+ **Resolve `{detectDocsHelper}`** from `{detectDocsProbeOrder}`; first existing path wins. HALT if no candidate exists.
78
+
79
+ Invoke doc detection to discover documentation URLs for the source repo:
80
+
81
+ ```bash
82
+ uv run {detectDocsHelper} --repo-url {source_repo}
83
+ ```
84
+
85
+ `--repo-url` is always required (the script uses it for GitHub API calls). If a local clone is also available at `{local_clone_path}`, add `--local-path {local_clone_path}` to enable docs-folder scanning in addition to API-based detection.
86
+
87
+ **Handle exit codes:**
88
+
89
+ - **Exit 0 (found docs):** Parse the JSON output array. Each entry has `{url, detected_via, content_hash, content_type}`. Proceed to §3 with the detected docs.
90
+ - **Exit 1 (none found):** Log: "No external documentation found — brief generated from source analysis only." Proceed to §4 with no doc enrichment.
91
+ - **Exit 2 (error):** Log warning: "Doc detection failed — proceeding without doc enrichment." Do NOT halt — doc enrichment is best-effort. Proceed to §4 with no doc enrichment.
92
+
93
+ ### 3. Enrich Brief with Detected Docs
94
+
95
+ For each detected doc entry, create a brief `doc_urls` entry:
96
+
97
+ - `url` ← `url` (direct copy)
98
+ - `label` ← derive from `content_type` if available:
99
+ - `"api-docs"` → `"API Documentation"`
100
+ - `"guide"` → `"Guide"`
101
+ - `"reference"` → `"Reference"`
102
+ - Otherwise derive from `detected_via`:
103
+ - `"homepageUrl"` → `"Homepage"`
104
+ - `"readme_link"` → `"README Link"`
105
+ - `"pages_api"` → `"GitHub Pages"`
106
+ - `"docs_folder"` → `"Docs Folder"`
107
+ - `source` ← coarse provenance derived from `detected_via` (per the `skill-brief.v1.json` `doc_urls[].source` enum): `homepageUrl` → `homepage`, `readme_link` → `readme-detection`, `pages_api` → `pages-api`, `docs_folder` → `docs-folder`. This marks the entry as opportunistically detected, distinct from a registry-guaranteed corpus.
108
+
109
+ **Merge via the canonical helper.** Resolve `{mergeDocUrlsHelper}` from `{mergeDocUrlsProbeOrder}` (first existing path wins; HALT if neither exists). Pass the upstream brief's `doc_urls` as `existing` and the entries just mapped above as `detected`:
110
+
111
+ ```bash
112
+ echo '{"scope_type": "{scope_type}", "existing": {upstream brief doc_urls JSON, [] if none}, "detected": {mapped detected entries JSON}}' \
113
+ | uv run {mergeDocUrlsHelper}
114
+ ```
115
+
116
+ The helper returns `{"doc_urls": [...], "suppressed": [...]}`. It deduplicates by **normalized** URL (lowercase host, strip a trailing `/index.html` and any trailing `/`), so a seeded `…/book/` and a README's `…/book/index.html` collapse to one entry; existing/corpora-seeded entries always win and keep their `source: language-registry`, so the registry-vs-detected distinction survives the merge. For a **whole-language reference** (`scope_type == "full-library"` AND ≥1 `existing` entry has `source: language-registry`) it additionally suppresses README noise on a corpus host: non-corpus path segments (`/whatsnew/`, `/contribute`, `/wiki/`) and non-primary-locale duplicates of a kept page (`/ja/master/` when `/en/master/` is kept). Ordinary skills (any other `scope_type`, or no registry corpora) pass through with dedup only — no suppression. Use the returned `doc_urls` as the brief's merged list.
117
+
118
+ **Log suppressed entries.** When `suppressed` is non-empty, log one line per entry — `"info: suppressed {url} ({reason})"` — so the operator can see what the whole-language noise filter dropped (never drop silently). The N==0 DEGRADED case (a whole-language repo whose registry returned no corpora) carries no `language-registry` entry, so suppression stays inactive and its README docs are kept — this is intentional (there is no canonical corpus host to filter against).
119
+
120
+ ### 4. Validate Enriched Brief
121
+
122
+ Assemble the enriched brief context as a flat JSON object following the write-brief §3 contract:
123
+
124
+ ```json
125
+ {
126
+ "name": "{skill_name}",
127
+ "target_version": "{target_version or null}",
128
+ "detected_version": null,
129
+ "source_type": "{source_type or 'source'}",
130
+ "source_repo": "{source_repo}",
131
+ "language": "{language}",
132
+ "description": "{description}",
133
+ "forge_tier": "{forge_tier}",
134
+ "created": "{created}",
135
+ "created_by": "{created_by}",
136
+ "scope_type": "{scope_type}",
137
+ "scope_include": ["{scope.include patterns}"],
138
+ "scope_exclude": ["{scope.exclude patterns}"],
139
+ "scope_notes": "{scope.notes or ''}",
140
+ "scope_rationale": null,
141
+ "scope_tier_a_include": null,
142
+ "scope_amendments": null,
143
+ "doc_urls": [{"url": "...", "label": "...", "source": "..."}],
144
+ "scripts_intent": "{scripts_intent or null}",
145
+ "assets_intent": "{assets_intent or null}",
146
+ "source_authority": "{source_authority or null}",
147
+ "target_ref": "{target_ref or null}",
148
+ "source_ref": "{source_ref or null}",
149
+ "version_resolved": "{version}"
150
+ }
151
+ ```
152
+
153
+ The `version_resolved` key pins the output to the upstream brief's version — without it, the writer's precedence logic falls through to `1.0.0` since `target_version` and `detected_version` are both null on the auto path.
154
+
155
+ ### 5. Write Enriched Brief
156
+
157
+ **Resolve `{writeSkillBriefHelper}`** from `{writeSkillBriefProbeOrder}`; first existing path wins. HALT if no candidate exists.
158
+
159
+ Write the enriched brief through the canonical writer:
160
+
161
+ ```bash
162
+ echo '<context-json>' | uv run {writeSkillBriefHelper} write --target {forge_data_folder}/{skill_name}/skill-brief.yaml --from-flat
163
+ ```
164
+
165
+ **On script failure (non-zero exit):**
166
+ - Exit 1 (validation/invariant): Emit error envelope per §6 with `halt_reason: "input-invalid"`, then HARD HALT.
167
+ - Exit 2 (I/O failure): Emit error envelope per §6 with `halt_reason: "write-failed"`, then HARD HALT.
168
+
169
+ **On success:** Capture `brief_path` and `version` from the response envelope for step-auto-validate's envelope emission.
170
+
171
+ ### 6. Error Envelope (Canonical)
172
+
173
+ Every HARD HALT in this step emits the error envelope on stderr:
174
+
175
+ **Resolve `{emitBriefEnvelopeHelper}`** from `{emitBriefEnvelopeProbeOrder}`; first existing path wins. HALT if no candidate exists.
176
+
177
+ ```bash
178
+ echo '{"status":"error","skill_name":"{skill_name or unknown}","halt_reason":"{reason}","mode":"auto"}' | \
179
+ uv run {emitBriefEnvelopeHelper} emit --target stderr
180
+ ```
181
+
182
+ ### 7. Chain to Auto-Validate
183
+
184
+ Load, read fully, then execute {nextStepFile} to present the auto-brief validation gate, where the user can approve, edit, or reject the brief before the pipeline continues.
185
+
186
+ ## CRITICAL STEP COMPLETION NOTE
187
+
188
+ ONLY WHEN the enriched brief has been written and validated will you load and read fully {nextStepFile} to begin the auto-brief validation gate.
@@ -0,0 +1,209 @@
1
+ ---
2
+ nextStepFile: 'health-check.md'
3
+ rejectTargetFile: 'confirm-brief.md'
4
+ validateBriefSchemaProbeOrder:
5
+ - '{project-root}/_bmad/skf/shared/scripts/skf-validate-brief-schema.py'
6
+ - '{project-root}/src/shared/scripts/skf-validate-brief-schema.py'
7
+ writeSkillBriefProbeOrder:
8
+ - '{project-root}/_bmad/skf/shared/scripts/skf-write-skill-brief.py'
9
+ - '{project-root}/src/shared/scripts/skf-write-skill-brief.py'
10
+ emitBriefEnvelopeProbeOrder:
11
+ - '{project-root}/_bmad/skf/shared/scripts/skf-emit-brief-result-envelope.py'
12
+ - '{project-root}/src/shared/scripts/skf-emit-brief-result-envelope.py'
13
+ ---
14
+
15
+ <!-- Config: communicate in {communication_language}. -->
16
+
17
+ # Step 1b: Auto-Brief Validation
18
+
19
+ ## STEP GOAL:
20
+
21
+ To present the user with a concise summary of the auto-generated brief and offer three actions — approve, edit, or reject — before the pipeline continues. On approve or edit, the result envelope is emitted and the pipeline chains to the health check. On reject, the pipeline falls back to the interactive brief review cycle with pre-populated fields.
22
+
23
+ ## Rules
24
+
25
+ - This step is conditional — only loaded from step-auto-brief.md when `[auto]` mode is active
26
+ - The brief MUST already exist on disk (written by step-auto-brief §5) before this step runs
27
+ - Do NOT render YAML or JSON envelopes in the LLM — delegate to deterministic scripts
28
+ - Do NOT modify confirm-brief.md or write-brief.md — the [R]eject path reuses them as-is
29
+ - The 10-line summary is always displayed, even in headless mode, for logging transparency
30
+
31
+ ## MANDATORY SEQUENCE
32
+
33
+ ### 1. Load Auto-Brief
34
+
35
+ Read the brief from `{forge_data_folder}/{skill_name}/skill-brief.yaml` (written by step-auto-brief §5).
36
+
37
+ **Resolve `{validateBriefSchemaHelper}`** from `{validateBriefSchemaProbeOrder}`; first existing path wins. HALT if no candidate exists.
38
+
39
+ Validate the brief against the schema:
40
+
41
+ ```bash
42
+ uv run {validateBriefSchemaHelper} {forge_data_folder}/{skill_name}/skill-brief.yaml
43
+ ```
44
+
45
+ The script returns JSON `{valid, errors[], warnings[], halt_reason, brief}`.
46
+
47
+ - **`valid: false`** — HARD HALT with exit code 2 (`input-invalid`): "**Auto-brief at `{forge_data_folder}/{skill_name}/skill-brief.yaml` is invalid: {first error message}.**" Emit error envelope per §7 with `halt_reason: "input-invalid"`.
48
+ - **`valid: true`** — proceed with the parsed `brief` payload. Surface any non-empty `warnings[]` to the log.
49
+
50
+ **IF the file does not exist:**
51
+ - HARD HALT with exit code 2 (`input-missing`): "**Auto-brief not found at `{forge_data_folder}/{skill_name}/skill-brief.yaml` — step-auto-brief must write the brief before this step runs.**" Emit error envelope per §7 with `halt_reason: "input-missing"`.
52
+
53
+ Extract from the parsed brief:
54
+ - `skill_name` ← `brief.name`
55
+ - `version` ← `brief.version`
56
+ - `source_repo` ← `brief.source_repo`
57
+ - `language` ← `brief.language`
58
+ - `scope_type` ← `brief.scope.type`
59
+ - `scope_include` ← `brief.scope.include`
60
+ - `scope_exclude` ← `brief.scope.exclude`
61
+ - `forge_tier` ← `brief.forge_tier`
62
+ - `description` ← `brief.description`
63
+ - `doc_urls` ← `brief.doc_urls`
64
+
65
+ ### 2. Present 10-Line Summary
66
+
67
+ Render a concise summary from the brief fields for rapid scanning:
68
+
69
+ ```
70
+ Auto-Brief Summary: {skill_name}
71
+ ─────────────────────────────────
72
+ Source: {source_repo}
73
+ Language: {language}
74
+ Scope: {scope_type} ({N} include, {M} exclude patterns)
75
+ Docs: {doc_urls count} sources detected | "None detected"
76
+ Version: {version}
77
+ Forge Tier: {forge_tier}
78
+ Pipeline: forge-auto ({forge_tier} tier)
79
+ Description: "{description}"
80
+ ```
81
+
82
+ Where `{N}` is the count of `scope_include` patterns and `{M}` is the count of `scope_exclude` patterns. If `doc_urls` is null or empty, display "None detected". The `Pipeline` line names the auto pipeline and the resolved `{forge_tier}` — it carries no numeric quality target, which would be an unverified guarantee an automator might parse as fact.
83
+
84
+ ### 3. Validation Gate
85
+
86
+ **GATE: [A]pprove** — Present `[A]pprove` / `[E]dit` / `[R]eject` to user.
87
+
88
+ "**Review the auto-generated brief above.**
89
+
90
+ Select an action:
91
+ [A] Approve — accept the brief as-is and continue the pipeline
92
+ [E] Edit — modify specific fields before continuing
93
+ [R] Reject — fall back to interactive brief review with pre-populated fields"
94
+
95
+ If `{headless_mode}`: auto-proceed with `[A]pprove`, log: "headless: auto-approve auto-brief".
96
+
97
+ Wait for user response. Branch on the response:
98
+
99
+ - `[A]` or `approve` → §4 ([A]pprove path)
100
+ - `[E]` or `edit` → §5 ([E]dit path)
101
+ - `[R]` or `reject` → §6 ([R]eject path)
102
+ - Any other input → answer briefly, re-display the menu
103
+
104
+ ### 4. [A]pprove Path
105
+
106
+ **Resolve `{emitBriefEnvelopeHelper}`** from `{emitBriefEnvelopeProbeOrder}`; first existing path wins. HALT if no candidate exists.
107
+
108
+ Emit the `SKF_BRIEF_RESULT_JSON` envelope with `mode: "auto"`:
109
+
110
+ ```bash
111
+ echo '{"status":"success","brief_path":"{brief_path}","skill_name":"{skill_name}","version":"{version}","language":"{language}","scope_type":"{scope_type}","halt_reason":null,"mode":"auto"}' | \
112
+ uv run {emitBriefEnvelopeHelper} emit
113
+ ```
114
+
115
+ Where `{brief_path}` is `{forge_data_folder}/{skill_name}/skill-brief.yaml`.
116
+
117
+ Chain to {nextStepFile} (health-check.md) — load, read fully, then execute.
118
+
119
+ ### 5. [E]dit Path
120
+
121
+ Present each brief field with its current value and accept natural language modification requests.
122
+
123
+ "**Editable fields:**
124
+
125
+ 1. **Name:** {skill_name}
126
+ 2. **Source:** {source_repo}
127
+ 3. **Language:** {language}
128
+ 4. **Scope type:** {scope_type}
129
+ 5. **Include patterns:** {scope_include}
130
+ 6. **Exclude patterns:** {scope_exclude}
131
+ 7. **Description:** {description}
132
+ 8. **Doc URLs:** {doc_urls or "None"}
133
+ 9. **Version:** {version}
134
+ 10. **Forge tier:** {forge_tier}
135
+
136
+ Tell me what to change (e.g. 'change scope type to public-api', 'add doc URL https://...')."
137
+
138
+ Wait for user response. Apply changes to the brief context.
139
+
140
+ **Re-write the modified brief** through the canonical writer:
141
+
142
+ **Resolve `{writeSkillBriefHelper}`** from `{writeSkillBriefProbeOrder}`; first existing path wins.
143
+
144
+ Assemble the modified brief context as a flat JSON object (same format as step-auto-brief §4):
145
+
146
+ ```bash
147
+ echo '<modified-flat-json>' | uv run {writeSkillBriefHelper} write --target {forge_data_folder}/{skill_name}/skill-brief.yaml --from-flat
148
+ ```
149
+
150
+ The canonical writer validates the brief internally — on non-zero exit, surface the error and re-prompt for corrections. The edit loop allows multiple modifications — each write re-validates before accepting.
151
+
152
+ **Re-validate the written brief** against the schema to confirm correctness:
153
+
154
+ **Resolve `{validateBriefSchemaHelper}`** from `{validateBriefSchemaProbeOrder}`; first existing path wins.
155
+
156
+ ```bash
157
+ uv run {validateBriefSchemaHelper} {forge_data_folder}/{skill_name}/skill-brief.yaml
158
+ ```
159
+
160
+ If validation fails (the writer missed a constraint), surface the error and re-prompt for corrections.
161
+
162
+ Re-present the 10-line summary (§2 format) with updated values so the user can verify the change.
163
+
164
+ "Updated brief written. **Select:** [A] Approve and continue · [E] Edit more · [R] Reject"
165
+
166
+ - `[A]` → emit envelope per §4, chain to {nextStepFile}
167
+ - `[E]` → repeat §5 edit loop
168
+ - `[R]` → §6 ([R]eject path)
169
+
170
+ ### 6. [R]eject Path
171
+
172
+ "**Falling back to interactive brief — fields pre-populated from auto-detection.**"
173
+
174
+ Hydrate brief context variables from the auto-brief on disk, using the same field mapping as the ratify path in gather-intent §3.1a:
175
+
176
+ - `name` ← `brief.name`; `version` ← `brief.version`; `target_version` ← `brief.target_version`
177
+ - `target_ref` ← `brief.target_ref`; `source_ref` ← `brief.source_ref` (optional git refs; preserve when present)
178
+ - `source_repo` ← `brief.source_repo`; `source_type` ← `brief.source_type`; `source_authority` ← `brief.source_authority`; `doc_urls` ← `brief.doc_urls`
179
+ - `language` ← `brief.language`; `description` ← `brief.description`; `forge_tier` ← `brief.forge_tier`
180
+ - `created` ← `brief.created`; `created_by` ← `brief.created_by`
181
+ - `scope.type` / `scope.include` / `scope.exclude` / `scope.tier_a_include` / `scope.notes` / `scope.rationale` / `scope.amendments` ← `brief.scope.*` (preserve `tier_a_include` and the `amendments` log verbatim — do not re-derive or drop them)
182
+ - `scripts_intent` ← `brief.scripts_intent`; `assets_intent` ← `brief.assets_intent`
183
+
184
+ Set `ratify_mode: true` and `ratify_source_path: {forge_data_folder}/{skill_name}/skill-brief.yaml` in workflow context.
185
+
186
+ Chain to {rejectTargetFile} (confirm-brief.md) — load, read fully, then execute. The user gets the full interactive review experience: view, adjust fields inline, revise scope via [R], or approve via [C] → write-brief.md → health-check.md.
187
+
188
+ The interactive chain's write-brief.md handles its own envelope emission with `mode: null` (interactive), which is correct since the user explicitly chose to leave auto mode.
189
+
190
+ ### 7. Error Envelope (Canonical)
191
+
192
+ Every HARD HALT in this step emits the error envelope on stderr:
193
+
194
+ **Resolve `{emitBriefEnvelopeHelper}`** from `{emitBriefEnvelopeProbeOrder}`; first existing path wins.
195
+
196
+ ```bash
197
+ echo '{"status":"error","skill_name":"{skill_name or unknown}","halt_reason":"{reason}","mode":"auto"}' | \
198
+ uv run {emitBriefEnvelopeHelper} emit --target stderr
199
+ ```
200
+
201
+ ### 8. Chain
202
+
203
+ Load, read fully, then execute the appropriate next step file based on the user's choice:
204
+ - [A]pprove or [E]dit (after final approve): {nextStepFile} (health-check.md)
205
+ - [R]eject: {rejectTargetFile} (confirm-brief.md)
206
+
207
+ ## CRITICAL STEP COMPLETION NOTE
208
+
209
+ ONLY WHEN the user has made their choice (approve, edit-then-approve, or reject) and the appropriate action has been taken (envelope emitted or context hydrated) will you load and read fully the next step file.
@@ -1,5 +1,4 @@
1
1
  ---
2
- briefSchemaFile: 'assets/skill-brief-schema.md'
3
2
  versionResolutionFile: 'references/version-resolution.md'
4
3
  qmdRegistrationFile: 'references/qmd-collection-registration.md'
5
4
  nextStepFile: 'health-check.md'
@@ -98,7 +97,7 @@ Assemble the brief context as a **flat** JSON object — every approved value is
98
97
  "scope_rationale": null | {"recommended":"...","chosen":"...","accepted_recommendation":true|false,"heuristic":"...","reason":"...","recorded":"YYYY-MM-DD"},
99
98
  "scope_tier_a_include": null | ["{tier-A authoring-surface patterns — from step 03 §3c capture, or hydrated on a ratify run}"],
100
99
  "scope_amendments": null | [{"path":"...","action":"...","reason":"...","date":"YYYY-MM-DD","workflow":"..."}],
101
- "doc_urls": null | [{"url": "...", "label": "..."}],
100
+ "doc_urls": null | [{"url": "...", "label": "...", "source": "{optional: language-registry|readme-detection|homepage|pages-api|docs-folder}"}],
102
101
  "scripts_intent": null | "{detect|none|free-text}",
103
102
  "assets_intent": null | "{detect|none|free-text}",
104
103
  "source_authority": null | "{official|community|internal}",
@@ -163,7 +162,7 @@ The script derives `exit_code` deterministically from `halt_reason` (null→0, i
163
162
 
164
163
  The script enforces the success/error halt_reason invariant (success requires null halt_reason; error requires non-null). The `user-cancelled` halt_reason is accepted for completeness (interactive `[X]` Cancel sites in step 1/03/04) but never appears on the headless code path.
165
164
 
166
- Invocation sites (each pointed at this block, not duplicated): step 1 §1 (write-failed pre-resolution), step 1 §8 (input-missing/input-invalid GATE), step 5 §2b (overwrite-cancelled), step 5 §3 (input-invalid/write-failed from script). Step-01 §1 forge-tier-missing and step 2 §1 (target-inaccessible/gh-auth-failed) currently HALT without emitting an envelope — pre-existing gaps tracked separately, not addressed by this canonicalization.
165
+ Invocation sites (each pointed at this block, not duplicated): step 1 §1 (write-failed pre-resolution; forge-tier-missing), step 1 §8 (input-missing/input-invalid GATE), step 2 §1 (target-inaccessible/gh-auth-failed), step 5 §2b (overwrite-cancelled), step 5 §3 (input-invalid/write-failed from script). The step 1 §1 forge-tier-missing and step 2 §1 target-inaccessible/gh-auth-failed sites emit through this block too, so every headless HALT class surfaces a `SKF_BRIEF_RESULT_JSON` envelope — there are no envelope-silent failure classes.
167
166
 
168
167
  When `{headless_mode}` is false, skip this section silently — no envelope is emitted.
169
168
 
@@ -0,0 +1,190 @@
1
+ ---
2
+ name: skf-campaign
3
+ description: Campaign orchestration — multi-library skill production with dependency tracking, file-based state, and resume. Use when the user asks to "run a campaign" or "orchestrate skills."
4
+ ---
5
+
6
+ # Campaign
7
+
8
+ ## Overview
9
+
10
+ Orchestrates the production of 15+ skills across multiple sessions by driving them through the full SKF pipeline (brief, generate, compile, test, export) in dependency order. Campaign sits at the top of the pipeline ladder — it does not produce skill artifacts directly but sequences the workflows that do. File-based state (`_campaign-state.yaml`) survives context death, enabling resume from any point.
11
+
12
+ ## Conventions
13
+
14
+ - Bare paths (e.g. `references/step-01-setup.md`) resolve from the skill root.
15
+ - `references/` holds step files chained by stage number, plus the directive contract at `references/campaign-directive-spec.md` (the canonical spec for `_campaign-directive.md`); `templates/` holds workflow-loaded templates; `scripts/` and `assets/` hold deterministic helpers and the state schema.
16
+ - `{skill-root}` resolves to this skill's installed directory (where `customize.toml` lives).
17
+ - `{project-root}`-prefixed paths resolve from the project working directory.
18
+ - `{skill-name}` resolves to the skill directory's basename.
19
+
20
+ ## Role
21
+
22
+ You are a campaign orchestrator operating in Ferris's Management mode. You sequence workflows, track per-skill state, enforce quality gates, and ensure every skill reaches its target tier — while the individual pipeline workflows handle the actual artifact production.
23
+
24
+ ## On Activation
25
+
26
+ Run these steps once, in order, before dispatching to Mode Routing.
27
+
28
+ 1. **Load config.** Read `{project-root}/_bmad/skf/config.yaml` and `{sidecar_path}/preferences.yaml` in one batched message (independent files). From config resolve `project_name`, `user_name`, `communication_language`, `document_output_language`, `skills_output_folder`, `forge_data_folder`, `sidecar_path`. From preferences resolve `headless_mode` (default false). If the config file is missing, fall back to `forge_data_folder = forge-data`.
29
+
30
+ 2. **Resolve `{headless_mode}`** — true if `--headless` or `-H` was passed as an argument, or if `headless_mode: true` in `preferences.yaml`. Default: false.
31
+
32
+ 3. **Resolve workflow customization.** Run:
33
+
34
+ ```bash
35
+ python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow
36
+ ```
37
+
38
+ The script merges three layers (scalars override, arrays append):
39
+
40
+ - `{skill-root}/customize.toml` — bundled defaults
41
+ - `_bmad/custom/<skill-name>.toml` under `{project-root}` — team overrides (committed)
42
+ - `_bmad/custom/<skill-name>.user.toml` under `{project-root}` — personal overrides (gitignored)
43
+
44
+ If it fails or is missing, fall back to `{skill-root}/customize.toml` directly. Apply the fallback for each scalar now so step files never repeat the conditional, and stash as workflow-context variables:
45
+
46
+ - `{campaignWorkspacePath}` ← `workflow.campaign_workspace_path` if non-empty, else `{forge_data_folder}/_campaign`
47
+ - `{qualityGateHard}` / `{qualityGateSoftTarget}` / `{qualityGateSoftFallback}` ← the `quality_gate_*` scalars (defaults `zero-critical-high` / `90` / `80`)
48
+ - `{reportTemplatePath}` ← `workflow.report_template_path` if non-empty, else `templates/campaign-report-template.md`
49
+ - `{kickoffTemplatePath}` ← `workflow.kickoff_template_path` if non-empty, else `templates/kickoff-template.md`
50
+ - `{briefTemplatePath}` ← `workflow.brief_template_path` if non-empty, else `templates/campaign-brief-template.yaml`
51
+ - `{onComplete}` ← `workflow.on_complete` (empty = no-op)
52
+
53
+ Load `workflow.persistent_facts` (literal sentences and `file:` references, globs expanded) and keep them in mind for the whole campaign — they are injected into every per-skill kickoff. Run any `activation_steps_prepend` before step 1 and any `activation_steps_append` after this step.
54
+
55
+ Every step file resolves its frontmatter path vars (`stateFile`, `backupFile`, …) under `{campaignWorkspacePath}` — overriding `campaign_workspace_path` relocates the whole campaign workspace without editing a single step.
56
+
57
+ 4. **Parse CLI overrides** into the workflow context:
58
+
59
+ | Flag | Effect |
60
+ | --- | --- |
61
+ | `--headless` / `-H` | Force `{headless_mode} = true` (see step 2). |
62
+ | `--brief <file>` | Seed step-01 targets from a `campaign-brief.yaml` instead of interactive prompts. Implies `--headless`. |
63
+ | `--manifest <file>` | Seed step-01 targets from a plain-text `name,repo_url,tier,pin` manifest. Implies `--headless`. |
64
+ | `--from <skill>` | Resume override — see Mode Routing. |
65
+
66
+ If `--brief` or `--manifest` is set, force `{headless_mode} = true` (log "headless: coerced by --brief/--manifest" if it was false).
67
+
68
+ **`--manifest` format:** one target per line, comma-separated `name,repo_url,tier,pin` (omit `pin` or leave it empty for latest); add a trailing `;dep1,dep2` segment for `depends_on`. Blank lines and `#` comments are skipped. If any line fails to parse, HALT in step-01 listing the offending line numbers rather than silently dropping targets — a partial target set is never run.
69
+
70
+ 5. **Dispatch** per Mode Routing below.
71
+
72
+ ## Workflow Rules
73
+
74
+ These rules apply to every step in this workflow:
75
+
76
+ - State-first — write state to disk before chaining to the next step or workflow
77
+ - Read-backup-modify-write for all state mutations (see State Contract below)
78
+ - Validate `_campaign-state.yaml` on every load by running `uv run scripts/campaign-validate-state.py --state-file {stateFile}` and HALT (exit code 3, `invalid-state`) on non-zero — never hand-validate the schema
79
+ - Zero memory dependency (NFR-2) — campaign state is 100% recoverable from disk; never rely on conversation context for progress tracking
80
+ - Treat a missing or unparseable `SKF_*_RESULT_JSON` envelope from any sub-skill as a sub-skill failure; never write partial state from an unparsed envelope
81
+ - Append a one-line entry to the campaign decision log (`{campaignWorkspacePath}/_campaign-decision-log.md`, append-only) at every operator decision or auto-decision (skip/force, architecture-doc skip, export cancel/proceed, campaign overwrite, `.bak` recovery, user-cancel) so rationale survives compaction and resume
82
+ - **Universal cancel affordance** — at any interactive gate the operator may type `cancel`, `exit`, or `:q` to leave cleanly. HARD HALT with **exit code 12 (`user-cancelled`)**, log the cancellation to the decision log, and leave state intact and resumable. This gives the operator a documented way out at every prompt between Setup and the Export gate. **The Export gate is the one exception:** it has its own `[C]ancel` with exit code 11 (`export-cancelled`), which remains canonical there — do not also emit 12 at the export gate, so an automator's exit-code branch stays deterministic. These keywords are recognized only as a response *to an interactive prompt*; a skill or campaign named `cancel`/`exit` supplied as data is never treated as a cancel.
83
+ - Always communicate in `{communication_language}`
84
+ - If `{headless_mode}` is true, auto-proceed through confirmation gates with their default action and log each auto-decision
85
+ - If `{headless_mode}` is true, emit a single-line JSON progress event to **stderr** at each step's entry and exit so schedulers stream live progress instead of post-mortem-parsing the final envelope:
86
+ - entry: `{"stage":N,"name":"<slug>","status":"start"}`
87
+ - exit (just before chaining): `{"stage":N,"name":"<slug>","status":"done"}`
88
+ - on HARD HALT: `{"stage":N,"name":"<slug>","status":"halt","exit":<code>}` instead of "done"
89
+
90
+ `N` is the stage number (0–10, per the Stages table) and `<slug>` is the kebab portion of the step filename. For the non-numbered routing/terminal steps (`resume`, `health-check`) emit `"stage":null` with the slug. One line per event; do not pretty-print.
91
+
92
+ ## Stages
93
+
94
+ | # | Step | File | Auto-proceed |
95
+ |---|------|------|--------------|
96
+ | 0 | Setup | references/step-01-setup.md | Yes |
97
+ | 1 | Strategy | references/step-02-strategy.md | Yes |
98
+ | 2 | Pin Validation | references/step-03-pins.md | Yes |
99
+ | 3 | Provenance | references/step-04-provenance.md | Yes |
100
+ | 4 | Skill Loop | references/step-05-skill-loop.md | Yes |
101
+ | 5 | Tier B Batch | references/step-06-batch.md | Yes |
102
+ | 6 | Capstone | references/step-07-capstone.md | Yes |
103
+ | 7 | Verification | references/step-08-verify.md | Yes |
104
+ | 8 | Refinement | references/step-09-refine.md | Yes |
105
+ | 9 | Export | references/step-10-export.md | No (write-gate HALT) |
106
+ | 10 | Maintenance | references/step-11-maintenance.md | Yes |
107
+
108
+ **Stage numbering:** step files are 1-indexed (`step-01` … `step-11`); `campaign.current_stage` in state is 0-indexed, so step-`NN` runs stage `NN − 1` (e.g. step-01 = stage 0, step-11 = stage 10). The Resume Routing table in `references/step-resume.md` maps a resolved stage number to its step file; because `current_stage` records the highest *completed* stage, resume without an active skill advances to `current_stage + 1` (the stage that still needs to run) before consulting the table.
109
+
110
+ ## Invocation Contract
111
+
112
+ | Aspect | Detail |
113
+ |--------|--------|
114
+ | **Inputs** | `campaign` to start a new campaign; `campaign resume [--from=<skill>]` to resume from last active or specified skill; `campaign status` for a read-only progress summary |
115
+ | **Overrides** | `--headless` / `-H`, `--brief <file>`, `--manifest <file>`, `--from <skill>` — see On Activation step 4 |
116
+ | **Gates** | Step 9 (Export): write-gate HALT — requires explicit user approval before writing exported skills to disk |
117
+ | **Outputs** | `_campaign-state.yaml` (state), `campaign-brief.yaml` (machine-generated brief), `campaign-report.md` (post-campaign summary), `_campaign-decision-log.md` (append-only rationale), `SKF_CAMPAIGN_RESULT_JSON` (headless envelope) — all under `{campaignWorkspacePath}` |
118
+ | **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true |
119
+ | **Exit codes** | See "Exit Codes" below |
120
+
121
+ ## Exit Codes
122
+
123
+ Every HARD HALT exits with a stable, documented code so headless automators can branch on the failure class without grepping message text:
124
+
125
+ | Code | Meaning | Raised by |
126
+ | ---- | -------------------- | ----------------------------------------------------------- |
127
+ | 0 | success | step-11 (terminal) |
128
+ | 3 | invalid-state | any step §1 (`campaign-validate-state.py` non-zero on load) |
129
+ | 4 | circular-deps | step-02 §5 |
130
+ | 5 | invalid-pin | step-03 §5 |
131
+ | 6 | inaccessible-repo | step-04 §5 |
132
+ | 7 | dependency-deadlock | step-05 §4 (no skill ready and no recovery chosen) |
133
+ | 8 | missing-brief | step-03/04/05 §2 (brief missing or unreadable) |
134
+ | 9 | corrupt-state | step-resume §1 (primary unrecoverable, `.bak` also invalid) |
135
+ | 10 | report-failure | step-11 §2 — **degraded only**: the report could not be generated; the campaign still completes and state stays intact (never a hard halt that discards a finished campaign) |
136
+ | 11 | export-cancelled | step-10 §4 (operator chose `[C]ancel` — graceful, resumable) |
137
+ | 12 | user-cancelled | any interactive gate (operator typed `cancel` / `exit` / `:q` — graceful, resumable; see Workflow Rules) |
138
+
139
+ ## Result Contract on HARD HALT
140
+
141
+ In addition to the success-variant envelope (see Campaign Headless Envelope), every HARD HALT emits an **error variant** so automators don't silently break. Emit one line on **stderr**:
142
+
143
+ ```
144
+ SKF_CAMPAIGN_RESULT_JSON: {"status":"error","exit_code":<N>,"phase":"<slug>","error":{"code":"<class>","message":"<short>"},"skills_completed":N,"skills_failed":N,"campaign_report_path":null,"decision_log":"<path-or-null>"}
145
+ ```
146
+
147
+ `<class>` is the Exit Codes meaning (e.g. `circular-deps`, `inaccessible-repo`); `<slug>` is the step where the HALT occurred. One line, no pretty-print.
148
+
149
+ ## Mode Routing
150
+
151
+ On invocation:
152
+
153
+ 1. **`campaign resume [--from=<skill>]`** — load `references/step-resume.md`. Validates state integrity, checks backup consistency, and chains to the appropriate stage step file. If `--from=<skill>` is provided, override the resume point to the named skill.
154
+ 2. **`campaign`** (new, no existing state) — run from stage 0 (Setup).
155
+ 3. **`campaign`** (state exists) — detect existing `{campaignWorkspacePath}/_campaign-state.yaml` and prompt: **resume** via `references/step-resume.md`, or **overwrite** with a new campaign. On overwrite, first archive the existing `_campaign-state.yaml` and `campaign-brief.yaml` to `{campaignWorkspacePath}/archive/{name}-{timestamp}/`, log the archive to the decision log, and show a one-line summary of what is being set aside before chaining to step-01. In headless mode, default to **resume** (never silently clobber); archive-and-overwrite only when `--brief`/`--manifest` explicitly seeds a new campaign.
156
+ 4. **`campaign status`** (read-only) — load `{campaignWorkspacePath}/_campaign-state.yaml`, validate it via `campaign-validate-state.py`, display the Resume Detection summary block followed by the last ~15 lines of `{campaignWorkspacePath}/_campaign-decision-log.md` (so the operator sees the recent decision trail without flooding the terminal on a long campaign), then stop. No backup, no mutation, no chaining. Exit 0 (or 9 if the state is unrecoverable).
157
+
158
+ ## Resume Detection
159
+
160
+ When resuming:
161
+
162
+ 1. Read `_campaign-state.yaml`
163
+ 2. Validate via `campaign-validate-state.py` (halt on invalid; attempt `.bak` recovery per step-resume)
164
+ 3. Find last active or completed stage from `campaign.current_stage` (the highest completed stage); when no skill is active, resume continues from the stage *after* it (`current_stage + 1`, terminal-capped)
165
+ 4. Skip completed skills (status = `completed` or `skipped`)
166
+ 5. If `--from=<skill>` is provided, find the named skill and resume from its stage
167
+ 6. Continue from the next incomplete skill in `dependency_graph.execution_order`
168
+
169
+ ## State Contract
170
+
171
+ All state mutations follow the read-backup-modify-write pattern:
172
+
173
+ 1. **Read** `_campaign-state.yaml`
174
+ 2. **Validate** via `uv run scripts/campaign-validate-state.py --state-file {stateFile}` (halt on invalid)
175
+ 3. **Backup** — copy current `_campaign-state.yaml` to `_campaign-state.yaml.bak`
176
+ 4. **Modify** in memory
177
+ 5. **Update** `campaign.last_updated` to current ISO-8601 timestamp
178
+ 6. **Write** modified state back to `_campaign-state.yaml`
179
+
180
+ The `.bak` file is one-deep (overwritten on every write). If the primary file is corrupted (crash during write), the `.bak` file contains the last valid state — step-resume §1 recovers from it automatically rather than dead-halting.
181
+
182
+ ## Campaign Headless Envelope
183
+
184
+ When `{headless_mode}` is true, the final step emits a single-line JSON envelope on stdout:
185
+
186
+ ```
187
+ SKF_CAMPAIGN_RESULT_JSON: {"status":"success|error","skills_completed":0,"skills_failed":0,"quality_scores":{},"campaign_report_path":"","decision_log":"","duration":""}
188
+ ```
189
+
190
+ `status` is `"success"` when the campaign completes normally, `"error"` on any unrecoverable halt (with `exit_code` per the Exit Codes table — see Result Contract on HARD HALT). `skills_completed` and `skills_failed` count per-skill outcomes. `quality_scores` maps skill names to their test-skill scores. `campaign_report_path` points to the generated `campaign-report.md`. `decision_log` points to `_campaign-decision-log.md`. `duration` is the wall-clock time of the campaign run. Populate the counts, `quality_scores`, and `duration` directly from the `campaign-report.py` result JSON (step-11 §2) — do not recompute them by hand.