Campaign Workflows
The campaign command group runs bulk compliance operations across multiple controls, policies, or scope questions in a single coordinated run. Campaigns support an external-agent-first pattern with checkpoint persistence and lease-based concurrency for safe fan-out to multiple agents.
Campaign Domains and Modes
| Domain | Mode | Description |
|---|---|---|
controls | initial | Draft new narratives and evidence for controls |
controls | issues-fix | Address platform issues on existing controls |
controls | notes-fix | Deprecated alias for issues-fix |
controls | review-fix | Fix findings from a family review job |
policy | answer | Generate answers for policy questions |
policy | review-fix | Fix findings from a policy review |
scope | answer | Generate answers for scope questions |
scope | review-fix | Fix findings from a scope review |
Control Campaigns
Draft New Narratives for a Family
pretorin campaign controls --mode initial --family AC \
--system "My System" --framework-id fedramp-moderate
Fix Controls with Platform Issues
pretorin campaign controls --mode issues-fix --all-open-issues \
--system "My System" --framework-id fedramp-moderate
Fix Controls after Family Review
pretorin campaign controls --mode review-fix --family AC --review-job <job-id> \
--system "My System" --framework-id fedramp-moderate
Options
| Option | Description |
|---|---|
--system | Target system ID or name (required) |
--framework-id | Target framework ID (required) |
--family | Control family ID or abbreviation, case-insensitive (e.g., AC, AU). List them with pretorin frameworks families <framework-id> |
--controls | Specific control IDs (comma-separated) |
--all-controls | Target all controls in the framework |
--mode | Campaign mode: initial, issues-fix, notes-fix, review-fix (required) |
--all-open-issues | Target controls with open issues in the system/framework context |
--issue-source | Optional issue source filter |
--issue-control | Optional comma-separated issue control filter |
--issue-family | Optional issue family filter |
--include-resolved | Include resolved issues in issue discovery |
--artifacts | Artifact types to generate: narratives, evidence, or both (default: both). Apply is evidence-first: narratives cite the created evidence ids, so narratives alone is refused for a control with no citable evidence — use both. |
--review-job | Review job ID (required for review-fix mode) |
--concurrency | Number of parallel workers (default: 4) |
--max-retries | Maximum retry attempts per item (default: 2) |
--checkpoint | Path to checkpoint file for resume |
--apply | Apply proposals to platform after completion |
--output | Output mode: auto, live, compact, json |
Policy Campaigns
Answer All Incomplete Policy Questions
pretorin campaign policy --mode answer --all-incomplete
Fix Policy Review Findings
pretorin campaign policy --mode review-fix --policies <policy-id>
Options
| Option | Description |
|---|---|
--policies | Specific policy IDs (comma-separated) |
--all-incomplete | Target all incomplete policies |
--mode | Campaign mode: answer, review-fix (required) |
--system | Optional system context passthrough |
--concurrency | Number of parallel workers (default: 4) |
--max-retries | Maximum retry attempts per item (default: 2) |
--checkpoint | Path to checkpoint file for resume |
--apply | Apply proposals to platform after completion |
--output | Output mode: auto, live, compact, json |
Scope Campaigns
Answer Scope Questions
pretorin campaign scope --mode answer \
--system "My System" --framework-id fedramp-moderate
Options
| Option | Description |
|---|---|
--system | Target system ID or name (required) |
--framework-id | Target framework ID (required) |
--mode | Campaign mode: answer, review-fix (required) |
--concurrency | Number of parallel workers (default: 4) |
--max-retries | Maximum retry attempts per question (default: 2) |
--checkpoint | Path to checkpoint file for resume |
--apply | Apply proposals to platform after completion |
--output | Output mode: auto, live, compact, json |
Checking Campaign Status
pretorin campaign status --checkpoint .pretorin/campaigns/controls-initial-20260808-091500.json
--checkpoint is required — campaign status reads the run’s state from that
file, so there is no active-context fallback. --output accepts the same
auto, live, compact, and json modes as the campaign commands.
When a campaign command is run without --checkpoint, the checkpoint is written
to a timestamped default path,
.pretorin/campaigns/<domain>-<mode>-<YYYYMMDD-HHMMSS>.json. The prepared-run
output prints the exact pretorin campaign status --checkpoint ... invocation
for that file; pass --checkpoint explicitly if you would rather choose the
path yourself.
Campaign Lifecycle
- Prepare — The campaign snapshots platform state and creates a local checkpoint file
- Claim — Items are claimed with TTL-based leases (safe for multiple agents)
- Draft — Each item gets full context and drafting instructions
- Propose — Proposals are submitted without writing to the platform
- Apply — All accepted proposals are pushed to the platform in one operation
Use --apply to automatically apply after completion, or run campaign status to review before applying.
Issue Admission and Evaluation
A campaign proposal may recommend an Issue only when it supplies the complete
issue-create contract: one in-scope expectation key, exact unmet expectation,
one independently observed gap with its source/workspace basis, risk basis and
ratings, a concrete clearance condition, and a non-empty minimum-evidence list.
Apply re-reads current Issues and reuses a matching expectation before writing.
Malformed evidence recommendations, unknown evidence types, missing context,
source/recipe gaps, optional advice, and subtasks remain proposal warnings; they
are never synthesized into Issues.
When a new Issue lands, apply creates one minimal draft remediation plan on that
same Issue. A plan failure produces one needs_input action and never a
replacement or child Issue. issues-fix stays bounded to existing Issues:
campaign output may propose verification, but generic apply never bypasses the
governed verification_pending → verify closure path.
Idempotency and Replay
Apply is safe to retry. Every campaign create write carries an idempotency key the platform uses to recognize a repeat of a write it already committed. Evidence keys are scoped to the campaign run; Issue keys are stable for one system/framework/control/expectation so a fresh campaign cannot create a second Issue for the same expectation.
The run id
When a campaign checkpoint is created, the CLI mints a per-run UUID and stores it
in the checkpoint file as run_id:
{
"version": 2,
"identity": { "domain": "controls", "mode": "initial", "...": "..." },
"run_id": "3f6c2a1e-9d84-4c17-8f0b-1a2b3c4d5e6f",
"idempotency_support": "supported",
"items": { "...": "..." }
}
run_id is run state, not campaign identity — it sits beside identity,
never inside it, so resuming a checkpoint still passes identity validation.
- Resuming or retrying the same checkpoint reuses the same
run_id, so keys match the earlier attempt and the platform replays it. - A deliberate fresh campaign (a new checkpoint file, or a deleted one)
mints a new
run_id, so its evidence writes are new. Issue admission still reuses the stable expectation key and current-Issue dedupe read. - Checkpoints written before this feature have no
run_id; apply mints one and flushes it to disk before its first platform write.
Do not hand-edit or copy run_id between checkpoints. Copying it into an
unrelated campaign makes that campaign’s writes collide with the original run’s
keys.
Key scheme
Evidence keys are derived per artifact and sent on each evidence batch item:
{run_id}:{item_id}:{artifact_type}:{revision}:{index}
artifact_typeisevidencefor the campaign batch path.indexis the artifact’s position in the stored proposal — not its offset in the request being sent, so a resume that re-sends only the un-receipted subset reuses each artifact’s original key.revisionis the item’s proposal revision. Submitting a new proposal for an item that already has apply receipts bumps it, so the new content writes under fresh keys. A key must map to exactly one payload for the resource’s lifetime; without this, re-drafting a failed item would reuse a key the platform had already bound to the previous draft and fail permanently.
Canonical Issue creation instead hashes this stable identity:
system_id + framework_id + control_id + expectation_key
The resulting issue-expectation:<sha256> key deliberately ignores campaign
run and proposal revision. Reusing the key with changed content conflicts rather
than silently creating a second Issue for the same expectation. Both formats are
internal to the CLI; the platform treats them as opaque strings.
Checking whether the platform enforces keys
Apply probes GET /api/v1/public/capabilities once at the start of each run and
records the answer in the checkpoint as idempotency_support:
| Value | Meaning |
|---|---|
supported | The platform enforces client keys — retries are protected |
unsupported | The platform answered but ignores keys — protection falls back to local receipts |
unknown | The probe could not get an answer (auth, rate limit, outage, network) |
unknown is deliberately distinct from unsupported: an error response says
nothing about the deployed build, so it is never read as “the platform doesn’t
support this.” The probe decides only what a run may claim about duplicate
protection. Keys are always sent regardless of the result — a platform version
that predates the feature ignores the field, so sending is free, and backing off on
a flaky probe would drop the protection exactly when the network is unreliable.
The value is re-probed every run and never trusted from a previous run’s
checkpoint: a server that started mid-migration can legitimately flip. The per-write
proof is always the item’s own replayed status, not the capability flag.
Replay semantics
| Server response | Meaning | What the CLI does |
|---|---|---|
created / ok | New resource written | Receipt records ok; downstream steps run |
replayed | Key matched an earlier write | Receipt records replayed with the original resource ids; treated as applied, but no downstream step re-fires |
idempotency_key_conflict | Same key reused with different content | Hard per-item failure; the platform’s remediation message is surfaced |
A replayed item is a success: its original evidence ids are still cited by the narrative, and a resume will not re-send it. Control campaigns do not post a separate completion note: the legacy notes endpoint has no idempotency key, so no client can guarantee exactly-once notification after a committed response is lost. The durable artifact receipts are the completion record.
An idempotency_key_conflict is not retryable — re-running apply on the same
checkpoint derives the same key and conflicts again. It means a key was reused for
different content, which normally indicates a hand-edited checkpoint. To get new
keys, either submit a fresh proposal for the item (which bumps its revision and
supersedes the conflicting content) or re-prepare the campaign into a new
checkpoint file so it mints a new run_id. The error message names both.
Keys are unique within your organization and honored for the lifetime of the resource. Platform versions that predate idempotency-key support ignore the field, so sending it is always safe; against those versions, retry protection falls back to the CLI’s local receipts, which narrow the crash/retry duplication window rather than closing it.