Skip to content
🤖 Consolidated, AI-optimized SKF docs: llms-full.txt. Fetch this plain text file for complete context.

Campaign Orchestration

Campaign orchestration builds many related skills as one coordinated campaign, in dependency order. It is meant for projects with many libraries (15 or more), but one library is enough to start. Each Tier A library goes through the full SKF pipeline: brief, compile and test. Tier B libraries take a faster batch path. The finished skills are exported together at the end (see Tier A vs Tier B). The skf-campaign skill does not write skills itself. It runs the SKF workflows that do, in the right order.

When your project depends on many libraries, building one skill at a time is slow and easy to get wrong. Campaign runs the whole loop for you. You list the libraries and what each one depends on, and Campaign orders them so every skill is built after the skills it depends on. It then drives each one through the pipeline, applies the quality gates, and saves its progress to disk as it goes. If the AI session runs out of context or you close it, you can resume where you left off.


  • Run skf-setup first, so SKF knows your tools and folders.
  • Install the GitHub CLI (gh) and sign in. Pin Validation and Provenance use it to check every repository, and without it the campaign stops before any skill is built. Each library’s repository must be on GitHub.
  • An architecture document is optional. Only the Verification and Refinement stages use it (see Stages).

Ask Ferris to run campaign, or call the skf-campaign skill directly.

@Ferris campaign # start a new campaign
@Ferris campaign resume # resume from the last active skill
@Ferris campaign resume --from=<skill> # resume from a specific skill
@Ferris campaign status # read-only progress summary
  • campaign starts a new campaign from stage 0 (Setup). If a _campaign-state.yaml or its backup already exists, Ferris offers a choice: resume the existing campaign or overwrite with a new one.
  • campaign resume picks up where the last session left off. Ferris checks the state file first. If it is damaged, Ferris restores the .bak backup copy and continues from that.
  • campaign status checks the state file and shows the campaign name, the current stage, how many skills are done out of the total, and how many skills have each status. After that come the last lines of the decision log. Then it stops. It makes no backup, changes nothing and starts no stage, so you can check on a long campaign without moving it forward.
  • --from=<skill> moves the resume point to the named skill. To rebuild a skill that already finished, choose [R]e-run when Ferris asks.
FlagEffect
--headless / -HAnswer every question with its default and print machine-readable progress and results (see Headless / Automation).
--brief <file>Seed targets from a campaign-brief.yaml instead of interactive prompts. Implies --headless: a seeded run is an unattended (CI) run that takes every default. To review the plan and the export, give the same file at the Setup question instead, which stays interactive.
--manifest <file>Seed targets from a plain-text name,repo_url,tier,pin manifest (one per line; trailing ;dep1,dep2 for depends_on). Implies --headless, as --brief does, for the same reason.
--from=<skill>Resume override (see above).

An interactive Setup opens with one question: give the targets in whatever form you have them, such as a manifest or campaign-brief.yaml path, a pasted list or repository URLs. Add a campaign name, and the path to a _campaign-directive.md file or to your architecture document, if you have them.

Setup then drafts the campaign and shows it once for you to correct: the campaign name (your project name unless you gave one), the directive and architecture paths, and one table of the targets with these columns:

  • name: the skill name
  • repo_url: the GitHub repository, as a URL, an SSH URL or owner/repo. Setup writes it as https://github.com/<owner>/<repo> and rejects another host or a path inside a repository
  • tier: A (full pipeline) or B (quick batch), see Tier A vs Tier B. A unless you say otherwise
  • pin: empty for the latest release (the default). A Tier A pin must be a full X.Y.Z version (a v prefix is allowed), because brief-skill builds a Tier A skill from a release version only; a Tier B pin may also be a branch
  • depends_on: the other skills in this campaign that it depends on, none by default

After your corrections, Setup asks only about what is still missing or ambiguous, such as a target with no repository URL or a dependency that names no target, all in one question.

To skip these questions, pass the list as a manifest file with --manifest:

# name,repo_url,tier,pin;depends_on
core-lib,https://github.com/acme/core-lib,A,
web-sdk,https://github.com/acme/web-sdk,A,2.1.0;core-lib
cli-helpers,https://github.com/acme/cli-helpers,B

