use alloc::{
format,
string::{String, ToString},
vec::Vec,
};
use thiserror::Error;
mod environment;
mod parser;
mod render;
mod validate;
pub use environment::check_plan_environment;
pub use parser::parse_plan;
pub use render::{render_lane, render_topology};
pub use validate::{
validate_lifecycle_topology, validate_plan_repository_path, validate_plan_structure,
};
pub const PLAN_SCHEMA: &str = "shepherd.plan/2";
pub const TOPOLOGY_SCHEMA: &str = "shepherd.plan-topology/2";
pub const PROBE_SCHEMA: &str = "shepherd.plan-probes/1";
pub const PLAN_READINESS_V1_SCHEMA: &str = "shepherd.plan-readiness/1";
pub const PLAN_READINESS_SCHEMA: &str = "shepherd.plan-readiness/2";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PlanDocument {
pub manifest: PlanManifestV2,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PlanManifestV2 {
pub schema: String,
pub run: String,
pub seed: String,
pub mesh: String,
pub planning_evidence: String,
pub goal: String,
pub deliverables: Vec<String>,
pub lanes: Vec<String>,
pub root_roles: Vec<String>,
pub child_lead_roles: Vec<String>,
pub planning_lead: String,
pub engineer_count: usize,
pub review_rejection_limit: usize,
pub fourth_rejection: String,
pub root_continuation: String,
pub exclusions: Vec<String>,
pub capacity: PlanCapacity,
pub nodes: Vec<PlanNode>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PlanCapacity {
pub logical_lane_limit: usize,
pub host_process_ceiling: usize,
pub project_spawn_max_parallel: usize,
pub plan_process_ceiling: usize,
pub parent_role_cap: usize,
pub run_budget: usize,
pub simultaneous_process_ceiling: usize,
pub per_lane_child_wave_ceiling: usize,
pub disk_min_mib: u64,
pub model_quota: usize,
pub lifecycle: Option<LifecycleCapacity>,
pub turn_strategy: Option<TurnStrategy>,
pub backpressure: String,
pub cargo_targets: Vec<LaneBinding>,
pub conductors: Vec<LaneBinding>,
pub schedule: Vec<CapacityWave>,
pub scale_outcome: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LifecycleCapacity {
pub live_concurrency_ceiling: usize,
pub retained_descendant_slots: usize,
pub lifetime_descendant_slots: Option<usize>,
pub completed_session_reclamation: SessionReclamation,
pub interrupted_session_reclamation: SessionReclamation,
pub turn_reset_behavior: TurnResetBehavior,
pub reusable_sessions: bool,
pub nested_dispatch: bool,
pub persistent_agent_cost: usize,
pub independent_reviewer_reachable: bool,
pub capability_source: String,
pub capability_evidence_sha256: String,
}
impl LifecycleCapacity {
#[must_use]
pub fn with_evidence(mut self, source: impl Into<String>) -> Self {
self.capability_source = source.into();
self.capability_evidence_sha256 = self.computed_evidence_sha256();
self
}
#[must_use]
pub fn from_evidence_artifact(
mut self,
source: impl Into<String>,
artifact: &str,
) -> Option<Self> {
self.capability_source = source.into();
let expected = self.evidence_payload();
if artifact != expected {
return None;
}
self.capability_evidence_sha256 = crate::digest::sha256_hex(artifact.as_bytes());
Some(self)
}
#[must_use]
pub fn computed_evidence_sha256(&self) -> String {
crate::digest::sha256_hex(self.evidence_payload().as_bytes())
}
#[must_use]
pub fn evidence_is_valid(&self) -> bool {
self.capability_evidence_sha256.len() == 64
&& self
.capability_evidence_sha256
.bytes()
.all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
&& self.capability_evidence_sha256 == self.computed_evidence_sha256()
}
fn evidence_payload(&self) -> String {
let lifetime = self
.lifetime_descendant_slots
.map_or_else(|| "none".into(), |limit| limit.to_string());
format!(
"source={}\nlive={}\nretained={}\nlifetime={lifetime}\ncompleted={}\ninterrupted={}\nturn-reset={}\nreusable={}\nnested={}\npersistent-cost={}\nreviewer={}\n",
self.capability_source,
self.live_concurrency_ceiling,
self.retained_descendant_slots,
self.completed_session_reclamation,
self.interrupted_session_reclamation,
self.turn_reset_behavior,
self.reusable_sessions,
self.nested_dispatch,
self.persistent_agent_cost,
self.independent_reviewer_reachable,
)
}
}
#[derive(
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
strum::AsRefStr,
strum::Display,
strum::EnumCount,
strum::EnumIs,
strum::EnumString,
strum::IntoStaticStr,
strum::VariantNames,
)]
#[strum(serialize_all = "kebab-case")]
pub enum SessionReclamation {
Immediate,
TurnBoundary,
Never,
}
#[derive(
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
strum::AsRefStr,
strum::Display,
strum::EnumCount,
strum::EnumIs,
strum::EnumString,
strum::IntoStaticStr,
strum::VariantNames,
)]
#[strum(serialize_all = "kebab-case")]
pub enum TurnResetBehavior {
ReclaimsTerminal,
PreservesTerminal,
}
#[derive(
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
strum::AsRefStr,
strum::Display,
strum::EnumCount,
strum::EnumIs,
strum::EnumString,
strum::IntoStaticStr,
strum::VariantNames,
)]
#[strum(serialize_all = "kebab-case")]
pub enum TurnStrategy {
SameTurn,
ResetBetweenPhases,
FreshRootSessionBetweenPhases,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LaneBinding {
pub lane: String,
pub value: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CapacityWave {
pub lanes: Vec<String>,
pub process_slots: usize,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PlanNode {
pub id: String,
pub seed_deliverables: Vec<String>,
pub lane: String,
pub role: String,
pub work_kind: String,
pub outcome: String,
pub owns: Vec<String>,
pub forbidden: Vec<String>,
pub consumes: Vec<String>,
pub produces: Vec<String>,
pub depends_on: Vec<String>,
pub red: GateContract,
pub green: GateContract,
pub eval: EvalContract,
pub evidence: String,
pub review: ReviewContract,
pub failure_route: String,
pub rollback: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GateContract {
pub command: Vec<String>,
pub expects: String,
pub reason: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EvalContract {
pub command: Vec<String>,
pub threshold: Option<u32>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ReviewContract {
pub role: String,
pub predicate: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VerifiedPlanSeed {
pub run: String,
pub relative_path: String,
pub mesh: String,
pub deliverables: Vec<String>,
pub outcomes: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PlanTopology {
pub schema: String,
pub run: String,
pub seed: String,
pub mesh: String,
pub planning_evidence: String,
pub goal: String,
pub deliverables: Vec<String>,
pub lanes: Vec<PlanLane>,
pub nodes: Vec<PlanNode>,
pub topological_order: Vec<String>,
pub capacity: PlanCapacity,
pub capacity_policy: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PlanLane {
pub id: String,
pub conductor: String,
pub cargo_target: String,
pub node_ids: Vec<String>,
pub deliverables: Vec<String>,
}
#[derive(
Clone,
Copy,
Debug,
Eq,
PartialEq,
strum::AsRefStr,
strum::Display,
strum::EnumCount,
strum::EnumIs,
strum::EnumString,
strum::IntoStaticStr,
strum::VariantNames,
)]
#[strum(ascii_case_insensitive, serialize_all = "snake_case")]
pub enum PathKind {
File,
Directory,
}
#[derive(
Clone,
Copy,
Debug,
Eq,
PartialEq,
strum::AsRefStr,
strum::Display,
strum::EnumCount,
strum::EnumIs,
strum::EnumString,
strum::IntoStaticStr,
strum::VariantNames,
)]
#[strum(ascii_case_insensitive, serialize_all = "snake_case")]
pub enum PathState {
Missing,
File,
Directory,
Symlink,
Other,
}
#[derive(
Clone,
Copy,
Debug,
Eq,
PartialEq,
strum::AsRefStr,
strum::Display,
strum::EnumCount,
strum::EnumIs,
strum::EnumString,
strum::IntoStaticStr,
strum::VariantNames,
)]
#[strum(ascii_case_insensitive, serialize_all = "snake_case")]
pub enum ProbeExpectation {
Modify,
Create,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PlanEnvironmentProbe {
Path {
path: String,
expectation: ProbeExpectation,
kind: PathKind,
},
Symbol {
path: String,
symbol: String,
expected_matches: usize,
},
Interface {
path: String,
schema: String,
version: String,
},
Command {
argv: Vec<String>,
expected_exit: i32,
semantic_marker: String,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PlanProbeManifest {
pub schema: String,
pub worktree_identity: String,
pub baseline: String,
pub probes: Vec<PlanEnvironmentProbe>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InterfaceObservation {
pub schema: String,
pub version: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CommandObservation {
pub exit: i32,
pub stdout: String,
pub stderr: String,
}
pub trait SourceProbe {
type Error: core::fmt::Display;
fn worktree_identity(&self) -> Result<String, Self::Error>;
fn baseline(&self) -> Result<String, Self::Error>;
fn path_state(&self, path: &str) -> Result<PathState, Self::Error>;
fn symbol_matches(&self, path: &str, symbol: &str) -> Result<usize, Self::Error>;
fn interface(&self, path: &str) -> Result<InterfaceObservation, Self::Error>;
fn run(&self, argv: &[String]) -> Result<CommandObservation, Self::Error>;
fn available_disk_mib(&self) -> Result<u64, Self::Error>;
fn model_quota(&self) -> Result<usize, Self::Error>;
fn host_process_ceiling(&self) -> Result<usize, Self::Error>;
fn project_spawn_max_parallel(&self) -> Result<usize, Self::Error>;
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PlanCheckReport {
pub schema: String,
pub run: String,
pub worktree_identity: String,
pub baseline: String,
pub probe_count: usize,
pub available_disk_mib: u64,
pub model_quota: usize,
pub host_process_ceiling: usize,
pub project_spawn_max_parallel: usize,
}
#[derive(Clone, Debug, Eq, Error, PartialEq)]
pub enum PlanError {
#[error("plan parse error: {0}")]
Parse(String),
#[error("plan structure error: {0}")]
Structure(String),
#[error("plan environment error: {0}")]
Environment(String),
#[error("unknown plan lane `{0}`")]
UnknownLane(String),
}