Campaign
Overview
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 atop the pipeline ladder: it sequences the workflows that produce skills rather than producing artifacts itself. File-based state (_campaign-state.yaml) survives context death, enabling resume from any point.
Conventions
- Bare paths (e.g.
references/step-01-setup.md) resolve from the skill root.
references/ holds the stage-chained step files plus reference specs (e.g. the _campaign-directive.md contract at references/campaign-directive-spec.md); templates/, scripts/, and assets/ hold templates, deterministic helpers, and the state schema.
{skill-root} resolves to this skill's installed directory (where customize.toml lives).
{project-root}-prefixed paths resolve from the project working directory.
{skill-name} resolves to the skill directory's basename.
Role
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.
On Activation
Run these steps once, in order, before dispatching to Mode Routing.
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.
Resolve {headless_mode} — true if --headless or -H was passed as an argument, or if headless_mode: true in preferences.yaml. Default: false.
Resolve workflow customization. Run:
python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow
The script merges three layers (scalars override, arrays append):
{skill-root}/customize.toml — bundled defaults
_bmad/custom/<skill-name>.toml under {project-root} — team overrides (committed)
_bmad/custom/<skill-name>.user.toml under {project-root} — personal overrides (gitignored)
If it fails or is missing, fall back to {skill-root}/customize.toml directly. Resolve each scalar now (so step files never repeat the conditional) and stash as workflow-context variables:
{campaignWorkspacePath} ← workflow.campaign_workspace_path if non-empty, else {forge_data_folder}/_campaign
{qualityGateHard} / {qualityGateSoftTarget} / {qualityGateSoftFallback} ← the quality_gate_* scalars (defaults zero-critical-high / 90 / 80)
{reportTemplatePath} ← workflow.report_template_path if non-empty, else templates/campaign-report-template.md
{kickoffTemplatePath} ← workflow.kickoff_template_path if non-empty, else templates/kickoff-template.md
{briefTemplatePath} ← workflow.brief_template_path if non-empty, else templates/campaign-brief-template.yaml
{onComplete} ← workflow.on_complete (empty = no-op)
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.
Parse CLI overrides into the workflow context:
| Flag |
Effect |
--headless / -H |
Force {headless_mode} = true (see step 2). |
--brief <file> |
Seed step-01 targets from a campaign-brief.yaml instead of interactive prompts. Implies --headless. |
--manifest <file> |
Seed step-01 targets from a plain-text name,repo_url,tier,pin manifest. Implies --headless. |
--from <skill> |
Resume override — see Mode Routing. |
If --brief or --manifest is set, force {headless_mode} = true (log "headless: coerced by --brief/--manifest" if it was false).
--manifest format: one name,repo_url,tier,pin target per line (empty pin = latest); a trailing ;dep1,dep2 segment sets depends_on; blank and # lines are skipped. A malformed line HALTs step-01 with the offending line numbers — never a partial target set.
Dispatch per Mode Routing below.
Workflow Rules
These rules apply to every step in this workflow:
- State-first — write state to disk before chaining to the next step or workflow
- Read-backup-modify-write for all state mutations (State Contract in
references/campaign-contracts.md)
- 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
- Zero memory dependency — campaign state is 100% recoverable from disk; never rely on conversation context for progress tracking
- 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
- Append a one-line entry to the campaign decision log (
{campaignWorkspacePath}/_campaign-decision-log.md, append-only) at every operator or auto-decision (skip/force, overwrite, export cancel/proceed, .bak recovery, user-cancel) so rationale survives compaction and resume
- Universal cancel affordance — at any interactive gate between Setup and the Export gate,
cancel/exit/:q triggers a HARD HALT with exit code 12 (user-cancelled): log it and leave state intact and resumable. Exception: the Export gate's own [C]ancel stays exit code 11 (export-cancelled) — never also emit 12 there, so an automator's exit-code branch stays deterministic. These keywords count only as a response to a prompt; a skill or campaign named cancel/exit supplied as data is never treated as a cancel.
- Always communicate in
{communication_language}
- If
{headless_mode} is true, auto-proceed through confirmation gates with their default action and log each auto-decision
- If
{headless_mode} is true, emit a single-line JSON progress event to stderr at each step's entry, exit, and HARD HALT so schedulers stream live progress — event format in references/campaign-contracts.md (Headless Progress Events)
Stages
| # |
Step |
File |
Auto-proceed |
| 0 |
Setup |
references/step-01-setup.md |
Yes |
| 1 |
Strategy |
references/step-02-strategy.md |
Yes |
| 2 |
Pin Validation |
references/step-03-pins.md |
Yes |
| 3 |
Provenance |
references/step-04-provenance.md |
Yes |
| 4 |
Skill Loop |
references/step-05-skill-loop.md |
Yes |
| 5 |
Tier B Batch |
references/step-06-batch.md |
Yes |
| 6 |
Capstone |
references/step-07-capstone.md |
Yes |
| 7 |
Verification |
references/step-08-verify.md |
Yes |
| 8 |
Refinement |
references/step-09-refine.md |
Yes |
| 9 |
Export |
references/step-10-export.md |
No (write-gate HALT) |
| 10 |
Maintenance |
references/step-11-maintenance.md |
Yes |
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 (step-01 = stage 0, step-11 = stage 10). references/step-resume.md §3–§4 own how a resolved stage maps back to its step file on resume (including the current_stage + 1 advance when no skill is active).
Invocation Contract
| Aspect |
Detail |
| 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 |
| 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} |
Contracts
Exit codes, the HARD-HALT error envelope, the read-backup-modify-write State Contract, the headless success envelope, and per-step progress events live in references/campaign-contracts.md — consult it when you HALT, mutate state, or emit headless output.
Mode Routing
On invocation:
campaign resume [--from=<skill>] — load references/step-resume.md (validates state, recovers from backup, chains to the right stage). --from=<skill> overrides the resume point to the named skill.
campaign (new, no existing state) — run from stage 0 (Setup).
campaign (state exists) — detect existing {campaignWorkspacePath}/_campaign-state.yaml and prompt resume (via references/step-resume.md) or overwrite. On overwrite, first archive the existing _campaign-state.yaml and campaign-brief.yaml to {campaignWorkspacePath}/archive/{name}-{timestamp}/ and log it 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.
campaign status (read-only) — load {campaignWorkspacePath}/_campaign-state.yaml, validate it via campaign-validate-state.py, then run uv run scripts/campaign-status.py --state-file {campaignWorkspacePath}/_campaign-state.yaml and display its summary (campaign name, current stage, completed-vs-total, per-status counts) followed by the last ~15 lines of {campaignWorkspacePath}/_campaign-decision-log.md for the recent decision trail, then stop. No backup, no mutation, no chaining. Exit 0 (or 9 if the state is unrecoverable).
1---2name: skf-campaign3description: 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---56# Campaign78## Overview910Orchestrates 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 atop the pipeline ladder: it sequences the workflows that produce skills rather than producing artifacts itself. File-based state (`_campaign-state.yaml`) survives context death, enabling resume from any point.1112## Conventions1314- Bare paths (e.g. `references/step-01-setup.md`) resolve from the skill root.15- `references/` holds the stage-chained step files plus reference specs (e.g. the `_campaign-directive.md` contract at `references/campaign-directive-spec.md`); `templates/`, `scripts/`, and `assets/` hold templates, 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.1920## Role2122You 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.2324## On Activation2526Run these steps once, in order, before dispatching to Mode Routing.27281. **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`.29302. **Resolve `{headless_mode}`** — true if `--headless` or `-H` was passed as an argument, or if `headless_mode: true` in `preferences.yaml`. Default: false.31323. **Resolve workflow customization.** Run:3334 ```bash35 python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow36 ```3738 The script merges three layers (scalars override, arrays append):3940 - `{skill-root}/customize.toml` — bundled defaults41 - `_bmad/custom/<skill-name>.toml` under `{project-root}` — team overrides (committed)42 - `_bmad/custom/<skill-name>.user.toml` under `{project-root}` — personal overrides (gitignored)4344 If it fails or is missing, fall back to `{skill-root}/customize.toml` directly. Resolve each scalar now (so step files never repeat the conditional) and stash as workflow-context variables:4546 - `{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)5253 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.54554. **Parse CLI overrides** into the workflow context:5657 | Flag | Effect |58 | --- | --- |59 | `--headless` / `-H` | Force `{headless_mode} = true` (see step 2). |60 | `--brief <file>` | Seed step-01 targets from a `campaign-brief.yaml` instead of interactive prompts. Implies `--headless`. |61 | `--manifest <file>` | Seed step-01 targets from a plain-text `name,repo_url,tier,pin` manifest. Implies `--headless`. |62 | `--from <skill>` | Resume override — see Mode Routing. |6364 If `--brief` or `--manifest` is set, force `{headless_mode} = true` (log "headless: coerced by --brief/--manifest" if it was false).6566 **`--manifest` format:** one `name,repo_url,tier,pin` target per line (empty `pin` = latest); a trailing `;dep1,dep2` segment sets `depends_on`; blank and `#` lines are skipped. A malformed line HALTs step-01 with the offending line numbers — never a partial target set.67685. **Dispatch** per Mode Routing below.6970## Workflow Rules7172These rules apply to every step in this workflow:7374- State-first — write state to disk before chaining to the next step or workflow75- Read-backup-modify-write for all state mutations (State Contract in `references/campaign-contracts.md`)76- 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 schema77- Zero memory dependency — campaign state is 100% recoverable from disk; never rely on conversation context for progress tracking78- 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 envelope79- Append a one-line entry to the campaign decision log (`{campaignWorkspacePath}/_campaign-decision-log.md`, append-only) at every operator or auto-decision (skip/force, overwrite, export cancel/proceed, `.bak` recovery, user-cancel) so rationale survives compaction and resume80- **Universal cancel affordance** — at any interactive gate between Setup and the Export gate, `cancel`/`exit`/`:q` triggers a HARD HALT with **exit code 12 (`user-cancelled`)**: log it and leave state intact and resumable. Exception: the Export gate's own `[C]ancel` stays exit code 11 (`export-cancelled`) — never also emit 12 there, so an automator's exit-code branch stays deterministic. These keywords count only as a response *to a prompt*; a skill or campaign named `cancel`/`exit` supplied as data is never treated as a cancel.81- Always communicate in `{communication_language}`82- If `{headless_mode}` is true, auto-proceed through confirmation gates with their default action and log each auto-decision83- If `{headless_mode}` is true, emit a single-line JSON progress event to **stderr** at each step's entry, exit, and HARD HALT so schedulers stream live progress — event format in `references/campaign-contracts.md` (Headless Progress Events)8485## Stages8687| # | Step | File | Auto-proceed |88|---|------|------|--------------|89| 0 | Setup | references/step-01-setup.md | Yes |90| 1 | Strategy | references/step-02-strategy.md | Yes |91| 2 | Pin Validation | references/step-03-pins.md | Yes |92| 3 | Provenance | references/step-04-provenance.md | Yes |93| 4 | Skill Loop | references/step-05-skill-loop.md | Yes |94| 5 | Tier B Batch | references/step-06-batch.md | Yes |95| 6 | Capstone | references/step-07-capstone.md | Yes |96| 7 | Verification | references/step-08-verify.md | Yes |97| 8 | Refinement | references/step-09-refine.md | Yes |98| 9 | Export | references/step-10-export.md | No (write-gate HALT) |99| 10 | Maintenance | references/step-11-maintenance.md | Yes |100101**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` (step-01 = stage 0, step-11 = stage 10). `references/step-resume.md` §3–§4 own how a resolved stage maps back to its step file on resume (including the `current_stage + 1` advance when no skill is active).102103## Invocation Contract104105| Aspect | Detail |106|--------|--------|107| **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 |108| **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}` |109110## Contracts111112Exit codes, the HARD-HALT error envelope, the read-backup-modify-write **State Contract**, the headless success envelope, and per-step progress events live in `references/campaign-contracts.md` — consult it when you HALT, mutate state, or emit headless output.113114## Mode Routing115116On invocation:1171181. **`campaign resume [--from=<skill>]`** — load `references/step-resume.md` (validates state, recovers from backup, chains to the right stage). `--from=<skill>` overrides the resume point to the named skill.1192. **`campaign`** (new, no existing state) — run from stage 0 (Setup).1203. **`campaign`** (state exists) — detect existing `{campaignWorkspacePath}/_campaign-state.yaml` and prompt **resume** (via `references/step-resume.md`) or **overwrite**. On overwrite, first archive the existing `_campaign-state.yaml` and `campaign-brief.yaml` to `{campaignWorkspacePath}/archive/{name}-{timestamp}/` and log it 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.1214. **`campaign status`** (read-only) — load `{campaignWorkspacePath}/_campaign-state.yaml`, validate it via `campaign-validate-state.py`, then run `uv run scripts/campaign-status.py --state-file {campaignWorkspacePath}/_campaign-state.yaml` and display its summary (campaign name, current stage, completed-vs-total, per-status counts) followed by the last ~15 lines of `{campaignWorkspacePath}/_campaign-decision-log.md` for the recent decision trail, then stop. No backup, no mutation, no chaining. Exit 0 (or 9 if the state is unrecoverable).