Setup checks every target the same way, whether it comes from a manifest, a --brief file or a pasted list. It rejects a malformed line or target, a repo_url that is not a GitHub repository, a name that is not a skill name (lower-case letters, digits and hyphens, at most 64 characters), a Tier A pin that is not an X.Y.Z version, and a duplicate name. A headless run then stops with exit code 2 and lists each bad line or target with what is wrong with it, and an interactive Setup shows them and asks you to correct those targets. It never runs with only part of the list.


Campaign runs through 11 stages, numbered 0 to 10. Most run on their own. Setup asks for your campaign details, unless you pass --brief or --manifest. Two stages always stop for your answer: Strategy shows you the plan before any skill is built, and Export asks before it writes anything. The Skill Loop, Verification and Refinement ask only when they need a decision. In headless mode, every question takes its default answer.

StageNameWhat happensWaits for you?
0SetupAsks for the campaign name and the libraries to skill, writes _campaign-state.yaml, and generates campaign-brief.yamlYes, for its questions (not with --brief or --manifest)
1StrategyWorks out the build order so each skill comes after the skills it depends on, stops on a circular or unknown dependency, and shows you the planYes: [P]roceed or [C]ancel
2Pin ValidationChecks every version pin against the repository’s real releases and branches, and fills an empty pin with the latest releaseNo
3ProvenanceChecks that every repository is reachable and records the exact commit each skill is built fromNo
4Skill LoopBuilds each Tier A skill in order through brief (BS), compile (CS) and test (TS). Export waits for stage 9Only when a skill’s dependencies have not completed
5Tier B BatchBuilds every Tier B skill in one Quick Skill batch run (skf-quick-skill --batch)No
6CapstoneComposes one stack skill from the completed skills that clear the quality gate, using skf-create-stack-skill in compose modeNo
7VerificationRuns skf-verify-stack against your architecture document and records the overall verdict and coverage it reportsOnly to ask for a missing architecture document
8RefinementRuns skf-refine-architecture, which writes a refined copy of your architecture document based on the skillsOnly to ask for a missing architecture document
9ExportLists each completed skill with its quality-gate verdict, names the ones the gate leaves out and, once you approve, runs skf-export-skill on each pass and fallback skill to package it and update your context filesYes: [E]xport all or [C]ancel
10MaintenanceWrites campaign-report.md, runs your on_complete hook if you set one, then runs the end-of-workflow health checkNo

Verification and Refinement use the architecture document path you gave at Setup. Without one, they look for docs/architecture.md and then _bmad-output/planning-artifacts/architecture.md. A headless run with no architecture document skips both stages. If either stage fails, the failure is logged and the campaign carries on.


A machine-readable summary of the campaign, written during Setup by the same helper call that writes _campaign-state.yaml, from the targets Setup checked (what you enter, or --brief or --manifest). It holds the campaign name; each library’s repository URL, tier, version pin and dependencies, and any language or scope hint its source gave; the quality gate; the architecture document path; and your notes. Later stages read the repository URLs from it, and a fresh session uses it to pick the campaign back up.

The single source of truth for campaign progress. Every write goes through one helper, campaign-state.py, which validates the state before and after the change, copies the valid state it read to _campaign-state.yaml.bak (never an invalid one), and writes the new state atomically, through a temporary file renamed over the old one, so a crash leaves the old state or the new one, never half of one. It takes every timestamp from the clock, in UTC. All progress lives in this file, not in the conversation.

An optional Markdown file of standing instructions for the whole campaign. You give its path at Setup. Each stage that uses it re-reads it when the stage starts, so edits you make between stages take effect. It can hold ## Quality Overrides, ## Skip List, ## Pipeline Flags and ## Notes sections, and any other heading is read as general guidance. See the campaign directive spec for the full format.

Everything the campaign writes about itself sits in one folder: forge-data/_campaign/ by default ({forge_data_folder}/_campaign). It holds _campaign-state.yaml and its .bak backup, campaign-brief.yaml, the decision log, the Tier B batch input, the archive/ folder and campaign-report.md. To move it, set campaign_workspace_path (see Customization).

