shepherd-core 6.7.0

The harness-agnostic shepherd engine: domain types, configuration schema, and run state. Knows nothing about any CLI, harness, or process.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
//! Deterministic plan-v2 parsing, structural validation, projection, and probe policy.
//!
//! Parsing and structure validation are pure. Filesystem and process observations enter
//! only through [`SourceProbe`], which keeps host I/O in the adapter crate.

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";
/// Current native readiness receipts use [`PLAN_READINESS_SCHEMA`]. This
/// legacy identifier remains owned by the plan crate so older doctrine and
/// persisted inspection artifacts have a typed Rust owner without becoming an
/// accepted execution schema.
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,
    /// Explicit lifecycle facts required before plan readiness can become
    /// execution authority. `None` preserves inspection of legacy plan-v2
    /// artifacts but can never authorize execution.
    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>,
}

/// Host lifecycle facts that determine whether a graph can reach both work
/// and independent review. These are capabilities, not provider identities.
#[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 {
    /// Bind declared facts to a deterministic identity. This constructor is
    /// useful for authored plans and tests; production adapter profiles must
    /// use [`Self::from_evidence_artifact`] so a declaration cannot certify
    /// itself.
    #[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
    }

    /// Admit a production profile only when the checked-in measurement
    /// artifact is byte-identical to the canonical typed facts. The artifact
    /// digest then becomes the plan's evidence digest. Any drift returns no
    /// profile and planning fails closed.
    #[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,
        )
    }
}

/// When a terminal descendant stops consuming retained host capacity.
#[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,
}

/// Whether a new host turn releases terminal descendants from the prior turn.
#[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,
}

/// The plan's explicit use, or non-use, of a supported host turn boundary.
#[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,
    /// Start the next phase in a newly bound root session. This is the
    /// fail-closed escape hatch when a host reports turn-scoped reclamation
    /// but exposes no origin-authenticated turn event to plugins.
    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),
}