use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BlockContext {
#[serde(default)]
pub owner: BlockOwner,
#[serde(default)]
pub cause: BlockCause,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum BlockOwner {
WorkspaceGate,
Engine,
Operator,
#[default]
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum BlockCause {
WorkspaceCheck,
Grant,
SecretScan,
ContractBug,
FixCycleCap,
UntrustedValidator,
ValidatorTamper,
ReviewerIndependence,
Validation,
Authentication,
Operator,
#[default]
#[serde(other)]
Unknown,
}
impl BlockContext {
pub const WORKSPACE_GATE: Self = Self {
owner: BlockOwner::WorkspaceGate,
cause: BlockCause::WorkspaceCheck,
};
pub const OPERATOR: Self = Self {
owner: BlockOwner::Operator,
cause: BlockCause::Operator,
};
pub const fn engine(cause: BlockCause) -> Self {
Self {
owner: BlockOwner::Engine,
cause,
}
}
pub fn is_workspace_gate(self) -> bool {
self == Self::WORKSPACE_GATE
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MissionStatus {
Planning,
Approved,
Running,
Paused,
Blocked,
Validating,
Complete,
Failed,
Abandoned,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Mission {
pub id: String,
pub goal: String,
pub validation_contract: Vec<Assertion>,
pub milestones: Vec<Milestone>,
pub status: MissionStatus,
pub created_at: DateTime<Utc>,
pub base_branch: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_sha: Option<String>,
pub mission_branch: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub command_grants: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub touch_set: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub deny_exceptions: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub egress_grants: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub executor_route: Option<ExecutorRoute>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub standards_manifest: Option<StandardsPin>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reviewer_independence: Option<ReviewerIndependence>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Plan {
pub goal: String,
pub validation_contract: Vec<Assertion>,
pub milestones: Vec<PlanMilestone>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub considered_alternatives: Option<ConsideredAlternatives>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub command_grants: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub touch_set: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub standards_manifest: Option<Box<StandardsPin>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reviewer_independence: Option<ReviewerIndependence>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConsideredAlternatives {
pub chosen: String,
pub rejected: Vec<RejectedAlternative>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RejectedAlternative {
pub approach: String,
pub trade_off: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum StandardsPinSource {
RepoTracked,
ExternalPinned,
}
impl StandardsPinSource {
pub fn as_str(&self) -> &'static str {
match self {
Self::RepoTracked => "repo-tracked",
Self::ExternalPinned => "external-pinned",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PinnedRule {
pub id: String,
pub revision: u64,
pub rfc: String,
pub level: String,
pub effective_status: String,
pub statement: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub domains: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub stages: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub when_paths: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub task_classes: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub checker: Option<String>,
#[serde(default)]
pub waivable: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PinnedGate {
pub id: String,
pub command: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub when_paths: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StandardsPin {
pub pack_name: String,
pub pack_dir: String,
pub standards_root: String,
pub digest: String,
pub source: StandardsPinSource,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub task_class: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub touch_set: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub context_paths: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub gates: Vec<PinnedGate>,
pub rules: Vec<PinnedRule>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StandardsRuleRef {
pub id: String,
pub revision: u64,
pub effective_status: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanMilestone {
pub title: String,
pub features: Vec<PlanFeature>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanFeature {
pub title: String,
pub spec: String,
pub validation_criteria: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MilestoneStatus {
Pending,
Active,
Validating,
Complete,
Blocked,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Milestone {
pub id: String,
pub title: String,
pub features: Vec<Feature>,
pub status: MilestoneStatus,
pub fix_cycles: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start_sha: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub validator_guidance: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FeatureOrigin {
Plan,
Fix,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FeatureStatus {
Pending,
Active,
Complete,
Skipped,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Feature {
pub id: String,
pub title: String,
pub spec: String,
pub validation_criteria: Vec<String>,
pub origin: FeatureOrigin,
pub status: FeatureStatus,
pub worker_runs: Vec<String>,
pub commits: Vec<String>,
pub respawns: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CandidateLink {
pub unit: String,
pub index: u32,
pub count: u32,
pub backend: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DivergenceCandidate {
pub run_id: String,
pub branch: String,
pub backend: String,
pub tree: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Role {
Orchestrator,
Worker,
ValidatorScrutiny,
ValidatorFunctional,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RunResult {
Pass,
Fail,
Partial,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TokenUsage {
pub input: u64,
pub output: u64,
pub cache_read: u64,
pub cache_write: u64,
}
impl TokenUsage {
pub fn add(&mut self, other: &TokenUsage) {
self.input += other.input;
self.output += other.output;
self.cache_read += other.cache_read;
self.cache_write += other.cache_write;
}
}
fn default_quant() -> String {
"n/a".to_string()
}
fn is_zero(value: &u32) -> bool {
*value == 0
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkerRun {
pub id: String,
pub role: Role,
#[serde(skip_serializing_if = "Option::is_none")]
pub feature_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub milestone_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub candidate: Option<CandidateLink>,
pub sdk_session_id: String,
pub model: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub backend: Option<BackendKind>,
#[serde(default = "default_quant")]
pub quant: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub weight_hash: Option<String>,
pub started_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ended_at: Option<DateTime<Utc>>,
pub tokens: TokenUsage,
#[serde(skip_serializing_if = "Option::is_none")]
pub cost_usd: Option<f64>,
pub transcript_path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<RunResult>,
#[serde(skip_serializing_if = "Option::is_none")]
pub report: Option<WorkerReport>,
pub prompt_hash: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum AssertionCheck {
Command,
AgentJudgement,
PtyScript,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Assertion {
pub id: String,
pub statement: String,
pub check: AssertionCheck,
#[serde(skip_serializing_if = "Option::is_none")]
pub command: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pty_script: Option<PtyScript>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub negative_control: Option<crate::contract_controls::ControlSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PtyScript {
pub command: String,
#[serde(default)]
pub steps: Vec<PtyStep>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeout_secs: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "kebab-case")]
pub enum PtyStep {
Send { text: String },
Expect {
pattern: String,
#[serde(default)]
regex: bool,
#[serde(rename = "timeoutMs")]
#[serde(default, skip_serializing_if = "Option::is_none")]
timeout_ms: Option<u64>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkerReport {
pub result: RunResult,
pub summary: String,
#[serde(default)]
pub files_touched: Vec<String>,
#[serde(default)]
pub tests_added: Vec<String>,
#[serde(default)]
pub test_evidence: String,
#[serde(default)]
pub dependencies_added: Vec<String>,
#[serde(default)]
pub known_gaps: Vec<String>,
#[serde(default)]
pub commits: Vec<String>,
#[serde(default)]
pub commands_run: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub escalation: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub questions: Option<Vec<ReportQuestion>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReportQuestion {
pub text: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub options: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Finding {
pub subject: String,
pub severity: String,
pub evidence: String,
#[serde(default)]
pub suggested_fix: String,
#[serde(default)]
pub class: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rule: Option<RuleCitation>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RuleCitation {
pub id: String,
pub revision: u64,
pub source: String,
pub digest: String,
pub lifecycle: String,
pub level: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub checker: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ValidatorReport {
#[serde(default)]
pub findings: Vec<Finding>,
#[serde(default)]
pub summary: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase", default)]
pub struct WorkspacePin {
pub provider: String,
pub template: String,
pub version: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase", default)]
pub struct WorkspaceLifecycle {
pub state: String,
pub ts: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase", default)]
pub struct ProvisionedPreview {
pub name: String,
pub url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auth: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MissionState {
pub mission: Mission,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub feature_base_shas: BTreeMap<String, String>,
pub runs: BTreeMap<String, WorkerRun>,
pub totals: TokenUsage,
pub total_cost_usd: f64,
pub pending_user_messages: Vec<String>,
pub recent_decisions: Vec<String>,
pub config: MissionConfig,
#[serde(default)]
pub latest_plan_revision: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pending_revision: Option<PendingRevision>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pending_grant_request: Option<PendingGrantRequest>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub pending_questions: Vec<PendingQuestion>,
#[serde(default, skip_serializing_if = "is_zero")]
pub question_count: u32,
pub last_seq: u64,
#[serde(default)]
pub escalated_milestones: u32,
#[serde(default)]
pub local_executor_milestones: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workspace_provider: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workspace_pin: Option<WorkspacePin>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workspace_lifecycle: Option<WorkspaceLifecycle>,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
pub resolved_divergence_units: BTreeSet<String>,
}
impl MissionState {
pub fn escalation_rate(&self) -> f64 {
if self.local_executor_milestones == 0 {
0.0
} else {
self.escalated_milestones as f64 / self.local_executor_milestones as f64
}
}
pub fn executor_tier(&self) -> ExecutorTier {
self.config.executor_tier()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PendingRevision {
pub revision: u32,
pub plan: Plan,
pub instructions: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum GrantKind {
#[default]
Command,
TouchPath,
WorkerDeny,
Egress,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PendingGrantRequest {
pub milestone_id: String,
#[serde(default)]
pub kind: GrantKind,
pub command: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PendingQuestion {
pub question_id: String,
pub role: Role,
pub text: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub options: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub run_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub feature_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub milestone_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RoleConfig {
pub model: String,
pub reasoning_effort: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_turns: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_budget_usd: Option<f64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub backend: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub acp_command: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub acp_args: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_budget: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub temperature: Option<f64>,
#[serde(default)]
pub sandbox: SandboxConfig,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum SandboxEnforce {
#[default]
Off,
Fs,
#[serde(rename = "fs+net")]
FsNet,
}
impl SandboxEnforce {
pub fn as_str(self) -> &'static str {
match self {
SandboxEnforce::Off => "off",
SandboxEnforce::Fs => "fs",
SandboxEnforce::FsNet => "fs+net",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum SandboxProvider {
#[default]
Process,
Container,
}
impl SandboxProvider {
pub fn as_str(self) -> &'static str {
match self {
SandboxProvider::Process => "process",
SandboxProvider::Container => "container",
}
}
pub fn enforces_hard_net_boundary(self, egress: &[String]) -> bool {
match self {
SandboxProvider::Process => true,
SandboxProvider::Container => egress.is_empty(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase", default)]
pub struct SandboxConfig {
pub enforce: SandboxEnforce,
#[serde(default)]
pub provider: SandboxProvider,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub image: Option<String>,
pub extra_write: Vec<String>,
pub egress: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CandidateSpec {
pub backend: String,
pub model: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BackendKind {
Claude,
Codex,
Droid,
Kimi,
Local,
Acp,
Cursor,
}
impl BackendKind {
pub fn as_str(self) -> &'static str {
match self {
BackendKind::Claude => "claude",
BackendKind::Codex => "codex",
BackendKind::Droid => "droid",
BackendKind::Kimi => "kimi",
BackendKind::Local => "local",
BackendKind::Acp => "acp",
BackendKind::Cursor => "cursor",
}
}
pub fn supports_sandbox_enforcement(self) -> bool {
match self {
BackendKind::Claude => true,
BackendKind::Codex
| BackendKind::Droid
| BackendKind::Kimi
| BackendKind::Local
| BackendKind::Acp
| BackendKind::Cursor => false,
}
}
pub fn reports_cache_read_tokens(self) -> bool {
match self {
BackendKind::Claude | BackendKind::Codex | BackendKind::Droid | BackendKind::Cursor => {
true
}
BackendKind::Kimi | BackendKind::Local | BackendKind::Acp => false,
}
}
pub fn reports_cache_write_tokens(self) -> bool {
match self {
BackendKind::Claude | BackendKind::Droid | BackendKind::Cursor => true,
BackendKind::Codex | BackendKind::Kimi | BackendKind::Local | BackendKind::Acp => false,
}
}
pub fn supports_hook_status_signals(self) -> bool {
match self {
BackendKind::Cursor => true,
BackendKind::Claude
| BackendKind::Codex
| BackendKind::Droid
| BackendKind::Kimi
| BackendKind::Local
| BackendKind::Acp => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ExecutorTier {
Local,
Frontier,
}
impl Default for ExecutorTier {
fn default() -> Self {
ExecutorTier::Frontier
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase", default)]
pub struct RoutingConfig {
pub task_class_rules: Vec<TaskClassRoute>,
#[serde(default)]
pub pattern_rules: Vec<PatternRoute>,
}
impl RoutingConfig {
pub fn is_empty(&self) -> bool {
self.task_class_rules.is_empty() && self.pattern_rules.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskClassRoute {
pub task_class: String,
pub tier: ExecutorTier,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PatternRoute {
pub pattern: String,
pub tier: ExecutorTier,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecutorRoute {
pub tier: ExecutorTier,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rule: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum WorkerIsolation {
#[default]
Worktree,
Checkout,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase", default)]
pub struct WorkspaceConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub remote: Option<RemoteWorkspaceConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub teardown_mode: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase", default)]
pub struct RemoteWorkspaceConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub template: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token_env: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub idle_after_hours: Option<f64>,
}
pub const DEFAULT_RUBBER_STAMP_THRESHOLD_MS: u64 = 10_000;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase", default)]
pub struct HookStatusConfig {
pub enabled: bool,
pub endpoint: String,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default, deny_unknown_fields)]
pub struct ReviewerIndependence {
pub scrutiny: bool,
pub functional: bool,
}
impl ReviewerIndependence {
pub fn is_empty(&self) -> bool {
!self.scrutiny && !self.functional
}
pub fn requires(&self, role: Role) -> bool {
match role {
Role::ValidatorScrutiny => self.scrutiny,
Role::ValidatorFunctional => self.functional,
_ => false,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct MissionConfig {
pub orchestrator: RoleConfig,
pub worker: RoleConfig,
pub validator_scrutiny: RoleConfig,
pub validator_functional: RoleConfig,
pub skip_scrutiny: bool,
pub skip_functional: bool,
#[serde(default, skip_serializing_if = "ReviewerIndependence::is_empty")]
pub reviewer_independence: ReviewerIndependence,
pub max_fix_cycles_per_milestone: u32,
pub max_respawns: u32,
pub max_parallel_workers: u32,
pub event_stream_throttle_ms: u64,
pub planning_idle_release_minutes: u64,
pub auto_work: bool,
pub considered_alternatives_feature_threshold: usize,
pub considered_alternatives_touch_set_threshold: usize,
pub considered_alternatives_high_usd_threshold: f64,
pub deny_patterns: Vec<String>,
pub allow_validator_commands: Vec<String>,
pub dangerously_allow_all: bool,
pub allow_below_default_worker_model: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub claude_binary: Option<String>,
pub worker_isolation: WorkerIsolation,
#[serde(default)]
pub contract_env_passthrough: Vec<String>,
#[serde(default)]
pub workspace: WorkspaceConfig,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pack_dir: Option<String>,
pub rubber_stamp_threshold_ms: u64,
#[serde(default)]
pub worker_candidates: Vec<CandidateSpec>,
#[serde(default)]
pub routing: RoutingConfig,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hook_status: Option<HookStatusConfig>,
#[serde(default)]
pub validator_allow_uncontained_degrade: bool,
#[serde(default)]
pub local_backend_allowed_hosts: Vec<String>,
}
impl Default for MissionConfig {
fn default() -> Self {
MissionConfig {
orchestrator: RoleConfig {
model: "opus".into(),
reasoning_effort: "high".into(),
max_turns: None,
max_budget_usd: Some(20.0),
tools: vec![],
backend: None,
base_url: None,
context_budget: None,
temperature: None,
acp_command: None,
acp_args: vec![],
sandbox: SandboxConfig::default(),
},
worker: RoleConfig {
model: "sonnet".into(),
reasoning_effort: "medium".into(),
max_turns: Some(50),
max_budget_usd: Some(10.0),
tools: vec![],
backend: None,
base_url: None,
context_budget: None,
temperature: None,
acp_command: None,
acp_args: vec![],
sandbox: SandboxConfig::default(),
},
validator_scrutiny: RoleConfig {
model: "opus".into(),
reasoning_effort: "high".into(),
max_turns: Some(40),
max_budget_usd: Some(10.0),
tools: vec![],
backend: None,
base_url: None,
context_budget: None,
temperature: None,
acp_command: None,
acp_args: vec![],
sandbox: SandboxConfig::default(),
},
validator_functional: RoleConfig {
model: "sonnet".into(),
reasoning_effort: "medium".into(),
max_turns: Some(40),
max_budget_usd: Some(5.0),
tools: vec![],
backend: None,
base_url: None,
context_budget: None,
temperature: None,
acp_command: None,
acp_args: vec![],
sandbox: SandboxConfig::default(),
},
skip_scrutiny: false,
skip_functional: false,
reviewer_independence: ReviewerIndependence::default(),
max_fix_cycles_per_milestone: 2,
max_respawns: 2,
max_parallel_workers: 1,
event_stream_throttle_ms: 250,
planning_idle_release_minutes: 30,
auto_work: false,
considered_alternatives_feature_threshold: 4,
considered_alternatives_touch_set_threshold: 4,
considered_alternatives_high_usd_threshold: 0.0,
deny_patterns: vec![],
allow_validator_commands: vec![],
dangerously_allow_all: false,
allow_below_default_worker_model: false,
claude_binary: None,
worker_isolation: WorkerIsolation::Worktree,
contract_env_passthrough: vec![],
workspace: WorkspaceConfig::default(),
pack_dir: None,
rubber_stamp_threshold_ms: DEFAULT_RUBBER_STAMP_THRESHOLD_MS,
worker_candidates: vec![],
routing: RoutingConfig::default(),
hook_status: None,
validator_allow_uncontained_degrade: false,
local_backend_allowed_hosts: Vec::new(),
}
}
}
impl MissionConfig {
pub fn isolation(&self) -> WorkerIsolation {
self.worker_isolation
}
pub fn role(&self, role: Role) -> &RoleConfig {
match role {
Role::Orchestrator => &self.orchestrator,
Role::Worker => &self.worker,
Role::ValidatorScrutiny => &self.validator_scrutiny,
Role::ValidatorFunctional => &self.validator_functional,
}
}
pub fn backend_kind(&self, role: Role) -> BackendKind {
match self.role(role).backend.as_deref() {
Some("codex") => BackendKind::Codex,
Some("droid") => BackendKind::Droid,
Some("kimi") => BackendKind::Kimi,
Some("local") => BackendKind::Local,
Some("acp") => BackendKind::Acp,
Some("cursor") => BackendKind::Cursor,
_ => BackendKind::Claude,
}
}
pub fn executor_tier(&self) -> ExecutorTier {
if self.worker_candidates.is_empty()
&& self.backend_kind(Role::Worker) == BackendKind::Local
{
ExecutorTier::Local
} else {
ExecutorTier::Frontier
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum ControlCommand {
Pause,
Resume,
Msg {
text: String,
interrupt: bool,
},
ConfigChange {
patch: serde_json::Value,
},
RequestRevision {
instructions: String,
},
ApproveRevision {
revision: u32,
},
RejectRevision {
revision: u32,
},
ApproveGrant {
command: String,
},
DenyGrant {
command: String,
reason: String,
},
AnswerQuestion {
#[serde(rename = "questionId")]
question_id: String,
answer: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
option: Option<u32>,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn commands_run_backcompat_defaults_empty() {
let json = r#"{"result": "pass", "summary": "did the thing"}"#;
let report: WorkerReport = serde_json::from_str(json).unwrap();
assert!(report.commands_run.is_empty());
}
#[test]
fn routing_abstraction_worker_report_escalation_is_additive() {
let json = r#"{"result": "pass", "summary": "did the thing"}"#;
let report: WorkerReport = serde_json::from_str(json).unwrap();
assert_eq!(report.escalation, None);
let value = serde_json::to_value(&report).unwrap();
assert!(value.get("escalation").is_none());
let json = r#"{"result": "partial", "summary": "s", "escalation": "spec ambiguity beyond my confidence"}"#;
let report: WorkerReport = serde_json::from_str(json).unwrap();
assert_eq!(
report.escalation.as_deref(),
Some("spec ambiguity beyond my confidence")
);
let value = serde_json::to_value(&report).unwrap();
assert_eq!(value["escalation"], "spec ambiguity beyond my confidence");
}
#[test]
fn question_events_worker_report_questions_are_additive() {
let json = r#"{"result": "pass", "summary": "did the thing"}"#;
let report: WorkerReport = serde_json::from_str(json).unwrap();
assert_eq!(report.questions, None);
let value = serde_json::to_value(&report).unwrap();
assert!(value.get("questions").is_none());
let json = r#"{
"result": "partial",
"summary": "blocked on a human choice",
"questions": [
{ "text": "Which storage engine?", "options": ["sqlite", "in-memory"] },
{ "text": "What should the flag be called?" }
]
}"#;
let report: WorkerReport = serde_json::from_str(json).unwrap();
let questions = report.questions.as_ref().expect("questions parsed");
assert_eq!(questions.len(), 2);
assert_eq!(questions[0].text, "Which storage engine?");
assert_eq!(questions[0].options, vec!["sqlite", "in-memory"]);
assert_eq!(questions[1].options, Vec::<String>::new());
let value = serde_json::to_value(&report).unwrap();
assert_eq!(value["questions"][0]["options"][1], "in-memory");
assert!(value["questions"][1].get("options").is_none());
}
#[test]
fn question_events_answer_control_kind_wire_shape() {
let cmd = ControlCommand::AnswerQuestion {
question_id: "q-1".into(),
answer: "sqlite".into(),
option: Some(0),
};
let json = serde_json::to_value(&cmd).unwrap();
assert_eq!(json["kind"], "answer-question");
assert_eq!(json["questionId"], "q-1");
assert_eq!(json["answer"], "sqlite");
assert_eq!(json["option"], 0);
let back: ControlCommand = serde_json::from_value(json).unwrap();
match back {
ControlCommand::AnswerQuestion {
question_id,
answer,
option,
} => {
assert_eq!(question_id, "q-1");
assert_eq!(answer, "sqlite");
assert_eq!(option, Some(0));
}
_ => panic!("wrong variant"),
}
let cmd = ControlCommand::AnswerQuestion {
question_id: "q-2".into(),
answer: "call it --cache-dir".into(),
option: None,
};
let json = serde_json::to_value(&cmd).unwrap();
assert!(
!json.as_object().unwrap().contains_key("option"),
"option must not serialize when None: {json}"
);
let sparse: ControlCommand = serde_json::from_str(
r#"{"kind":"answer-question","questionId":"q-2","answer":"call it --cache-dir"}"#,
)
.unwrap();
match sparse {
ControlCommand::AnswerQuestion { option, .. } => assert_eq!(option, None),
_ => panic!("wrong variant"),
}
}
#[test]
fn finding_class_round_trips_through_serde() {
let finding = Finding {
subject: "a-1".to_string(),
severity: "major".to_string(),
evidence: "wrote outside touch-set".to_string(),
suggested_fix: String::new(),
class: "out-of-contract-write".to_string(),
rule: None,
};
let json = serde_json::to_value(&finding).unwrap();
assert_eq!(json["class"], "out-of-contract-write");
let round_tripped: Finding = serde_json::from_value(json).unwrap();
assert_eq!(round_tripped.class, "out-of-contract-write");
}
#[test]
fn finding_class_backcompat_defaults_empty() {
let json = r#"{
"subject": "a-1",
"severity": "major",
"evidence": "it broke"
}"#;
let finding: Finding = serde_json::from_str(json).unwrap();
assert_eq!(finding.class, "");
}
}