This log only grows. It records every decision in the campaign, whether you made it or a headless run took the default: skip or force at a dependency gate, overwrite, export or cancel at the Export gate, recovery from the backup, a cancel at any other question, and why a skill failed. Each entry is one timestamped line with its type: decision (your choice at a question), auto (the default a headless run took) or event (what happened without a choice, such as a failed skill). campaign status shows its last lines.

Campaign orders skills by their depends_on lists, so if skill B depends on skill A, skill A is built first. A dependency counts as met only when that skill has completed. If it has not, the Skill Loop stops at the waiting skill and asks you to [S]kip it, [F]orce it anyway, or [H]alt, which stops the campaign with exit code 13 (dependency-blocked) and leaves it ready to resume. A headless run never forces: it skips the waiting skill when every unmet dependency failed or was skipped, so the skills that do not depend on it still run, and halts with exit code 13 only while a dependency is still pending or active. A circular or unknown dependency stops the campaign at Strategy with exit code 4.

You choose a tier for each library at Setup.

  • Tier A gets the full pipeline during the Skill Loop: brief (BS), compile (CS) and test (TS). The tested skills that clear the quality gate are exported together at the Export stage.
  • Tier B is built in one batch by Quick Skill (skf-quick-skill --batch) during the Tier B Batch stage. It is faster and gives best-effort skills, with no separate test step.

Do not make a Tier A library depend on a Tier B library. A dependency counts as met only once that skill has completed, and Tier B skills are built after the Skill Loop, so the Tier A skill could never pass the dependency gate. Strategy rejects such a plan with exit code 4 and names each Tier A skill and the Tier B skill it depends on (tier_inversions): make the dependency Tier A, or drop it.

Campaign ships a customize.toml file that you can tune without forking the skill. When the campaign starts, it merges three layers: the bundled customize.toml, then a team override committed at _bmad/custom/skf-campaign.toml, then a personal override at _bmad/custom/skf-campaign.user.toml that git ignores. Both overrides sit under your project root. A single value in a later layer replaces the earlier one, and list entries add up. You can set:

  • Quality-gate values: quality_gate_hard, quality_gate_soft_target and quality_gate_soft_fallback (see Quality Gates below).
  • campaign_workspace_path: moves the whole campaign folder (state, backup, brief, batch input, archive, report and decision log), for example to a shared volume. Empty means the default, {forge_data_folder}/_campaign.
  • persistent_facts: plain sentences or file: references (globs allowed) added to every skill’s kickoff message, so house style and guardrails reach the whole campaign. By default it loads every project-context.md in your project. A file: path with no glob character that names no file stops the campaign at its first Tier A kickoff with exit code 2, while a glob that matches nothing adds nothing. To drop the default, add persistent_facts = ["!file:{project-root}/**/project-context.md"] to a team or personal override: an entry that starts with ! adds nothing and removes every earlier entry it names, so those files stay out of every Tier A kickoff.
  • Template overrides: report_template_path, kickoff_template_path and brief_template_path point to your own copies of the templates.
  • on_complete: a command run at the end of the campaign, after its final state write. It gets --report-path=<path> only when the report was written. If it fails, the failure is logged and the campaign still succeeds.
  • activation_steps_prepend / activation_steps_append: extra steps run before or after activation, for org-wide checks or loading context.

These overrides need BMAD Method’s customization script, which the BMAD Method installer adds. In a project with SKF alone, the campaign ignores _bmad/custom/ and uses the bundled values, prints [activation/warn] customization_resolver_unavailable: not found and, except for campaign status, logs that event in its decision log. See Workflows: Customizing a Workflow for the settings every workflow shares.

See the skill’s SKILL.md for the full merge contract.


One quality bar applies to every skill in the campaign. It has three parts:

  • Hard gate (zero-critical-high): a skill may have no critical or high-severity findings, such as a wrong or fabricated signature or a broken reference. The test step (skf-test-skill) fails a skill that has any. The campaign marks that skill failed and writes the reason to the decision log. Skills that depend on it wait at the dependency gate, and the other skills carry on. A missing export does not trip this gate: Test Skill rates it Medium, so it lowers the skill’s score instead (see Gap severities).
  • Soft target (default 90%): the score a skill should reach.
  • Soft fallback (default 80%): the floor. A skill that scores at or above the fallback but below the target still clears the gate, as fallback. A skill below the fallback is not exported.

