Campaigns
pretorin campaign runs bounded compliance work across a frozen set of items.
A Campaign is a local parent Plan; every independently executable
item has a deterministic local child Plan. Embedded Hatchet dispatches those
children and an isolated process using the selected Codex or OpenCode harness
executes each one through the existing Pretorin MCP tools.
Evidence, mappings, narratives, objective results, Issues, review jobs, and human decisions remain authoritative on the Pretorin platform. Campaign state contains operational identifiers and receipts, not copies of that compliance content. There is no platform Campaign model or Campaign-specific LLM.
First run
Choose the shared harness, verify its runtime, then start a Campaign. Codex is the default. Recipe activation is optional for the baseline best-effort pass, but required before an unattended child may execute a particular recipe:
# Install the local orchestration dependencies once. Omit --force for a fresh
# uv tool installation; combine with builtin-agent if both features are wanted.
uv tool install --force 'pretorin[campaign-runner]'
pretorin agent configure --agent codex
pretorin campaign doctor --agent codex
pretorin campaign install --agent codex
# Optional: customize the recipe pins that automatic work preflight will preserve.
# Use the canonical system UUID returned by `pretorin --json context list`.
pretorin recipe activate \
code-evidence-capture workspace-capture evidence-narrative-compose issue-create \
--system <system-uuid> --framework cmmc-l1
pretorin campaign start initial-pass \
--system <system-uuid> \
--framework cmmc-l1 \
--all-controls \
--agent codex
The explicit --agent is optional: agent configure saves the selection used
by both interactive agent runs and new Campaigns. An explicit flag wins over
PRETORIN_AGENT_HARNESS, which wins over the saved default; with none of those,
Codex is selected. On a fresh configuration, an explicit campaign install --agent codex|opencode saves that choice; an interactive install without the
flag prompts and saves the answer. Installing the alternate harness later does
not replace the shared default.
To use OpenCode, save or pass a concrete model:
pretorin agent configure \
--agent opencode \
--model openai/gpt-5.6-terra \
--reasoning-effort low
pretorin campaign start initial-pass \
--system <system-uuid> \
--framework cmmc-l1 \
--all-controls \
--agent opencode
campaign start accepts --agent, --model, --model-provider, and
--reasoning-effort low|medium|high. A Codex
Campaign inherits Pretorin’s configured model when no model override is given.
An OpenCode Campaign requires a concrete model from its saved harness settings,
OPENCODE_MODEL, or --model; this prevents a durable Campaign from depending
on an unreported OpenCode default. Explicit model and reasoning options override
the selected harness’s saved values. When reasoning effort is unset, the
provider/harness default remains in effect. Pretorin maps an OpenCode effort to
its native --variant and validates it against that attempt’s authenticated
model metadata before every model launch. Because OpenCode models need not
offer all three portable variants, an unsupported model/effort pair fails
closed rather than silently reverting to the model default. That check runs
after any approved runtime installation but before the draft is activated,
then repeats inside every worker attempt against its current credential
snapshot.
The authenticated OpenCode metadata probe currently requires POSIX
process-group isolation, so OpenCode Campaign reasoning-effort overrides fail
closed on Windows; an unset effort continues to use OpenCode’s native default.
campaign start validates authentication, runtime support, and scope; runs the
same ordinary preflight used by interactive agent tasks exactly once; freezes
its resolver verdicts and first-party recipe pins with the roster; creates or
strictly reconciles the parent and deterministic
child Plans; displays the roster, expected writes, and review policy; asks for
one confirmation; resolves or installs the selected runtime; dispatches the
work; and opens the read-only observer. Use --yes for a non-interactive
confirmation or --detach to return after dispatch.
When a scope has never been provisioned, work preflight seeds all runnable
official recipes through the existing provisioning engine. Existing explicit
pins are preserved. An unattended Campaign worker may execute only recipes whose
source, version, and complete executable content hash are pinned for the exact
system/framework. Children never honor later project or user shadowing and
cannot use force to bypass a missing or drifted pin. Each attempt receives a
private, read-only copy of the same frozen preflight artifact; retries do not
rediscover sources, refresh resolver status, or change recipe pins.
If a frozen pin is missing or drifted, review and re-activate the package recipe,
then start a new Campaign; re-activation cannot rewrite an existing snapshot.
Filesystem recipe inputs are confined to the frozen project root. InSpec scans
default to local; a remote target is accepted only when that exact target was
frozen into the assignment by a trusted caller.
The confirmation authorizes only the Plan-scoped draft writes declared by the Campaign. It never authorizes control approval, risk acceptance, destructive evidence operations, or a human review decision.
Use --dry-run to resolve and display the complete draft Plan without installing
runtimes, activating or dispatching agents, or writing platform artifacts:
pretorin campaign start initial-pass \
--system "Sentinel Shield JWIP" \
--framework cmmc-l1 \
--family AC \
--dry-run
For machine-readable output, use the root --json option.
Local CMMC demo
The CMMC demo is the normal Campaign workflow, not a second orchestration
script. Seed the platform demo data, then run the CLI from the clean root of the
local Sentinel Shield clone. The directory in which campaign start runs is
frozen as the parent and child Plan workspace.
make -C /path/to/monorepo seed-demo-cmmc
cd /path/to/sentinel-shield-platform
pretorin campaign start initial-pass \
--system "Sentinel Shield JWIP" \
--framework cmmc-l1 \
--controls AC.L1-3.1.1,IA.L1-3.5.2 \
--agent opencode \
--model openai/gpt-5.6-terra \
--reasoning-effort low \
--yes --detach
pretorin campaign watch <campaign-plan-id>
That two-control scope is the inexpensive workflow smoke test. Replace
--controls with --all-controls for the complete 17-control / 59-objective
run used by the larger CMMC Level 1 demo. Use --dry-run to inspect the frozen
Plan without launching a model or writing platform artifacts. Omitting the
explicit agent, model, and reasoning options uses the shared configured harness
profile.
Campaign types
CLI names use hyphens; persisted Plan values use underscores.
| CLI type | Stored value | Purpose | Selectors | Platform review |
|---|---|---|---|---|
initial-pass | initial_pass | First best-effort assessment of controls | all controls, family, explicit controls | Not requested unless --review is passed |
interim-pass | interim_pass | Revisit selected controls or review findings | all controls, family, explicit controls, open Issues, review job | Not requested unless --review is passed |
issue-triage | issue_triage | Reconcile existing open Issues and their next actions | open Issues, with optional source/control/family filters | No automatic review request |
evidence-review | evidence_review | Review selected evidence or control evidence coverage | evidence IDs, all controls, family, explicit controls | No automatic review request |
The primary control selectors are mutually exclusive:
# Entire framework
pretorin campaign start initial-pass --system "My System" \
--framework cmmc-l1 --all-controls
# One family
pretorin campaign start initial-pass --system "My System" \
--framework cmmc-l1 --family AC
# Explicit controls
pretorin campaign start interim-pass --system "My System" \
--framework cmmc-l1 --controls AC.L1-3.1.1,IA.L1-3.5.2
# All currently open Issues
pretorin campaign start issue-triage --system "My System" \
--framework cmmc-l1 --all-open-issues
initial, issues-fix, notes-fix, and review-fix remain compatibility
aliases through the 0.29.x release line and will not be removed before 0.30.0.
New scripts should use the four names in the table. initial uses the normal
control selectors; issues-fix and notes-fix require --all-open-issues;
review-fix requires a pinned --review-job and maps to interim-pass.
Lifecycle commands
The Campaign Plan ID printed by start is the durable re-entry handle:
pretorin campaign list
pretorin campaign show <campaign-plan-id>
pretorin campaign status <campaign-plan-id>
pretorin campaign watch <campaign-plan-id>
pretorin campaign resume <campaign-plan-id>
pretorin campaign cancel <campaign-plan-id>
show returns the frozen definition and bounded execution projection. status
reports the locally persisted execution, assessment, and last-recorded platform
review aggregates; it does not query the platform. watch opens the full-screen
observer and periodically refreshes locally recorded queued/running review job
IDs from the platform. That live review state is a read-only overlay and is not
written back into the Campaign record.
resume first reconciles and adopts any persisted non-terminal Hatchet runs,
then submits work only for children without a live run. Explicitly resuming a
completed_with_errors Campaign reopens failed items and their failed Plans
only; successful children and their process records remain intact.
cancel durably requests cancellation. A live worker forwards the request to
Hatchet and records the terminal state after outstanding runs stop; orphan
recovery may acknowledge it locally. The command can therefore return before
the Campaign is terminal. It never undoes platform writes that already
committed; use status or watch to observe completion.
status, watch, and resume may omit the ID when exactly one resumable
Campaign exists for the current Git workspace, including when the command runs
from a nested directory. Resumable means queued,
running, or finalizing work, plus a completed_with_errors Campaign whose failed
items still have retry capacity. If several are resumable, Pretorin prints their
IDs and refuses to guess. Completed and cancelled Campaigns require an explicit
ID even for status or watch.
Press q in the observer to detach safely. Detaching never cancels work. Attach
again later with campaign watch <campaign-plan-id>.
What a child does
Every agent attempt receives a frozen, least-privilege assignment. In its own MCP session it:
- checks local context and calls
start_taskwith runtime-owned parent/item identity; - adopts the returned workflow and deterministic child Plan;
- reads the workflow and Plan, activates required ordered steps, and advances them through the Plan state machine;
- reads the coordinator-owned immutable preflight snapshot and its frozen active recipe set;
- uses existing typed MCP writers with child Plan attribution and logical idempotency keys;
- records an outcome for every frozen assessment-objective UUID;
- publishes each objective’s agent-authored working narrative, exact evidence
roster, and
in_progressstatus through the existing objective API, without approving the objective or parent control; - requests platform review only when the user explicitly passed
--review; and - completes the child Plan only after deterministic process gates pass.
Preflight is informative, not a readiness gate. An unavailable source produces
a visible needs_input outcome and a concrete limitation. It does not fabricate
evidence or automatically create an Issue. grounded requires a persisted
platform evidence ID; gap requires the governed Issue admission contract plus
either pre-existing scoped evidence or a server-verified in-workspace path,
bounded line range, and whole-file SHA-256. Free-form observation prose and
evidence created by the same child are not independent proof. Evidence requests
are next actions, not collected evidence.
The bootstrap prompt carries this protocol only. The selected workflow and recipes remain the playbooks the agent actually executes.
State and review
The observer deliberately keeps three axes separate:
| Axis | Values |
|---|---|
| Execution | queued, running, retrying, succeeded, failed, cancelled |
| Assessment | grounded, gap, needs input, mixed, not applicable |
| Platform review | not requested, queued, running, succeeded, failed, awaiting human review |
An item can therefore succeed as a process while reporting needs_input, or
finish its automated review while still awaiting a human decision. Campaign
completion never approves its parent control.
The observer shows the parent/child Plan hierarchy, current step, attempts and Hatchet run IDs, objective rollups, the shared preflight context, persisted resource IDs, and platform review identity. It does not show hidden reasoning, secrets, or unrestricted command output and has no approve/apply action.
Runtime management
Campaigns share the agent subsystem’s Codex/OpenCode resolver and add embedded
Hatchet SDK 1.39.0 plus engine v0.105.16. The default auto policy resolves
an explicitly configured executable first, then a compatible system binary,
then a Pretorin-managed binary. Compatibility includes the maintained minimum
version and required CLI capabilities. system permits only the configured
path or compatible PATH discovery and never downloads. managed ignores
configured and PATH executables and uses only Pretorin’s managed artifact.
Save the policy with pretorin agent configure --runtime auto|system|managed.
Managed components are platform-specific, checksum-verified, and stored under
Pretorin-owned paths. Codex uses ~/.pretorin/bin/; OpenCode uses
~/.pretorin/runtimes/opencode/<version>/<platform>/opencode; the Hatchet
sidecar uses ~/.pretorin/runtimes/hatchet/<version>/<platform>/. Installation
never invokes pip or a global npm command. Install the Python dependencies
up front with pretorin[campaign-runner] (or combine
pretorin[builtin-agent,campaign-runner]); the runtime gate requires the exact
SDK pin before resolving or installing either native runtime.
pretorin campaign version --agent codex
pretorin campaign doctor --agent codex
pretorin campaign install --agent codex
pretorin campaign version --agent opencode
Omit --agent to inspect the shared default. version reports the selected
harness source, path, version, and managed status alongside Hatchet. doctor
checks executable capability/trust, optional Python dependencies,
configuration isolation, and local-host durability. install resolves only
the selected harness plus Hatchet: it reuses compatible configured or system
binaries and downloads only the selected managed fallback. It does not mutate
the Python environment.
Preparation freezes the concrete model, optional provider label, optional
reasoning effort, Codex model endpoint or effective OpenAI/Anthropic OpenCode
provider endpoint, and the executable’s version, source, managed status, and
identity digest into the Campaign record. It also records the Hatchet engine/SDK
version and freezes the child workflow, expected outputs, and process criteria.
The confirmation and observer show these coordinates. Retry, resume, and every
worker attempt reuse and revalidate them and fail closed when the exact
endpoint, source, identity, or Plan contract is unavailable or changed; changing
the saved harness, model, reasoning effort, model URL, executable override, or
PATH does not redirect an existing Campaign.
For development compatibility, PRETORIN_OPENCODE_COMMAND remains an explicit
OpenCode command override outside the normal auto|system|managed policy and is
resolved before that policy. Unset it when validating managed-only operation.
It is reported by version and doctor; Pretorin
persists its non-secret source and identity provenance, not the command text.
The override receives the same allowlisted attempt credentials as other
OpenCode sources and is operator-trusted development code, not a process
sandbox.
Each attempt receives distinct temporary harness configuration and home state while sharing verified binaries. Credentials travel only through an explicit environment allowlist and are not serialized to harness configuration, Plan files, Campaign state, or observer events. The assigned preflight artifact is copied into that tree; canonical Plan and Campaign roots are passed explicitly rather than rediscovered from the isolated home. See Environment variables.
The model-provider credential, Pretorin platform API key, and evidence-source credentials are separate trust domains. The trusted harness process receives only the selected model provider’s allowed credential plus the current platform key and source values needed by its assigned MCP child. The MCP child retains only source names/values authorized by the exact system/framework preflight artifact. OpenCode has no native shell; Codex’s sandboxed read-only commands receive a safe environment that excludes model, platform, and source credentials, so neither harness has a general secret-inspection path. Those values are resolved fresh on every attempt, so key rotation does not change the Campaign’s frozen runtime or copy a secret into durable state.
Automatic resolver bindings explicitly scoped to another repository are excluded from this work’s preflight view, even when both repositories use the same system/framework. Non-workspace manual and platform bindings remain available, and the other repository’s bindings remain in the canonical index for later work there.
AWS evidence sources currently support direct access/session credentials,
profiles and regions, role assumption, web identity, explicit config/credential
files, and conventional ~/.aws/config and ~/.aws/credentials files. They are
passed only when the scoped preflight artifact authorizes an AWS account or an
AWS cloud_control_plane resolver; the original-home files are converted to
explicit paths before the attempt’s isolated HOME is installed. See the
complete variable list.
Local Plans default to ~/.pretorin/plans/, bounded Campaign records to
~/.pretorin/campaigns/, preflight state to
~/.pretorin/preflight/, and per-Campaign embedded Hatchet data to
~/.pretorin/campaigns/.hatchet/<campaign-plan-id>/. These locations explain
the same-host recovery boundary. Detached supervisors discard raw process
output and retain only bounded, content-free Campaign events; platform artifact
bodies do not live there.
Retry, recovery, and idempotency
Embedded Hatchet is the only queue, concurrency, retry, backoff, cancellation, and run-ID authority. The default is four concurrent items and two retries. Pretorin does not add a second semaphore or polling queue.
A retry or same-host restart reopens the same parent Plan and deterministic child Plans. Successful children are reconciled and skipped. Platform writes use a logical identity derived from the parent Plan, child Plan, structural version, step reference, and artifact identity, so replay after a lost response returns the original resource instead of creating a duplicate. Before a terminal local receipt is replayed, the typed handler revalidates the authoritative platform resource; an uncertain write remains pending or fails closed instead of trusting stale local state. Local aggregate finalization is atomic and happens once.
Execution is intentionally local-host durable. It can resume after the process restarts on the same machine, but it cannot continue while that host is off or migrate to another host. Remote execution is deferred until Plans have a shared persistence contract.
Legacy migration
The old campaign controls|policy|scope, campaign execute --checkpoint,
campaign apply, and checkpoint-path status/watch forms remain deprecated
compatibility adapters through the 0.29.x release line. They will not be removed
before 0.30.0. They are hidden from primary help and print a migration warning.
The checkpoint execute adapter is retained only for deterministic mock
fixtures; it no longer launches a real agent harness because checkpoint records
cannot carry the parent-owned shared-preflight contract. Use start or resume
for Codex/OpenCode execution.
Likewise, a Plan-backed Campaign created before the frozen preflight, worker
contract, and ordered item roster were persisted may be inspected or cancelled,
but cannot be resumed with a real harness; start a new Campaign.
| Legacy concept | Replacement |
|---|---|
| Prepare a checkpoint, then execute it | campaign start <type> |
| Checkpoint path as run identity | Campaign Plan ID |
| Local proposal queue | Existing typed MCP writers and platform state |
Generic coordinator apply | Child-owned typed writes after the one bounded authorization at start |
| Local proposal review | Existing platform review and human approval |
controls --mode initial | start initial-pass |
controls --mode issues-fix | start issue-triage |
controls --mode notes-fix | start issue-triage --all-open-issues |
controls --mode review-fix | start interim-pass --review-job ... |
Policy and scope questionnaire bulk work remains available through their domain-specific workflows and commands. Those domains are not silently mapped onto the four control/evidence Campaign types.