Four precision primitives for multi-day, multi-agent runs.
- jouleclaw-contract: ValidationContract / Assertion / Feature /
Evidence / verify() -> ContractVerdict. Authored before code,
lives independently of implementation. Typed coverage matrix
with per-assertion rollup; must-have gaps block ship.
- jouleclaw-graph: RunEvent::HandoffRecorded variant +
WorkerHandoffEvent + HandoffCommand (BLAKE3 over NUL-joined
argv). Forces every agent to write down what happened; hash
chain protects against post-hoc edits.
- jouleclaw-mission: three-role Mission { contract, run,
evidence } with async Worker trait. Orchestrator authors,
Worker does, Validator judges. Serial-by-default dispatch;
validate() is a pure function of (contract, evidence).
- jouleclaw-hydra-client: ValueReport companion to CallReport.
Tracks closure rate (delivered / attempted) per call;
mean_closure_rate aggregates across a run. The activity-vs-
value gap closed at the dispatch layer.
Spec gains normative sections 16 (contract) / 17 (handoff) /
18 (mission) / 19 (value report). Vocab v1.jsonld + v1.ttl +
v1.shapes.ttl extended with the new classes, enumerations,
properties, and SHACL shapes.
Drive-by: jouleclaw-stack had jouleclaw-prov as a dev-dep while
using it in library code; promoted to [dependencies].
Tests: contract 17/17, graph 40/40 (+2 new), mission 6/6,
hydra-client 20/20 (+4 new). Two runnable examples
(missions_pass, three_role_demo).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
185 lines
6.3 KiB
Rust
185 lines
6.3 KiB
Rust
//! End-to-end: Orchestrator authors a contract + a graph, a
|
|
//! Worker runs each feature, the Validator returns the verdict.
|
|
//!
|
|
//! Run with: `cargo run -p jouleclaw-mission --example three_role_demo`
|
|
|
|
use async_trait::async_trait;
|
|
use jouleclaw_contract::{
|
|
Acceptance, Assertion, AssertionCheck, EvidenceItem, Feature, Priority,
|
|
ValidationContract,
|
|
};
|
|
use jouleclaw_graph::{
|
|
Edge, MergeStrategy, Node, NodeId, NodeKind, RunGraph, WorkerHandoffEvent,
|
|
};
|
|
use jouleclaw_mission::{
|
|
Mission, MissionOptions, Worker, WorkerAssignment, WorkerOutcome,
|
|
};
|
|
|
|
// One Worker stand-in. Real implementations route to an L1 tool,
|
|
// an L3 model, or a human-in-the-loop.
|
|
struct DemoWorker;
|
|
|
|
#[async_trait]
|
|
impl Worker for DemoWorker {
|
|
async fn run(
|
|
&self,
|
|
a: WorkerAssignment,
|
|
) -> Result<WorkerOutcome, Box<dyn std::error::Error + Send + Sync>> {
|
|
// Pretend each feature emits the evidence its claimed
|
|
// assertion requires.
|
|
let mut ev = Vec::new();
|
|
for c in &a.feature.claims {
|
|
if let Some(assertion) = a.contract.assertion(c) {
|
|
match &assertion.check {
|
|
AssertionCheck::Command {
|
|
command_hash,
|
|
expected_exit,
|
|
} => ev.push(EvidenceItem::CommandRun {
|
|
command_hash: command_hash.clone(),
|
|
exit_code: *expected_exit,
|
|
}),
|
|
AssertionCheck::Artifact { path, expected_hash } => {
|
|
ev.push(EvidenceItem::ArtifactPresent {
|
|
path: path.clone(),
|
|
hash: expected_hash
|
|
.clone()
|
|
.unwrap_or_else(|| "ff".repeat(32)),
|
|
})
|
|
}
|
|
AssertionCheck::Tool { tool_id, args_hash } => {
|
|
ev.push(EvidenceItem::ToolResult {
|
|
tool_id: tool_id.clone(),
|
|
args_hash: args_hash.clone(),
|
|
passed: true,
|
|
})
|
|
}
|
|
AssertionCheck::Checker { checker_id } => {
|
|
ev.push(EvidenceItem::CheckerResult {
|
|
checker_id: checker_id.clone(),
|
|
passed: true,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(WorkerOutcome {
|
|
handoff: WorkerHandoffEvent {
|
|
worker: a.worker_id.clone(),
|
|
node_id: None,
|
|
feature_id: Some(a.feature.id.clone()),
|
|
closed_at: 1_700_000_000,
|
|
completed: vec![format!("did {}", a.feature.title)],
|
|
undone: vec![],
|
|
commands: vec![],
|
|
issues_found: vec![],
|
|
procedures_followed: vec!["AGENTS.md".into()],
|
|
},
|
|
evidence: ev,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
// ── Orchestrator: author the contract + the graph ────────────
|
|
let contract = ValidationContract::seal(
|
|
"Three-role mission demo",
|
|
"did:key:zOrchestrator",
|
|
vec![
|
|
Assertion {
|
|
id: "A-1".into(),
|
|
title: "Build passes".into(),
|
|
rationale: "cargo test -p target".into(),
|
|
priority: Priority::MustHave,
|
|
check: AssertionCheck::Command {
|
|
command_hash: "build-hash".into(),
|
|
expected_exit: 0,
|
|
},
|
|
},
|
|
Assertion {
|
|
id: "A-2".into(),
|
|
title: "Receipt journal written".into(),
|
|
rationale: String::new(),
|
|
priority: Priority::MustHave,
|
|
check: AssertionCheck::Artifact {
|
|
path: "out/receipts.jsonl".into(),
|
|
expected_hash: None,
|
|
},
|
|
},
|
|
],
|
|
vec![
|
|
Feature {
|
|
id: "F-build".into(),
|
|
title: "Build".into(),
|
|
description: String::new(),
|
|
claims: vec!["A-1".into()],
|
|
},
|
|
Feature {
|
|
id: "F-receipts".into(),
|
|
title: "Wire receipts".into(),
|
|
description: String::new(),
|
|
claims: vec!["A-2".into()],
|
|
},
|
|
],
|
|
Acceptance::default(),
|
|
)
|
|
.expect("contract seals");
|
|
|
|
let graph = RunGraph {
|
|
entry: NodeId(1),
|
|
terminals: vec![NodeId(2)],
|
|
nodes: vec![
|
|
Node {
|
|
id: NodeId(1),
|
|
kind: NodeKind::Stage("F-build".into()),
|
|
label: "F-build".into(),
|
|
merge: MergeStrategy::AllRequired,
|
|
},
|
|
Node {
|
|
id: NodeId(2),
|
|
kind: NodeKind::Stage("F-receipts".into()),
|
|
label: "F-receipts".into(),
|
|
merge: MergeStrategy::AllRequired,
|
|
},
|
|
],
|
|
edges: vec![Edge {
|
|
from: NodeId(1),
|
|
to: NodeId(2),
|
|
label: None,
|
|
}],
|
|
};
|
|
|
|
let mut mission = Mission::start(
|
|
contract,
|
|
graph,
|
|
"demo",
|
|
MissionOptions {
|
|
strict_workers: true,
|
|
},
|
|
)
|
|
.expect("mission starts");
|
|
|
|
// ── Workers: dispatched serially by the convenience helper ───
|
|
let worker = DemoWorker;
|
|
let verdict = mission
|
|
.run_serial(&worker, "did:key:zWorker")
|
|
.await
|
|
.expect("workers run");
|
|
|
|
// ── Validator: the verdict ───────────────────────────────────
|
|
println!("contract: {}", verdict.contract_hash);
|
|
println!("passed: {}", verdict.passed);
|
|
println!("summary: {}", verdict.summary);
|
|
println!("handoffs: {}", mission.run.journal.entries().len() - 1);
|
|
println!(
|
|
"coverage: {} verified / {} should-have-gaps / {} blocking",
|
|
verdict
|
|
.coverage
|
|
.assertion_rollup
|
|
.values()
|
|
.filter(|r| matches!(r, jouleclaw_contract::AssertionRollup::Verified))
|
|
.count(),
|
|
verdict.should_have_gaps.len(),
|
|
verdict.blocking_gaps.len(),
|
|
);
|
|
}
|