You can change all three through customization. The campaign brief and a directive’s ## Quality Overrides section take precedence when the campaign runs. A Tier A skill completes only on a Test Skill PASS, with --threshold set to its effective soft target. Export then enforces the gate: a skill that scores at or above the soft target is pass, one at or above the soft fallback is fallback, and anything else is not exported. A Tier B skill is classified by its skill-check score, and one without a score is not exported.


Campaign expects the AI session to end before the campaign does, for example when it runs out of context. Its progress lives on disk in _campaign-state.yaml, so it survives a lost session, a timeout or a machine restart.

When you resume:

  1. Ferris validates the state file against the campaign schema. When it fails, campaign-state.py recover copies the backup over it, but only when the backup validates
  2. Completed skills are skipped automatically
  3. If a skill was in progress, the campaign returns to it (the Skill Loop for Tier A, the Tier B Batch for Tier B). Otherwise it starts the stage after the last stage that finished
  4. All cross-skill context (dependency graph, quality scores, provenance records) is restored from disk

A campaign that already finished has nothing to resume: Ferris says so, and a headless resume prints the campaign’s success line again. A Setup that stopped after writing the state but before the brief halts the resume with exit code 8 (missing-brief): run campaign and choose overwrite to start again.

Use --from=<skill> to resume at a specific skill. If that skill already finished (completed, failed or skipped), Ferris asks whether to [R]e-run it, jump to the [N]ext unfinished skill, or [H]alt. Headless runs take [N]ext, so they never re-run a finished skill. An unknown skill name stops the campaign with exit code 2.

  • Running campaign when a state file or its backup already exists asks whether to resume or overwrite. Overwrite first runs campaign-state.py archive, which moves _campaign-state.yaml, its .bak and campaign-brief.yaml into archive/<name>-<UTC timestamp>/, and records this in the decision log, so a new campaign never silently replaces an old one. Setup itself refuses to overwrite a state file or a backup. Headless runs resume by default; they archive and overwrite only when --brief or --manifest seeds a new campaign.
  • Damaged state file, good backup: resume runs campaign-state.py recover, which copies the .bak over the state file only when the backup validates, and logs the recovery. If the backup is also unusable, the campaign stops with exit code 9 (corrupt-state) and reports both errors. Every write keeps a valid state in the backup and replaces the state file in one step, so a crash during a write never leaves a state file older than its backup.

A finished campaign leaves:

  • Individual skills: one for each library that completed. The Export stage exports the ones that clear the quality gate; the others stay on disk and in the report.
  • One capstone stack skill: composed from the completed skills that clear the quality gate. The Export stage exports only the individual skills, so run skf-export-skill on the stack skill yourself when you want it packaged.
  • Verification report and refined architecture: from Verification and Refinement, when an architecture document was found.
  • Campaign report (campaign-report.md): the quality gate; a table of every skill with its tier, status, score, pin and workarounds; a Stages section with whether the capstone, verification and refinement stages completed, were skipped or failed, and why a stage failed; an Export Gate section with each completed skill’s verdict, whether Export exported it or why its export failed, and the skills the gate kept from export; the lowest, highest and average scores; time per skill; and a section on failed or skipped skills.
  • Decision log (_campaign-decision-log.md): every decision made during the campaign.
  • Headless result line (SKF_CAMPAIGN_RESULT_JSON): printed in headless mode for scripts to read, as the run’s last line. Besides the counts and the score of each skill, it carries export_verdicts (each completed skill’s verdict at Export), skills_excluded (the completed skills the gate kept from export), skills_exported (the skills Export exported) and export_failures (each skill whose export failed, with its halt_reason and exit_code). The last two are null when the Export stage did not run. An export_failures entry’s exit_code is null when the export printed no line (its halt_reason is then no-result) or one Campaign could not read (result-unreadable). A failed export does not fail the campaign, which still ends with status success, so a CI job that needs every skill exported checks export_failures. It also carries stage_outcomes: for the capstone, verification and refinement stages, completed, skipped (a stage skipped on purpose, such as verification without an architecture document) or failed, with the failure’s halt_reason (no-result when the sub-skill printed no line, result-unreadable when Campaign could not read it, otherwise the sub-skill’s own) and exit_code. It is null when the capstone stage did not run, and a failed stage does not fail the campaign either, so a pipeline can tell a failed verification from a skipped one.

