From 05300c8addf2cc3d8ac8b3fbcd7701639ec0f63f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 01:29:35 +0100 Subject: [PATCH] Seal immutable frontier planning results --- epiphany-core/src/agent_launch.rs | 70 ++++++ epiphany-core/src/coordinator_launch.rs | 157 +++++++++++++ .../src/coordinator_launch_context.rs | 4 + epiphany-core/src/lib.rs | 2 + epiphany-core/src/runtime_spine.rs | 210 +++++++++++++++++- epiphany-core/src/surfaces/worker_launch.rs | 2 +- .../src/bin/epiphany-openai-runtime.rs | 18 ++ epiphany-openai-runtime/src/lib.rs | 161 +++++++++++++- notes/fresh-workspace-handoff.md | 17 ++ notes/repo-frontier-planning-authority-map.md | 16 +- state/ledgers.msgpack | Bin 492623 -> 492914 bytes state/map.yaml | 2 +- 12 files changed, 645 insertions(+), 14 deletions(-) diff --git a/epiphany-core/src/agent_launch.rs b/epiphany-core/src/agent_launch.rs index b15bfcf4..6161c572 100644 --- a/epiphany-core/src/agent_launch.rs +++ b/epiphany-core/src/agent_launch.rs @@ -412,6 +412,61 @@ pub fn epiphany_role_launch_output_schema(role_id: EpiphanyRoleResultRoleId) -> schema } +pub fn epiphany_frontier_planning_output_schema() -> serde_json::Value { + let mut schema = epiphany_role_launch_output_schema(EpiphanyRoleResultRoleId::Imagination); + let properties = schema["properties"] + .as_object_mut() + .expect("role output schema properties"); + properties.remove("statePatch"); + properties.remove("selfPatch"); + properties.insert( + "frontierPlanningRequestId".to_string(), + serde_json::json!({ + "type": "string", + "minLength": 1, + "description": "Exact echo of the coordinator-bound repo frontier planning request." + }), + ); + properties.insert( + "frontierPlanCandidate".to_string(), + serde_json::json!({ + "type": "object", + "required": [ + "planning_request_id", "model_revision", "model_hash", + "frontier_item_id", "frontier_item_hash", "safe_paths", "action", "command", + "checks", "stop_conditions", "rollback_steps", "commit_message", "proposed_at" + ], + "properties": { + "planning_request_id": {"type": "string", "minLength": 1}, + "model_revision": {"type": "integer", "minimum": 0}, + "model_hash": {"type": "string", "minLength": 1}, + "frontier_item_id": {"type": "string", "minLength": 1}, + "frontier_item_hash": {"type": "string", "minLength": 1}, + "safe_paths": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, + "action": {"type": "string", "minLength": 1}, + "command": {"type": "string", "minLength": 1}, + "checks": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, + "stop_conditions": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, + "rollback_steps": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, + "commit_message": {"type": "string", "minLength": 1}, + "proposed_at": {"type": "string", "minLength": 1} + }, + "additionalProperties": false + }), + ); + schema["required"] = serde_json::json!([ + "roleId", + "verdict", + "summary", + "nextSafeMove", + "filesInspected", + "frontierPlanningRequestId", + "frontierPlanCandidate" + ]); + schema["additionalProperties"] = serde_json::Value::Bool(false); + schema +} + pub fn epiphany_reorient_launch_output_schema() -> serde_json::Value { serde_json::json!({ "type": "object", @@ -956,6 +1011,21 @@ mod tests { ); } + #[test] + fn frontier_planning_schema_exposes_candidate_without_generic_patch_mouths() { + let schema = epiphany_frontier_planning_output_schema(); + assert!( + schema["properties"] + .get("frontierPlanningRequestId") + .is_some() + ); + assert!(schema["properties"].get("frontierPlanCandidate").is_some()); + assert!(schema["properties"].get("statePatch").is_none()); + assert!(schema["properties"].get("selfPatch").is_none()); + assert!(schema["properties"].get("repoModelPatch").is_none()); + assert_eq!(schema["additionalProperties"], false); + } + #[test] fn reorient_worker_prompt_is_bounded_not_full_persistent_memory() { let prompt = build_epiphany_reorient_launch_instruction(CoreEpiphanyReorientAction::Resume); diff --git a/epiphany-core/src/coordinator_launch.rs b/epiphany-core/src/coordinator_launch.rs index be78e454..46d93164 100644 --- a/epiphany-core/src/coordinator_launch.rs +++ b/epiphany-core/src/coordinator_launch.rs @@ -838,6 +838,62 @@ pub(crate) mod tests { Ok((store, state, launch, planning)) } + fn frontier_planning_result( + planning: &RepoFrontierPlanningRequest, + job_id: &str, + proposed_at: &str, + ) -> Result { + let mut candidate = RepoFrontierPlanCandidate { + schema_version: REPO_FRONTIER_PLAN_CANDIDATE_SCHEMA_VERSION.into(), + candidate_id: String::new(), + planning_request_id: planning.request_id.clone(), + model_revision: planning.model_revision, + model_hash: planning.model_hash.clone(), + frontier_item_id: planning.frontier_item_id.clone(), + frontier_item_hash: planning.frontier_item_hash.clone(), + safe_paths: vec![planning.source_scope[0].clone()], + action: "Implement the bounded frontier plan.".into(), + command: "cargo test --lib".into(), + checks: vec!["Focused tests pass.".into()], + stop_conditions: vec!["Authority scope changes.".into()], + rollback_steps: vec!["Revert the bounded commit.".into()], + commit_message: "Implement bounded frontier plan".into(), + proposed_at: proposed_at.into(), + contract: REPO_FRONTIER_PLANNING_CONTRACT.into(), + }; + candidate.candidate_id = canonical_repo_frontier_plan_candidate_id(&candidate)?; + Ok(EpiphanyRuntimeRoleWorkerResult { + schema_version: RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION.into(), + result_id: format!("frontier-planning-result-{job_id}"), + job_id: job_id.into(), + role_id: "imagination".into(), + verdict: "draft-ready".into(), + summary: "Produced one bounded candidate.".into(), + next_safe_move: "Mind admission.".into(), + checkpoint_summary: None, + scratch_summary: None, + files_inspected: planning.source_scope.clone(), + frontier_node_ids: vec![planning.frontier_item_id.clone()], + evidence_ids: vec![planning.admission_receipt_id.clone()], + artifact_refs: Vec::new(), + open_questions: Vec::new(), + evidence_gaps: Vec::new(), + risks: Vec::new(), + state_patch_msgpack: None, + self_patch_msgpack: None, + item_error: None, + metadata: std::collections::BTreeMap::new(), + repo_model_patch_msgpack: None, + verification_request_id: None, + frontier_route_id: None, + repo_frontier_modeling_request_id: None, + proposal_modeling_request_id: None, + claim_repair_request_id: None, + frontier_planning_request_id: Some(planning.request_id.clone()), + frontier_plan_candidate_msgpack: Some(rmp_serde::to_vec_named(&candidate)?), + }) + } + fn research_launch(state: &EpiphanyThreadState) -> EpiphanyJobLaunchRequest { build_epiphany_role_launch_request( "thread-1", @@ -1321,6 +1377,107 @@ pub(crate) mod tests { Ok(()) } + #[test] + fn frontier_planning_result_is_exact_immutable_and_patch_sealed() -> Result<()> { + let root = tempfile::tempdir()?; + let (store, state, launch, planning) = + frontier_planning_launch_fixture(root.path(), "result-exact")?; + let plan = plan_coordinator_job_launch( + &state, + &launch, + &store, + "launcher-result".into(), + "backend-result".into(), + )?; + commit_coordinator_job_launch( + &store, + &planning.thread_id, + &state, + &launch, + &plan, + "2026-07-15T09:00:03Z".into(), + )?; + let result = frontier_planning_result(&planning, "backend-result", "2026-07-15T09:00:04Z")?; + put_runtime_role_worker_result(&store, &result)?; + put_runtime_role_worker_result(&store, &result)?; + let stored = runtime_role_worker_result(&store, "backend-result")? + .expect("immutable planning result"); + assert_eq!(stored, result); + let candidate = stored + .frontier_plan_candidate()? + .expect("typed candidate payload"); + assert_eq!(candidate.planning_request_id, planning.request_id); + assert_eq!( + candidate.candidate_id, + canonical_repo_frontier_plan_candidate_id(&candidate)? + ); + assert!( + coordinator_acceptance_cache(&store)? + .get_all::()? + .is_empty(), + "candidate must remain inside immutable result until Mind admission" + ); + let mut counterfeit = result; + counterfeit.summary = "different result bytes".into(); + let before = std::fs::read(&store)?; + assert!(put_runtime_role_worker_result(&store, &counterfeit).is_err()); + assert_eq!(std::fs::read(&store)?, before); + Ok(()) + } + + #[test] + fn frontier_planning_result_refuses_hostile_substitution_before_persistence() -> Result<()> { + let root = tempfile::tempdir()?; + for mutation in 0..9 { + let (store, state, launch, planning) = + frontier_planning_launch_fixture(root.path(), &format!("result-{mutation}"))?; + let job_id = format!("backend-result-{mutation}"); + let plan = plan_coordinator_job_launch( + &state, + &launch, + &store, + format!("launcher-result-{mutation}"), + job_id.clone(), + )?; + commit_coordinator_job_launch( + &store, + &planning.thread_id, + &state, + &launch, + &plan, + "2026-07-15T09:00:03Z".into(), + )?; + let mut result = frontier_planning_result(&planning, &job_id, "2026-07-15T09:00:04Z")?; + match mutation { + 0 => result.frontier_planning_request_id = Some("swapped-request".into()), + 1 => result.frontier_planning_request_id = None, + 2 => result.frontier_plan_candidate_msgpack = None, + 3 => result.role_id = "modeling".into(), + 4 => result.state_patch_msgpack = Some(vec![0]), + 5 => result.self_patch_msgpack = Some(vec![0]), + 6 => result.repo_model_patch_msgpack = Some(vec![0]), + _ => { + let mut candidate = result.frontier_plan_candidate()?.unwrap(); + if mutation == 7 { + candidate.frontier_item_hash = "adjacent-frontier".into(); + } else { + candidate.safe_paths = vec!["outside/authority".into()]; + } + result.frontier_plan_candidate_msgpack = + Some(rmp_serde::to_vec_named(&candidate)?); + } + } + let before = std::fs::read(&store)?; + assert!( + put_runtime_role_worker_result(&store, &result).is_err(), + "hostile planning result mutation {mutation} must fail" + ); + assert_eq!(std::fs::read(&store)?, before); + assert!(runtime_role_worker_result(&store, &job_id)?.is_none()); + } + Ok(()) + } + #[test] fn concurrent_frontier_planning_launches_leave_only_winner_artifacts() -> Result<()> { let root = tempfile::tempdir()?; diff --git a/epiphany-core/src/coordinator_launch_context.rs b/epiphany-core/src/coordinator_launch_context.rs index 6911fe94..2f812d4e 100644 --- a/epiphany-core/src/coordinator_launch_context.rs +++ b/epiphany-core/src/coordinator_launch_context.rs @@ -938,6 +938,8 @@ mod tests { repo_frontier_modeling_request_id: None, proposal_modeling_request_id: Some(selection.request_id), claim_repair_request_id: None, + frontier_planning_request_id: None, + frontier_plan_candidate_msgpack: None, }; crate::put_runtime_role_worker_result(store, &result)?; crate::commit_repo_model_admission( @@ -1419,6 +1421,8 @@ mod tests { repo_frontier_modeling_request_id: None, proposal_modeling_request_id: None, claim_repair_request_id: None, + frontier_planning_request_id: None, + frontier_plan_candidate_msgpack: None, }; crate::put_runtime_role_worker_result(&runtime_store, &verification_result)?; crate::put_soul_verdict_receipt( diff --git a/epiphany-core/src/lib.rs b/epiphany-core/src/lib.rs index 466c3b9b..336bc920 100644 --- a/epiphany-core/src/lib.rs +++ b/epiphany-core/src/lib.rs @@ -59,6 +59,7 @@ pub use agent_launch::build_epiphany_reorient_launch_request_with_dynamic_contex pub use agent_launch::build_epiphany_role_launch_request; pub use agent_launch::build_epiphany_role_launch_request_with_dynamic_context; pub use agent_launch::epiphany_agent_prompt_with_memory; +pub use agent_launch::epiphany_frontier_planning_output_schema; pub use agent_launch::epiphany_reorient_launch_output_schema; pub use agent_launch::epiphany_role_binding_id; pub use agent_launch::epiphany_role_label; @@ -776,6 +777,7 @@ pub use runtime_spine::RuntimeSpineJobOptions; pub use runtime_spine::RuntimeSpineJobResultOptions; pub use runtime_spine::RuntimeSpineSessionOptions; pub use runtime_spine::append_runtime_event; +pub use runtime_spine::canonical_repo_frontier_plan_candidate_id; pub use runtime_spine::clear_epiphany_job_binding_backend; pub use runtime_spine::commit_repo_frontier_modeling_request; pub use runtime_spine::commit_repo_frontier_verification_request_for_chain; diff --git a/epiphany-core/src/runtime_spine.rs b/epiphany-core/src/runtime_spine.rs index 96addcb8..ec5b61a8 100644 --- a/epiphany-core/src/runtime_spine.rs +++ b/epiphany-core/src/runtime_spine.rs @@ -48,7 +48,9 @@ use crate::repo_model_gateway::{ REPO_FRONTIER_HANDS_AUTHORITY_CONTRACT, REPO_FRONTIER_HANDS_AUTHORITY_SCHEMA_VERSION, REPO_FRONTIER_MODELING_REQUEST_CONTRACT, REPO_FRONTIER_MODELING_REQUEST_SCHEMA_VERSION, REPO_FRONTIER_PLAN_ADOPTION_SCHEMA_VERSION, REPO_FRONTIER_PLAN_CANDIDATE_SCHEMA_VERSION, - REPO_FRONTIER_PLANNING_CONTRACT, REPO_FRONTIER_PLANNING_REQUEST_SCHEMA_VERSION, + REPO_FRONTIER_PLANNING_CONTRACT, REPO_FRONTIER_PLANNING_LAUNCH_BINDING_CONTRACT, + REPO_FRONTIER_PLANNING_LAUNCH_BINDING_SCHEMA_VERSION, + REPO_FRONTIER_PLANNING_REQUEST_SCHEMA_VERSION, REPO_FRONTIER_PROPOSAL_MODELING_LAUNCH_BINDING_CONTRACT, REPO_FRONTIER_PROPOSAL_MODELING_LAUNCH_BINDING_SCHEMA_VERSION, REPO_FRONTIER_PROPOSAL_MODELING_REQUEST_CONTRACT, @@ -174,7 +176,7 @@ pub const RUNTIME_SPINE_SCHEMA_VERSION: &str = "epiphany.runtime_spine.v0"; pub const RUNTIME_WORKER_LAUNCH_REQUEST_SCHEMA_VERSION: &str = "epiphany.runtime.worker_launch_request.v1"; pub const RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION: &str = - "epiphany.runtime.role_worker_result.v1"; + "epiphany.runtime.role_worker_result.v2"; pub const RUNTIME_REORIENT_WORKER_RESULT_SCHEMA_VERSION: &str = "epiphany.runtime.reorient_worker_result.v0"; pub const COORDINATOR_RUN_RECEIPT_SCHEMA_VERSION: &str = "epiphany.coordinator_run_receipt.v0"; @@ -410,6 +412,10 @@ pub struct EpiphanyRuntimeRoleWorkerResult { pub proposal_modeling_request_id: Option, #[cultcache(key = 25, default)] pub claim_repair_request_id: Option, + #[cultcache(key = 26, default)] + pub frontier_planning_request_id: Option, + #[cultcache(key = 27, default)] + pub frontier_plan_candidate_msgpack: Option>, } impl EpiphanyRuntimeRoleWorkerResult { @@ -430,6 +436,13 @@ impl EpiphanyRuntimeRoleWorkerResult { "role worker repoModelPatch", ) } + + pub fn frontier_plan_candidate(&self) -> Result> { + decode_optional_msgpack( + self.frontier_plan_candidate_msgpack.as_deref(), + "role worker frontierPlanCandidate", + ) + } } #[derive(Clone, Debug, PartialEq, DatabaseEntry)] @@ -1457,6 +1470,11 @@ pub fn put_runtime_role_worker_result( validate_non_empty(&result.job_id, "role worker result job id")?; validate_non_empty(&result.result_id, "role worker result id")?; validate_non_empty(&result.role_id, "role worker result role id")?; + if result.schema_version != RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION { + return Err(anyhow!("role worker result schema version mismatch")); + } + let mut cache = runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; let is_verification = result.role_id == "verification"; if is_verification != (result @@ -1553,8 +1571,94 @@ pub fn put_runtime_role_worker_result( _ => {} } } - let mut cache = runtime_spine_cache(store_path)?; - cache.pull_all_backing_stores()?; + let has_planning_echo = result.frontier_planning_request_id.is_some(); + let has_planning_candidate = result.frontier_plan_candidate_msgpack.is_some(); + if has_planning_echo != has_planning_candidate { + return Err(anyhow!( + "frontier planning result requires both its request echo and typed candidate" + )); + } + if has_planning_echo { + if !result.role_id.eq_ignore_ascii_case("imagination") { + return Err(anyhow!( + "only Imagination results may echo a frontier planning request" + )); + } + if result.state_patch_msgpack.is_some() + || result.self_patch_msgpack.is_some() + || result.repo_model_patch_msgpack.is_some() + || result.verification_request_id.is_some() + || result.frontier_route_id.is_some() + || result.repo_frontier_modeling_request_id.is_some() + || result.proposal_modeling_request_id.is_some() + || result.claim_repair_request_id.is_some() + { + return Err(anyhow!( + "frontier planning result may carry only its request echo and typed candidate authority" + )); + } + let request_id = result + .frontier_planning_request_id + .as_deref() + .ok_or_else(|| anyhow!("frontier planning request echo disappeared"))?; + let request = cache + .get::(request_id)? + .ok_or_else(|| anyhow!("frontier planning result requires persisted request"))?; + validate_current_repo_frontier_planning_request(&cache, &request)?; + let candidate = result + .frontier_plan_candidate()? + .ok_or_else(|| anyhow!("frontier planning candidate disappeared"))?; + validate_repo_frontier_plan_candidate_against_request(&cache, &candidate, &request)?; + let bindings = cache + .get_all::()? + .into_iter() + .filter(|binding| binding.planning_request_id == request.request_id) + .collect::>(); + if bindings.len() != 1 { + return Err(anyhow!( + "frontier planning result requires exactly one coordinator launch binding" + )); + } + let binding = &bindings[0]; + let worker_launch = cache + .get::(&result.job_id)? + .ok_or_else(|| anyhow!("frontier planning result requires its worker launch"))?; + let launch_document = worker_launch.launch_document()?; + let projection = match &launch_document { + EpiphanyWorkerLaunchDocument::Role(document) => { + document.frontier_planning_context.as_ref() + } + EpiphanyWorkerLaunchDocument::Reorient(_) => None, + }; + let expected_projection = + crate::RepoFrontierPlanningContextProjection::from_request(&request); + let launch_hash = format!( + "{:x}", + Sha256::digest(&worker_launch.launch_document_msgpack) + ); + if binding.schema_version != REPO_FRONTIER_PLANNING_LAUNCH_BINDING_SCHEMA_VERSION + || binding.contract != REPO_FRONTIER_PLANNING_LAUNCH_BINDING_CONTRACT + || binding.binding_record_id + != format!("repo-frontier-planning-launch-{}", request.request_id) + || binding.job_id != result.job_id + || binding.binding_id != EPIPHANY_IMAGINATION_ROLE_BINDING_ID + || binding.runtime_id != request.runtime_id + || binding.thread_id != request.thread_id + || binding.worker_launch_document_sha256 != launch_hash + || worker_launch.job_id != result.job_id + || worker_launch.role != EPIPHANY_IMAGINATION_OWNER_ROLE + || worker_launch.binding_id != EPIPHANY_IMAGINATION_ROLE_BINDING_ID + || worker_launch.frontier_planning_request_id.as_deref() + != Some(request.request_id.as_str()) + || worker_launch.proposal_modeling_request_id.is_some() + || worker_launch.claim_repair_request_id.is_some() + || projection != Some(&expected_projection) + { + return Err(anyhow!( + "frontier planning result does not exactly bind request, launch, runtime, thread, and candidate" + )); + } + } if let Some(existing) = cache.get::(&result.job_id)? { return if existing == *result { Ok(()) @@ -3540,6 +3644,94 @@ pub(crate) fn put_repo_frontier_plan_candidate( put_immutable_planning_entry(store_path, &candidate.candidate_id, candidate) } +fn validate_repo_frontier_plan_candidate_against_request( + cache: &CultCache, + candidate: &RepoFrontierPlanCandidate, + request: &RepoFrontierPlanningRequest, +) -> Result<()> { + if candidate.schema_version != REPO_FRONTIER_PLAN_CANDIDATE_SCHEMA_VERSION + || candidate.contract != REPO_FRONTIER_PLANNING_CONTRACT + || candidate.selected_fields_invalid() + || candidate.planning_request_id != request.request_id + || candidate.model_revision != request.model_revision + || candidate.model_hash != request.model_hash + || candidate.frontier_item_id != request.frontier_item_id + || candidate.frontier_item_hash != request.frontier_item_hash + { + return Err(anyhow!( + "frontier planning candidate substituted request identity or required cargo" + )); + } + let expected_candidate_id = canonical_repo_frontier_plan_candidate_id(candidate)?; + if candidate.candidate_id != expected_candidate_id { + return Err(anyhow!("frontier planning candidate id is not canonical")); + } + let admission = cache + .get::(&request.admission_receipt_id)? + .ok_or_else(|| anyhow!("frontier planning candidate requires exact admission receipt"))?; + if admission.schema_version != REPO_MODEL_ADMISSION_RECEIPT_SCHEMA_VERSION + || admission.contract != REPO_MODEL_ADMISSION_CONTRACT + || admission.admitted_revision != request.model_revision + || admission.admitted_hash != request.model_hash + { + return Err(anyhow!( + "frontier planning candidate admission binding mismatch" + )); + } + let entry = cache + .get::(crate::MEMORY_GRAPH_KEY)? + .ok_or_else(|| anyhow!("frontier planning candidate requires canonical model"))?; + let model = entry.snapshot()?; + if model.model_revision != request.model_revision + || crate::memory_graph_model_hash(&model)? != request.model_hash + { + return Err(anyhow!("frontier planning candidate model is stale")); + } + let item = model + .frontier + .iter() + .find(|item| item.id == request.frontier_item_id) + .ok_or_else(|| anyhow!("frontier planning candidate frontier disappeared"))?; + if format!("{:x}", Sha256::digest(rmp_serde::to_vec_named(item)?)) != request.frontier_item_hash + || item.source_scope != request.source_scope + || !candidate.safe_paths.iter().all(|path| { + request.source_scope.iter().any(|scope| { + path == scope + || path.starts_with(&format!("{}/", scope.trim_end_matches(['/', '\\']))) + }) + }) + { + return Err(anyhow!( + "frontier planning candidate exceeds exact frontier authority" + )); + } + Ok(()) +} + +pub fn canonical_repo_frontier_plan_candidate_id( + candidate: &RepoFrontierPlanCandidate, +) -> Result { + let semantic_bytes = rmp_serde::to_vec_named(&( + &candidate.planning_request_id, + candidate.model_revision, + &candidate.model_hash, + &candidate.frontier_item_id, + &candidate.frontier_item_hash, + &candidate.safe_paths, + &candidate.action, + &candidate.command, + &candidate.checks, + &candidate.stop_conditions, + &candidate.rollback_steps, + &candidate.commit_message, + &candidate.proposed_at, + ))?; + Ok(format!( + "repo-frontier-plan-candidate-{:x}", + Sha256::digest(semantic_bytes) + )) +} + impl RepoFrontierPlanCandidate { fn selected_fields_invalid(&self) -> bool { self.candidate_id.trim().is_empty() @@ -6980,6 +7172,8 @@ pub(crate) mod tests { repo_frontier_modeling_request_id: None, proposal_modeling_request_id: Some(selection.request_id), claim_repair_request_id: None, + frontier_planning_request_id: None, + frontier_plan_candidate_msgpack: None, }; let review = RepoModelAdmissionReview { schema_version: REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION.to_string(), @@ -7109,6 +7303,8 @@ pub(crate) mod tests { repo_frontier_modeling_request_id: None, proposal_modeling_request_id: None, claim_repair_request_id: Some(repair.request_id.clone()), + frontier_planning_request_id: None, + frontier_plan_candidate_msgpack: None, }; let review = RepoModelAdmissionReview { schema_version: REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION.into(), @@ -8382,6 +8578,8 @@ pub(crate) mod tests { repo_frontier_modeling_request_id: None, proposal_modeling_request_id: None, claim_repair_request_id: None, + frontier_planning_request_id: None, + frontier_plan_candidate_msgpack: None, }; put_runtime_role_worker_result(&store, &verification_result)?; let verdict = SoulVerdictReceipt { @@ -8501,6 +8699,8 @@ pub(crate) mod tests { repo_frontier_modeling_request_id: Some(fixture.modeling_request.request_id.clone()), proposal_modeling_request_id: None, claim_repair_request_id: None, + frontier_planning_request_id: None, + frontier_plan_candidate_msgpack: None, }; let review = RepoModelAdmissionReview { schema_version: REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION.to_string(), @@ -8777,6 +8977,8 @@ pub(crate) mod tests { repo_frontier_modeling_request_id: None, proposal_modeling_request_id: None, claim_repair_request_id: None, + frontier_planning_request_id: None, + frontier_plan_candidate_msgpack: None, }; put_runtime_role_worker_result(&fixture.store, &adjacent)?; put_soul_verdict_receipt( diff --git a/epiphany-core/src/surfaces/worker_launch.rs b/epiphany-core/src/surfaces/worker_launch.rs index c00b2e73..5bddb549 100644 --- a/epiphany-core/src/surfaces/worker_launch.rs +++ b/epiphany-core/src/surfaces/worker_launch.rs @@ -24,7 +24,7 @@ use crate::EpiphanyReorientFreshnessStatus; use crate::EpiphanyReorientPressureLevel; use crate::EpiphanyReorientReason; -pub const ROLE_WORKER_OUTPUT_CONTRACT_ID: &str = "epiphany.worker.role_result.v1"; +pub const ROLE_WORKER_OUTPUT_CONTRACT_ID: &str = "epiphany.worker.role_result.v2"; pub const REORIENT_WORKER_OUTPUT_CONTRACT_ID: &str = "epiphany.worker.reorient_result.v0"; pub const REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_SCHEMA_VERSION: &str = "epiphany.worker.repo_frontier_proposal_modeling_context.v0"; diff --git a/epiphany-openai-runtime/src/bin/epiphany-openai-runtime.rs b/epiphany-openai-runtime/src/bin/epiphany-openai-runtime.rs index 629ec269..deaa0401 100644 --- a/epiphany-openai-runtime/src/bin/epiphany-openai-runtime.rs +++ b/epiphany-openai-runtime/src/bin/epiphany-openai-runtime.rs @@ -971,6 +971,9 @@ mod tests { state_revision: 1, objective: Some("Verify the worker loop.".to_string()), dynamic_prompt_context: None, + proposal_modeling_context: None, + claim_repair_context: None, + frontier_planning_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: Vec::new(), @@ -992,6 +995,9 @@ mod tests { "role", epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID, ), + proposal_modeling_request_id: None, + claim_repair_request_id: None, + frontier_planning_request_id: None, created_at: now(), }, )?; @@ -1051,6 +1057,9 @@ mod tests { state_revision: 1, objective: Some("Verify runtime error sealing.".to_string()), dynamic_prompt_context: None, + proposal_modeling_context: None, + claim_repair_context: None, + frontier_planning_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: Vec::new(), @@ -1072,6 +1081,9 @@ mod tests { "role", epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID, ), + proposal_modeling_request_id: None, + claim_repair_request_id: None, + frontier_planning_request_id: None, created_at: now(), }, )?; @@ -1119,6 +1131,9 @@ mod tests { state_revision: 1, objective: Some("Verify the worker loop ceiling.".to_string()), dynamic_prompt_context: None, + proposal_modeling_context: None, + claim_repair_context: None, + frontier_planning_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: Vec::new(), @@ -1140,6 +1155,9 @@ mod tests { "role", epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID, ), + proposal_modeling_request_id: None, + claim_repair_request_id: None, + frontier_planning_request_id: None, created_at: now(), }, )?; diff --git a/epiphany-openai-runtime/src/lib.rs b/epiphany-openai-runtime/src/lib.rs index d38d89ab..564d41a8 100644 --- a/epiphany-openai-runtime/src/lib.rs +++ b/epiphany-openai-runtime/src/lib.rs @@ -1298,7 +1298,11 @@ fn worker_output_schema_json(document: &EpiphanyWorkerLaunchDocument) -> Result< EpiphanyWorkerLaunchDocument::Role(document) => { let role_id = role_result_id_for_launch_role(&document.role_id) .with_context(|| format!("unknown role launch id {:?}", document.role_id))?; - epiphany_core::epiphany_role_launch_output_schema(role_id) + if document.frontier_planning_context.is_some() { + epiphany_core::epiphany_frontier_planning_output_schema() + } else { + epiphany_core::epiphany_role_launch_output_schema(role_id) + } } EpiphanyWorkerLaunchDocument::Reorient(_) => { epiphany_core::epiphany_reorient_launch_output_schema() @@ -1323,8 +1327,13 @@ fn role_result_id_for_launch_role( fn worker_output_contract_text(document: &EpiphanyWorkerLaunchDocument) -> &'static str { match document { + EpiphanyWorkerLaunchDocument::Role(document) + if document.frontier_planning_context.is_some() => + { + "Required frontier-planning result fields: roleId, verdict, summary, nextSafeMove, filesInspected, frontierPlanningRequestId, frontierPlanCandidate. Echo the exact request and candidate identity from the typed launch context. Do not emit statePatch, selfPatch, or repoModelPatch." + } EpiphanyWorkerLaunchDocument::Role(_) => { - "Required role-result fields: roleId, verdict, summary, nextSafeMove, filesInspected. Modeling workers must include repoModelPatch; Imagination workers must include statePatch. Modeling statePatch is optional observations/evidence only. Use arrays for frontierNodeIds, evidenceIds, openQuestions, evidenceGaps, risks, and artifactRefs when present." + "Required role-result fields: roleId, verdict, summary, nextSafeMove, filesInspected. Modeling workers must include repoModelPatch; ordinary Imagination workers must include statePatch. Modeling statePatch is optional observations/evidence only. Use arrays for frontierNodeIds, evidenceIds, openQuestions, evidenceGaps, risks, and artifactRefs when present." } EpiphanyWorkerLaunchDocument::Reorient(_) => { "Required reorient-result fields: mode, summary, nextSafeMove. Include checkpointStillValid, filesInspected, frontierNodeIds, evidenceIds, openQuestions, and continuityRisks when present." @@ -1355,6 +1364,27 @@ struct RoleWorkerResultIngress { frontier_route_id: Option, repo_frontier_modeling_request_id: Option, proposal_modeling_request_id: Option, + claim_repair_request_id: Option, + frontier_planning_request_id: Option, + frontier_plan_candidate: Option, +} + +#[derive(Debug, Clone, Default, serde::Deserialize)] +#[serde(default)] +struct RepoFrontierPlanCandidateIngress { + planning_request_id: String, + model_revision: u64, + model_hash: String, + frontier_item_id: String, + frontier_item_hash: String, + safe_paths: Vec, + action: String, + command: String, + checks: Vec, + stop_conditions: Vec, + rollback_steps: Vec, + commit_message: String, + proposed_at: String, } #[derive(Debug, Clone, Default, serde::Deserialize)] @@ -1450,6 +1480,37 @@ fn role_worker_result_from_ingress( encode_optional_document(&result.repo_model_patch, "repoModelPatch"); let (self_patch_msgpack, self_patch_error) = encode_optional_document(&result.self_patch, "selfPatch"); + let (frontier_plan_candidate_msgpack, frontier_plan_candidate_error) = if let Some(ingress) = + result.frontier_plan_candidate.as_ref() + { + let mut candidate = epiphany_core::RepoFrontierPlanCandidate { + schema_version: epiphany_core::REPO_FRONTIER_PLAN_CANDIDATE_SCHEMA_VERSION.to_string(), + candidate_id: String::new(), + planning_request_id: ingress.planning_request_id.clone(), + model_revision: ingress.model_revision, + model_hash: ingress.model_hash.clone(), + frontier_item_id: ingress.frontier_item_id.clone(), + frontier_item_hash: ingress.frontier_item_hash.clone(), + safe_paths: clean_string_vec(&ingress.safe_paths), + action: ingress.action.trim().to_string(), + command: ingress.command.trim().to_string(), + checks: clean_string_vec(&ingress.checks), + stop_conditions: clean_string_vec(&ingress.stop_conditions), + rollback_steps: clean_string_vec(&ingress.rollback_steps), + commit_message: ingress.commit_message.trim().to_string(), + proposed_at: ingress.proposed_at.trim().to_string(), + contract: epiphany_core::REPO_FRONTIER_PLANNING_CONTRACT.to_string(), + }; + match epiphany_core::canonical_repo_frontier_plan_candidate_id(&candidate) { + Ok(candidate_id) => { + candidate.candidate_id = candidate_id; + encode_optional_document(&Some(candidate), "frontierPlanCandidate") + } + Err(error) => (None, Some(format!("frontierPlanCandidate: {error}"))), + } + } else { + (None, None) + }; EpiphanyRuntimeRoleWorkerResult { schema_version: epiphany_core::RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION.to_string(), result_id: result_id.to_string(), @@ -1475,8 +1536,11 @@ fn role_worker_result_from_ingress( state_patch_msgpack, self_patch_msgpack, item_error: merge_optional_errors( - merge_optional_errors(state_patch_error, self_patch_error), - repo_model_patch_error, + merge_optional_errors( + merge_optional_errors(state_patch_error, self_patch_error), + repo_model_patch_error, + ), + frontier_plan_candidate_error, ), metadata: std::collections::BTreeMap::new(), repo_model_patch_msgpack, @@ -1488,6 +1552,11 @@ fn role_worker_result_from_ingress( proposal_modeling_request_id: clean_optional_string( result.proposal_modeling_request_id.as_deref(), ), + claim_repair_request_id: clean_optional_string(result.claim_repair_request_id.as_deref()), + frontier_planning_request_id: clean_optional_string( + result.frontier_planning_request_id.as_deref(), + ), + frontier_plan_candidate_msgpack, } } @@ -1626,6 +1695,8 @@ mod tests { epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID, ), proposal_modeling_request_id: None, + claim_repair_request_id: None, + frontier_planning_request_id: None, }; let result = role_worker_result_from_ingress( &launch, @@ -1645,6 +1716,80 @@ mod tests { Ok(()) } + #[test] + fn frontier_planning_ingress_derives_typed_candidate_identity() -> Result<()> { + let parsed = parse_assistant_json::( + r#"{ + "roleId":"imagination", + "verdict":"draft-ready", + "summary":"bounded plan", + "nextSafeMove":"Mind admission", + "frontierPlanningRequestId":"planning-request-1", + "frontierPlanCandidate":{ + "planning_request_id":"planning-request-1", + "model_revision":7, + "model_hash":"model-hash", + "frontier_item_id":"frontier-1", + "frontier_item_hash":"frontier-hash", + "safe_paths":["src"], + "action":"Implement the bounded cut.", + "command":"cargo test --lib", + "checks":["focused test passes"], + "stop_conditions":["scope changes"], + "rollback_steps":["revert commit"], + "commit_message":"Implement bounded cut", + "proposed_at":"2026-07-15T10:00:00Z" + } + }"#, + )?; + let launch = EpiphanyRuntimeWorkerLaunchRequest { + schema_version: epiphany_core::RUNTIME_WORKER_LAUNCH_REQUEST_SCHEMA_VERSION.to_string(), + job_id: "planning-job-1".into(), + binding_id: epiphany_core::EPIPHANY_IMAGINATION_ROLE_BINDING_ID.into(), + role: epiphany_core::EPIPHANY_IMAGINATION_OWNER_ROLE.into(), + authority_scope: "epiphany.role.imagination".into(), + instruction: "plan".into(), + output_contract_id: epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID.into(), + document_kind: "role".into(), + launch_document_msgpack: Vec::new(), + metadata: std::collections::BTreeMap::new(), + organ_launch_contract: epiphany_core::default_launch_organ_contract( + "epiphany.role.imagination", + "role", + epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID, + ), + proposal_modeling_request_id: None, + claim_repair_request_id: None, + frontier_planning_request_id: Some("planning-request-1".into()), + }; + let result = role_worker_result_from_ingress( + &launch, + "imagination", + "planning-result-1", + &parsed, + Vec::new(), + ); + assert_eq!( + result.frontier_planning_request_id.as_deref(), + Some("planning-request-1") + ); + assert!(result.state_patch_msgpack.is_none()); + assert!(result.self_patch_msgpack.is_none()); + let candidate = result + .frontier_plan_candidate()? + .expect("typed frontier candidate"); + assert_eq!(candidate.planning_request_id, "planning-request-1"); + assert_eq!( + candidate.candidate_id, + epiphany_core::canonical_repo_frontier_plan_candidate_id(&candidate)? + ); + assert_eq!( + candidate.schema_version, + epiphany_core::REPO_FRONTIER_PLAN_CANDIDATE_SCHEMA_VERSION + ); + Ok(()) + } + fn test_openai_event( request_id: &str, sequence: u64, @@ -1818,6 +1963,8 @@ mod tests { .to_string(), ), proposal_modeling_context: None, + claim_repair_context: None, + frontier_planning_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: Vec::new(), @@ -1840,6 +1987,8 @@ mod tests { epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID, ), proposal_modeling_request_id: None, + claim_repair_request_id: None, + frontier_planning_request_id: None, created_at: now(), }, )?; @@ -1945,6 +2094,8 @@ mod tests { .to_string(), ), proposal_modeling_context: None, + claim_repair_context: None, + frontier_planning_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: Vec::new(), @@ -1967,6 +2118,8 @@ mod tests { epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID, ), proposal_modeling_request_id: None, + claim_repair_request_id: None, + frontier_planning_request_id: None, created_at: now(), }, )?; diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 204457eb..59877ffd 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -965,3 +965,20 @@ and the loser leaves no job, worker launch, or opened event. Full core proof: 322 passed, 1 ignored. Next build the exclusive immutable Imagination result echo carrying the typed candidate, then Mind-only Adopt/Refuse/Hold admission and the narrow Adopt model transition that makes Hands routing true. + +The immutable Imagination result slice is now complete. Runtime role result v2 +adds an exclusive `frontier_planning_request_id` plus dedicated canonical +MessagePack `RepoFrontierPlanCandidate`. Persistence replays the exact planning +request, request-keyed launch binding, stored launch-document hash/projection, +runtime/thread, current model/admission/frontier, deterministic candidate +identity, and bounded safe paths. Planning results reject generic state, Self, +RepoModel, Verification, and Modeling authority cargo before writing. The +OpenAI ingress uses a specialized planning schema, supplies candidate +schema/contract itself, derives candidate identity from semantic cargo, and +does not expose generic patch mouths. The candidate remains embedded in the +immutable result; no standalone candidate document is written. Proof: core +325 passed/1 ignored; OpenAI runtime 9 passed; hostile missing/swapped request, +missing/adjacent/escaped candidate, wrong role, generic patch smuggling, exact +retry, and immutable collision checks pass. Next implement Mind-only terminal +decision plus the dedicated Adopt RepoModel transition from Imagination to +Hands, then delete the old internal candidate/adoption fixture writers. diff --git a/notes/repo-frontier-planning-authority-map.md b/notes/repo-frontier-planning-authority-map.md index ce131b00..117c7b4d 100644 --- a/notes/repo-frontier-planning-authority-map.md +++ b/notes/repo-frontier-planning-authority-map.md @@ -11,10 +11,18 @@ worker launch document. Caller-authored projections, mixed authority carriers, stale requests, causal substitutions, and duplicate/racing launches are refused without loser artifacts. -The downstream nerve is still incomplete. Imagination has no exclusive -immutable result carrying the candidate; Mind has no correlated admission; the -old internal candidate/adoption fixture writers still exist; and Adopt still -changes no RepoModel bytes or Hands truth. +Imagination's output is now a v2 immutable runtime result with an exclusive +planning-request echo and one dedicated canonical MessagePack candidate. Result +persistence replays the current request, launch binding, stored launch bytes, +projection, runtime/thread, model, admission, frontier, candidate identity, and +safe-path scope before its absent-only write. Generic state, Self, RepoModel, +Verification, and Modeling authority cargo are forbidden on this path. The +candidate remains inside the immutable result until Mind admission; no +independent candidate writer participates in production. + +The downstream nerve is still incomplete. Mind has no correlated admission; +the old internal candidate/adoption fixture writers still exist for the legacy +test chain; and Adopt still changes no RepoModel bytes or Hands truth. The public candidate/adoption writers remain removed. The existing internal functions are test scaffolding only and die when result correlation and Mind diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 49f31ea56c41bd56638ee2ed93e41ef5035ec2cd..e8ede15767f8211c0e6c0d8e29116efda78e3996 100644 GIT binary patch delta 311 zcmXYpy-EW?6ouXRvuJt|i*Ug{i7Q(qsl+N(5X8c6cIG6zFgvrEpJ*!a2O_qzr1u3B zQV2ePt?wc}fD^FzIQN`$@BMyi{XDnc529w$>nG{e*GBv}S{+5xD0+RU8;iOT1=hM5 z*a~b1c0TGx{CT&0o5juD^rGACudiRC)#2Xnuq?e}nZ)w^KHk4IuwFWvC8YE<;f70Z ziy7pcz;g{tE(c}WD?tWR59^498D$1xNh=U%N~?l1Np4<2ti31%b09W$Ff{g3l4M@- zyi;lc*%=$HToEdR%Sy8OfF@8d1*eKRo>Qs4%Pm%H@RLI1D SMitcQkcJ~ArTqs!Z*0r} delta 61 zcmey=C3n6