The Export stage (stage 9) asks before it writes anything. It lists each completed skill with its verdict, names the skills it leaves out because they fall below the soft fallback or have no score, and waits for [E]xport all or [C]ancel. [E]xport all exports only the pass and fallback skills, each under the skill folder its build wrote: a Tier B skill under the name Quick Skill gave it, the library’s name, even when you named the target differently. In headless mode it exports them without asking.


Campaign can run unattended across several sessions. Add --headless / -H, or set headless_mode: true in _bmad/_memory/forger-sidecar/preferences.yaml. --brief and --manifest turn headless mode on for you and supply the targets: a seeded run is meant for CI, so it takes every default. To review the plan and the export, give the same file at the Setup question of an interactive run instead. In headless mode every question takes its default answer. Each step prints a one-line JSON progress event to stderr when it starts and when it ends ({"stage":N,"name":"<slug>","status":"start|done"}), or {"stage":N,"name":"<slug>","status":"halt","exit":<code>} when it stops on an error. At the end, the health check displays the SKF_CAMPAIGN_RESULT_JSON line as the run’s last line. The line follows skf-campaign-result-envelope.v1.json, and its success form carries exit_code 0 and a null halt_reason.

  • Cancel: at any question between Setup and the Export gate, type cancel, exit or :q to stop cleanly. The campaign stops with exit code 12 (user-cancelled), logs the cancel, and leaves its state ready to resume. The Export gate is the exception: its own [C]ancel exits with code 11 (export-cancelled).
  • Exit codes: every stop on an error exits with a fixed code, so scripts can branch on the kind of failure without reading the message: 0 success, 2 invalid-input (for example no targets, a target Setup rejects, gh not available, or an unknown --from skill), 3 invalid-state, 4 circular-deps (a dependency cycle, an unknown dependency, or a Tier A skill that depends on a Tier B skill), 5 invalid-pin, 6 inaccessible-repo, 7 dependency-deadlock, 8 missing-brief, 9 corrupt-state, 10 report-failure (the campaign still completes; only the report is missing), 11 export-cancelled, 12 user-cancelled, 13 dependency-blocked (a [H]alt at a skill whose dependency has not completed; the campaign can resume).
  • Error line: when the campaign stops on an error, it prints the SKF_CAMPAIGN_RESULT_JSON line on stderr in its error form. The line carries every field the schema requires, with status: "error", exit_code and halt_reason, the counts so far and the other fields empty or null, plus phase (the step where it stopped) and an error object holding code and message, which only a halt carries.

The full exit-code table and result-line formats are in the skill’s campaign contracts, under “Exit Codes” and “Result Contract on HARD HALT”.


Campaign time grows with the number of libraries. Each Tier A skill runs the full brief, compile and test chain, and Tier B skills run in one batch. A campaign is meant to span several sessions: its state is on disk, so you can stop after any skill and resume later without losing progress. What affects total time:

  • Skill count and tier mix: Tier A skills, each with a full pipeline, take most of the time. Tier B batch processing is lighter per skill.
  • Dependency depth: in a deep chain of dependencies, each skill waits for the ones before it. A wide, shallow graph splits more easily across sessions.
  • Capstone breadth: composing the stack skill (stage 6) takes longer the more skills it combines.
  • Forge tier: at Deep tier (ast-grep, GitHub CLI and QMD), each skill spends more time on QMD knowledge search.

Plan a campaign as work over several sessions, not one sitting. The campaign saves its state to disk as it goes, so losing the session between skills costs you nothing.


  • Workflows: pipeline mode, headless mode and circuit breakers
  • forge-auto: one command that builds and exports a single skill. A campaign runs a similar chain for each Tier A library and exports them together at the end
  • BMAD Synergy: where campaign fits in BMAD Phase 3 (Solutioning)