diff --git a/epiphany-core/src/agent_launch.rs b/epiphany-core/src/agent_launch.rs index 6161c572..f2f4db18 100644 --- a/epiphany-core/src/agent_launch.rs +++ b/epiphany-core/src/agent_launch.rs @@ -21,6 +21,8 @@ use epiphany_state_model::EpiphanyThreadState; pub const EPIPHANY_IMAGINATION_ROLE_BINDING_ID: &str = "planning-synthesis-worker"; pub const EPIPHANY_IMAGINATION_OWNER_ROLE: &str = "epiphany-imagination"; +pub const EPIPHANY_MIND_ROLE_BINDING_ID: &str = "mind-admission-reviewer"; +pub const EPIPHANY_MIND_OWNER_ROLE: &str = "epiphany-mind-admission-review"; pub const EPIPHANY_RESEARCH_ROLE_BINDING_ID: &str = "research-source-gather-worker"; pub const EPIPHANY_RESEARCH_OWNER_ROLE: &str = "epiphany-eyes"; pub const EPIPHANY_MODELING_ROLE_BINDING_ID: &str = "modeling-checkpoint-worker"; @@ -467,6 +469,32 @@ pub fn epiphany_frontier_planning_output_schema() -> serde_json::Value { schema } +pub fn epiphany_frontier_plan_mind_output_schema() -> serde_json::Value { + serde_json::json!({ + "type": "object", + "properties": { + "roleId": {"type": "string", "const": "mindAdmissionReview"}, + "verdict": {"type": "string", "minLength": 1}, + "summary": {"type": "string", "minLength": 1}, + "nextSafeMove": {"type": "string", "minLength": 1}, + "filesInspected": {"type": "array", "items": {"type": "string"}}, + "frontierPlanMindRequestId": {"type": "string", "minLength": 1}, + "frontierPlanMindDecision": { + "type": "object", + "required": ["mindRequestId", "planningRequestId", "imaginationResultId", "candidateId", "candidateSha256", "decision", "rationale", "decidedAt"], + "properties": { + "mindRequestId": {"type": "string", "minLength": 1}, "planningRequestId": {"type": "string", "minLength": 1}, + "imaginationResultId": {"type": "string", "minLength": 1}, "candidateId": {"type": "string", "minLength": 1}, + "candidateSha256": {"type": "string", "minLength": 1}, "decision": {"type": "string", "enum": ["adopt", "refuse", "hold"]}, + "rationale": {"type": "string", "minLength": 1}, "decidedAt": {"type": "string", "minLength": 1} + }, "additionalProperties": false + } + }, + "required": ["roleId", "verdict", "summary", "nextSafeMove", "filesInspected", "frontierPlanMindRequestId", "frontierPlanMindDecision"], + "additionalProperties": false + }) +} + pub fn epiphany_reorient_launch_output_schema() -> serde_json::Value { serde_json::json!({ "type": "object", @@ -530,6 +558,7 @@ pub struct EpiphanySharedPromptConfig { #[derive(Debug, serde::Deserialize)] pub struct EpiphanyRolePromptConfig { pub imagination: String, + pub mind: String, pub modeling: String, pub verification: String, #[allow(dead_code)] @@ -613,6 +642,74 @@ pub fn build_epiphany_role_launch_request( ) } +pub fn build_epiphany_frontier_plan_mind_launch_request( + thread_id: &str, + expected_revision: Option, + max_runtime_seconds: Option, + state: &EpiphanyThreadState, + mind_request_id: String, +) -> Result { + let linked_subgoal_ids = epiphany_active_subgoal_ids(Some(state)); + let linked_graph_node_ids = epiphany_active_graph_node_ids(Some(state)); + let authority_scope = "epiphany.procedure.mind_admission_review"; + let launch_document = EpiphanyWorkerLaunchDocument::Role(EpiphanyRoleWorkerLaunchDocument { + thread_id: thread_id.to_string(), + role_id: "mindAdmissionReview".to_string(), + state_revision: state.revision, + objective: state.objective.clone(), + dynamic_prompt_context: None, + proposal_modeling_context: None, + claim_repair_context: None, + frontier_planning_context: None, + frontier_plan_mind_context: None, + active_subgoal_id: state.active_subgoal_id.clone(), + active_subgoals: state + .subgoals + .iter() + .filter(|subgoal| Some(subgoal.id.as_str()) == state.active_subgoal_id.as_deref()) + .cloned() + .collect(), + active_graph_node_ids: linked_graph_node_ids.clone(), + investigation_checkpoint: state.investigation_checkpoint.clone(), + scratch: state.scratch.clone(), + invariants: state.invariants.clone(), + graphs: Some(state.graphs.clone()), + recent_evidence: state.recent_evidence.iter().take(8).cloned().collect(), + recent_observations: state.observations.iter().take(8).cloned().collect(), + graph_frontier: state.graph_frontier.clone(), + graph_checkpoint: state.graph_checkpoint.clone(), + planning: Some(state.planning.clone()), + churn: state.churn.clone(), + }); + let output_contract_id = launch_document.output_contract_id().to_string(); + let organ_launch_contract = default_launch_organ_contract( + authority_scope, + launch_document.document_kind(), + &output_contract_id, + ); + Ok(EpiphanyJobLaunchRequest { + expected_revision, + binding_id: EPIPHANY_MIND_ROLE_BINDING_ID.to_string(), + kind: CoreEpiphanyJobKind::Specialist, + scope: "role-scoped frontier plan judgment".to_string(), + owner_role: EPIPHANY_MIND_OWNER_ROLE.to_string(), + authority_scope: authority_scope.to_string(), + linked_subgoal_ids, + linked_graph_node_ids, + instruction: epiphany_worker_prompt( + epiphany_specialist_prompt_config().roles.mind.as_str(), + ), + launch_document, + output_contract_id, + organ_launch_contract, + max_runtime_seconds, + proposal_modeling_request_id: None, + claim_repair_request_id: None, + frontier_planning_request_id: None, + frontier_plan_mind_request_id: Some(mind_request_id), + }) +} + pub fn build_epiphany_role_launch_request_with_dynamic_context( thread_id: &str, role_id: EpiphanyRoleResultRoleId, @@ -659,6 +756,7 @@ pub fn build_epiphany_role_launch_request_with_dynamic_context( proposal_modeling_context: None, claim_repair_context: None, frontier_planning_context: None, + frontier_plan_mind_context: None, active_subgoal_id: state.active_subgoal_id.clone(), active_subgoals: state .subgoals @@ -702,6 +800,7 @@ pub fn build_epiphany_role_launch_request_with_dynamic_context( proposal_modeling_request_id: None, claim_repair_request_id: None, frontier_planning_request_id: None, + frontier_plan_mind_request_id: None, }) } @@ -807,6 +906,7 @@ pub fn build_epiphany_job_launch_request( proposal_modeling_request_id: None, claim_repair_request_id: None, frontier_planning_request_id: None, + frontier_plan_mind_request_id: None, } } @@ -932,6 +1032,7 @@ mod tests { prompts.shared.persistent_memory.as_str(), ), ("roles.imagination", prompts.roles.imagination.as_str()), + ("roles.mind", prompts.roles.mind.as_str()), ("roles.modeling", prompts.roles.modeling.as_str()), ("roles.verification", prompts.roles.verification.as_str()), ("roles.research", prompts.roles.research.as_str()), @@ -981,6 +1082,42 @@ mod tests { assert!(!prompt.contains("Heartbeat: every lane")); } + #[test] + fn frontier_plan_mind_launch_is_a_real_prompted_role_with_strict_schema() { + let request = build_epiphany_frontier_plan_mind_launch_request( + "thread-mind", + Some(0), + Some(60), + &EpiphanyThreadState::default(), + "mind-request-1".into(), + ) + .expect("Mind launch request"); + assert_eq!(request.owner_role, EPIPHANY_MIND_OWNER_ROLE); + assert_eq!(request.binding_id, EPIPHANY_MIND_ROLE_BINDING_ID); + assert!( + request + .instruction + .contains("admission-review procedure serving Epiphany Mind") + ); + assert_eq!( + request.frontier_plan_mind_request_id.as_deref(), + Some("mind-request-1") + ); + let EpiphanyWorkerLaunchDocument::Role(document) = request.launch_document else { + panic!("Mind must use a role launch document") + }; + assert_eq!(document.role_id, "mindAdmissionReview"); + let schema = epiphany_frontier_plan_mind_output_schema(); + assert_eq!( + schema["properties"]["roleId"]["const"], + "mindAdmissionReview" + ); + assert_eq!( + schema["properties"]["frontierPlanMindDecision"]["properties"]["decision"]["enum"], + serde_json::json!(["adopt", "refuse", "hold"]) + ); + } + #[test] fn modeling_schema_exposes_only_typed_authority_purposes() { let schema = epiphany_role_launch_output_schema(EpiphanyRoleResultRoleId::Modeling); diff --git a/epiphany-core/src/bin/epiphany-hands-action-smoke.rs b/epiphany-core/src/bin/epiphany-hands-action-smoke.rs index cead182d..869be4ef 100644 --- a/epiphany-core/src/bin/epiphany-hands-action-smoke.rs +++ b/epiphany-core/src/bin/epiphany-hands-action-smoke.rs @@ -47,6 +47,9 @@ fn main() -> Result<()> { requested_at: "2026-06-02T00:00:10Z".to_string(), contract: "Smoke intent proves Hands action starts as typed runtime-spine state." .to_string(), + frontier_route_id: String::new(), + plan_candidate_sha256: String::new(), + plan_action: String::new(), }; put_hands_action_intent(&store, &intent)?; diff --git a/epiphany-core/src/bin/epiphany-hands-action.rs b/epiphany-core/src/bin/epiphany-hands-action.rs index f30bb442..cef5d798 100644 --- a/epiphany-core/src/bin/epiphany-hands-action.rs +++ b/epiphany-core/src/bin/epiphany-hands-action.rs @@ -801,6 +801,9 @@ mod tests { substrate_gate_grant_receipt_id: "substrate-grant-test".to_string(), requested_at: "2026-06-02T00:00:01Z".to_string(), contract: "Test Hands intent.".to_string(), + frontier_route_id: String::new(), + plan_candidate_sha256: String::new(), + plan_action: String::new(), }; put_hands_action_intent(store, &intent)?; let review = hands_action_review_for_intent( diff --git a/epiphany-core/src/bin/epiphany-mvp-coordinator.rs b/epiphany-core/src/bin/epiphany-mvp-coordinator.rs index 8d0b4c96..b985217b 100644 --- a/epiphany-core/src/bin/epiphany-mvp-coordinator.rs +++ b/epiphany-core/src/bin/epiphany-mvp-coordinator.rs @@ -853,6 +853,9 @@ fn record_hands_implementation_gate( requested_at: requested_at.clone(), contract: "Coordinator continuation becomes a typed Hands action intent before any file edit, command, or commit may count as implementation evidence." .to_string(), + frontier_route_id: String::new(), + plan_candidate_sha256: String::new(), + plan_action: String::new(), }; put_hands_action_intent(runtime_store, &intent)?; diff --git a/epiphany-core/src/coordinator_acceptance.rs b/epiphany-core/src/coordinator_acceptance.rs index e2802482..71f2cada 100644 --- a/epiphany-core/src/coordinator_acceptance.rs +++ b/epiphany-core/src/coordinator_acceptance.rs @@ -1175,6 +1175,7 @@ mod tests { frontier_modeling_request_id: String::new(), proposal_modeling_request_id: String::new(), claim_repair_request_id: String::new(), + frontier_plan_decision_id: String::new(), }, )?; let mut fresh = existing.clone(); diff --git a/epiphany-core/src/coordinator_launch.rs b/epiphany-core/src/coordinator_launch.rs index 46d93164..258cff97 100644 --- a/epiphany-core/src/coordinator_launch.rs +++ b/epiphany-core/src/coordinator_launch.rs @@ -138,6 +138,37 @@ fn commit_coordinator_job_launch_in_cache( } else { None }; + let frontier_plan_mind_launch = + if let Some(request_id) = request.frontier_plan_mind_request_id.as_deref() { + let (mind_request, planning, candidate, identity) = + validate_frontier_plan_mind_launch(cache, current_state, request, request_id)?; + let projection = + RepoFrontierPlanMindContextProjection::new(&mind_request, &planning, &candidate); + match &mut effective_launch_document { + EpiphanyWorkerLaunchDocument::Role(document) => { + if document.proposal_modeling_context.is_some() + || document.claim_repair_context.is_some() + || document.frontier_planning_context.is_some() + || document.frontier_plan_mind_context.is_some() + { + return Err(anyhow!("Mind frontier decision context is exclusive")); + } + document.frontier_plan_mind_context = Some(projection); + } + EpiphanyWorkerLaunchDocument::Reorient(_) => { + return Err(anyhow!( + "reorient launch cannot carry Mind decision context" + )); + } + } + let hash = format!( + "{:x}", + Sha256::digest(rmp_serde::to_vec_named(&effective_launch_document)?) + ); + Some((mind_request, identity, hash)) + } else { + None + }; let prepared = prepare_runtime_spine_heartbeat_job( &cache, RuntimeSpineHeartbeatJobOptions { @@ -163,6 +194,7 @@ fn commit_coordinator_job_launch_in_cache( proposal_modeling_request_id: request.proposal_modeling_request_id.clone(), claim_repair_request_id: request.claim_repair_request_id.clone(), frontier_planning_request_id: request.frontier_planning_request_id.clone(), + frontier_plan_mind_request_id: request.frontier_plan_mind_request_id.clone(), created_at: created_at.clone(), }, )?; @@ -263,6 +295,31 @@ fn commit_coordinator_job_launch_in_cache( .0, ); } + if let Some((mind_request, identity, worker_launch_document_sha256)) = frontier_plan_mind_launch + { + let binding = RepoFrontierPlanMindLaunchBinding { + schema_version: REPO_FRONTIER_PLAN_MIND_LAUNCH_BINDING_SCHEMA_VERSION.into(), + binding_record_id: format!( + "repo-frontier-plan-mind-launch-{}", + mind_request.request_id + ), + mind_request_id: mind_request.request_id, + job_id: plan.backend_job_id.clone(), + binding_id: request.binding_id.clone(), + runtime_id: identity.runtime_id, + thread_id: mind_request.thread_id, + launched_at: created_at.clone(), + worker_launch_document_sha256, + contract: REPO_FRONTIER_PLAN_MIND_LAUNCH_BINDING_CONTRACT.into(), + }; + if cache + .get::(&binding.binding_record_id)? + .is_some() + { + return Err(anyhow!("Mind request is already bound to a launch")); + } + batch.push(cache.prepare_entry(&binding.binding_record_id, &binding)?.0); + } if request.binding_id == EPIPHANY_RESEARCH_ROLE_BINDING_ID { let grant = substrate_gate_repo_access_grant_for_launch( format!("substrate-grant-{}", plan.backend_job_id), @@ -295,15 +352,20 @@ pub fn plan_coordinator_job_launch( launcher_job_id: String, backend_job_id: String, ) -> Result { - let (caller_proposal_projection, caller_repair_projection, caller_planning_projection) = - match &request.launch_document { - EpiphanyWorkerLaunchDocument::Role(document) => ( - document.proposal_modeling_context.as_ref(), - document.claim_repair_context.as_ref(), - document.frontier_planning_context.as_ref(), - ), - EpiphanyWorkerLaunchDocument::Reorient(_) => (None, None, None), - }; + let ( + caller_proposal_projection, + caller_repair_projection, + caller_planning_projection, + caller_mind_projection, + ) = match &request.launch_document { + EpiphanyWorkerLaunchDocument::Role(document) => ( + document.proposal_modeling_context.as_ref(), + document.claim_repair_context.as_ref(), + document.frontier_planning_context.as_ref(), + document.frontier_plan_mind_context.as_ref(), + ), + EpiphanyWorkerLaunchDocument::Reorient(_) => (None, None, None, None), + }; if caller_proposal_projection.is_some() { return Err(anyhow!( "caller-prepopulated proposal Modeling context is forbidden; coordinator commit owns projection" @@ -319,10 +381,16 @@ pub fn plan_coordinator_job_launch( "caller-prepopulated frontier planning context is forbidden; coordinator commit owns projection" )); } + if caller_mind_projection.is_some() { + return Err(anyhow!( + "caller-prepopulated Mind decision context is forbidden; coordinator commit owns projection" + )); + } if [ request.proposal_modeling_request_id.is_some(), request.claim_repair_request_id.is_some(), request.frontier_planning_request_id.is_some(), + request.frontier_plan_mind_request_id.is_some(), ] .into_iter() .filter(|present| *present) @@ -330,7 +398,7 @@ pub fn plan_coordinator_job_launch( > 1 { return Err(anyhow!( - "proposal Modeling, claim repair, and frontier planning launches are mutually exclusive" + "specialized authority launches are mutually exclusive" )); } if let Some(expected) = request.expected_revision @@ -353,6 +421,10 @@ pub fn plan_coordinator_job_launch( let mut cache = runtime_spine_cache(runtime_store)?; cache.pull_all_backing_stores()?; validate_frontier_planning_launch(&cache, state, request, request_id)?; + } else if let Some(request_id) = request.frontier_plan_mind_request_id.as_deref() { + let mut cache = runtime_spine_cache(runtime_store)?; + cache.pull_all_backing_stores()?; + validate_frontier_plan_mind_launch(&cache, state, request, request_id)?; } else if request.owner_role == EPIPHANY_MODELING_OWNER_ROLE { // Ordinary Modeling launches remain valid, but carry no proposal authority. } @@ -410,6 +482,52 @@ pub fn plan_coordinator_job_launch( }) } +fn validate_frontier_plan_mind_launch( + cache: &CultCache, + state: &EpiphanyThreadState, + launch: &EpiphanyJobLaunchRequest, + request_id: &str, +) -> Result<( + RepoFrontierPlanMindRequest, + RepoFrontierPlanningRequest, + RepoFrontierPlanCandidate, + EpiphanyRuntimeIdentity, +)> { + if launch.owner_role != EPIPHANY_MIND_OWNER_ROLE + || launch.binding_id != EPIPHANY_MIND_ROLE_BINDING_ID + { + return Err(anyhow!( + "frontier plan decision may only be carried by the Mind role launch" + )); + } + let request = cache + .get::(request_id)? + .ok_or_else(|| anyhow!("Mind request does not exist"))?; + let (planning, candidate) = + crate::runtime_spine::validate_repo_frontier_plan_mind_request(cache, &request)?; + let identity = cache + .get::(RUNTIME_IDENTITY_KEY)? + .ok_or_else(|| anyhow!("Mind launch requires runtime identity"))?; + let persisted = cache + .get::(crate::THREAD_STATE_KEY)? + .ok_or_else(|| anyhow!("Mind launch requires thread state"))?; + if persisted.state()? != *state + || request.runtime_id != identity.runtime_id + || request.thread_id != persisted.thread_id + || launch.launch_document.thread_id() != request.thread_id + { + return Err(anyhow!("Mind launch provenance mismatch")); + } + if cache + .get_all::()? + .iter() + .any(|b| b.mind_request_id == request_id) + { + return Err(anyhow!("Mind request already bound")); + } + Ok((request, planning, candidate, identity)) +} + fn validate_frontier_planning_launch( cache: &CultCache, state: &EpiphanyThreadState, @@ -891,9 +1009,97 @@ pub(crate) mod tests { 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)?), + frontier_plan_mind_request_id: None, + frontier_plan_mind_decision_msgpack: None, }) } + fn launch_frontier_mind_result( + store: &Path, + imagination_result: &EpiphanyRuntimeRoleWorkerResult, + decision: RepoFrontierPlanDecision, + suffix: &str, + ) -> Result { + let mind_request = crate::commit_repo_frontier_plan_mind_request( + store, + &imagination_result.result_id, + "2026-07-15T09:00:05Z", + )?; + let mut cache = coordinator_acceptance_cache(store)?; + cache.pull_all_backing_stores()?; + let state = cache + .get_required::(THREAD_STATE_KEY)? + .state()?; + let launch = crate::build_epiphany_frontier_plan_mind_launch_request( + &mind_request.thread_id, + Some(state.revision), + Some(60), + &state, + mind_request.request_id.clone(), + ) + .map_err(|error| anyhow!(error))?; + let job_id = format!("backend-mind-decision-{suffix}"); + let plan = plan_coordinator_job_launch( + &state, + &launch, + store, + format!("launcher-mind-{suffix}"), + job_id.clone(), + )?; + commit_coordinator_job_launch( + store, + &mind_request.thread_id, + &state, + &launch, + &plan, + "2026-07-15T09:00:06Z".into(), + )?; + let payload = RepoFrontierPlanMindDecision { + mind_request_id: mind_request.request_id.clone(), + planning_request_id: mind_request.planning_request_id.clone(), + imagination_result_id: mind_request.imagination_result_id.clone(), + candidate_id: mind_request.candidate_id.clone(), + candidate_sha256: mind_request.candidate_sha256.clone(), + decision, + rationale: format!("Mind judged the exact typed candidate as {decision:?}."), + decided_at: "2026-07-15T09:00:07Z".into(), + }; + let result = EpiphanyRuntimeRoleWorkerResult { + schema_version: RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION.into(), + result_id: format!("frontier-mind-result-{suffix}"), + job_id, + role_id: "mindAdmissionReview".into(), + verdict: format!("{decision:?}").to_lowercase(), + summary: "Judged one exact frontier plan candidate.".into(), + next_safe_move: "Coordinator may admit this immutable judgment.".into(), + checkpoint_summary: None, + scratch_summary: None, + files_inspected: Vec::new(), + frontier_node_ids: Vec::new(), + evidence_ids: vec![mind_request.request_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: None, + frontier_plan_candidate_msgpack: None, + frontier_plan_mind_request_id: Some(mind_request.request_id), + frontier_plan_mind_decision_msgpack: Some(rmp_serde::to_vec_named(&payload)?), + }; + put_runtime_role_worker_result(store, &result)?; + Ok(result) + } + fn research_launch(state: &EpiphanyThreadState) -> EpiphanyJobLaunchRequest { build_epiphany_role_launch_request( "thread-1", @@ -1428,7 +1634,7 @@ pub(crate) mod tests { #[test] fn frontier_planning_result_refuses_hostile_substitution_before_persistence() -> Result<()> { let root = tempfile::tempdir()?; - for mutation in 0..9 { + for mutation in 0..10 { let (store, state, launch, planning) = frontier_planning_launch_fixture(root.path(), &format!("result-{mutation}"))?; let job_id = format!("backend-result-{mutation}"); @@ -1456,7 +1662,7 @@ pub(crate) mod tests { 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]), - _ => { + 7 | 8 => { let mut candidate = result.frontier_plan_candidate()?.unwrap(); if mutation == 7 { candidate.frontier_item_hash = "adjacent-frontier".into(); @@ -1466,6 +1672,7 @@ pub(crate) mod tests { result.frontier_plan_candidate_msgpack = Some(rmp_serde::to_vec_named(&candidate)?); } + _ => result.item_error = Some("planner failed after emitting cargo".into()), } let before = std::fs::read(&store)?; assert!( @@ -1478,6 +1685,573 @@ pub(crate) mod tests { Ok(()) } + #[test] + fn mind_adopt_installs_plan_in_model_and_hands_route_copies_it() -> Result<()> { + let root = tempfile::tempdir()?; + let (store, state, launch, planning) = + frontier_planning_launch_fixture(root.path(), "mind-adopt")?; + let plan = plan_coordinator_job_launch( + &state, + &launch, + &store, + "launcher-mind-adopt".into(), + "backend-mind-adopt".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-mind-adopt", "2026-07-15T09:00:04Z")?; + put_runtime_role_worker_result(&store, &result)?; + let mind_result = + launch_frontier_mind_result(&store, &result, RepoFrontierPlanDecision::Adopt, "adopt")?; + + let before = runtime_current_repo_model(&store)?.expect("pre-Adopt model"); + let mut illicit_item = before + .frontier + .iter() + .find(|item| item.id == planning.frontier_item_id) + .unwrap() + .clone(); + illicit_item.recommended_next_organ = "Hands".into(); + let illicit = RepoModelPatch { + patch_id: "generic-adopt-bypass".into(), + base_revision: before.model_revision, + base_hash: memory_graph_model_hash(&before)?, + applied_at: "2026-07-15T09:00:05Z".into(), + purpose: RepoModelPatchPurpose::Evolution, + operations: vec![RepoModelPatchOperation::ReviseFrontier { item: illicit_item }], + }; + assert!(derive_repo_model_patch(&before, &illicit).is_err()); + + let decision = commit_repo_frontier_plan_decision(&store, &mind_result.result_id)?; + let retry = commit_repo_frontier_plan_decision(&store, &mind_result.result_id)?; + assert_eq!(retry, decision); + assert!(!decision.model_admission_receipt_id.is_empty()); + let current = runtime_current_repo_model(&store)?.expect("admitted Adopt model"); + assert_eq!(current.model_revision, before.model_revision + 1); + let item = current + .frontier + .iter() + .find(|item| item.id == planning.frontier_item_id) + .expect("adopted frontier"); + assert_eq!(item.recommended_next_organ, "Hands"); + let adopted = item.adopted_plan.as_ref().expect("model-owned plan"); + assert_eq!(adopted.planning_request_id, planning.request_id); + assert_eq!(adopted.result_id, result.result_id); + assert_eq!(adopted.job_id, result.job_id); + assert_eq!(adopted.candidate_id, decision.candidate_id); + assert_eq!(adopted.candidate_sha256, decision.candidate_sha256); + let mut illicit_adopted_item = item.clone(); + illicit_adopted_item.source_scope = vec!["different/execution-scope".into()]; + let illicit_adopted_revision = RepoModelPatch { + patch_id: "generic-adopted-anatomy-mutation".into(), + base_revision: current.model_revision, + base_hash: memory_graph_model_hash(¤t)?, + applied_at: "2026-07-15T09:00:06Z".into(), + purpose: RepoModelPatchPurpose::Evolution, + operations: vec![RepoModelPatchOperation::ReviseFrontier { + item: illicit_adopted_item, + }], + }; + assert!(derive_repo_model_patch(¤t, &illicit_adopted_revision).is_err()); + let route = select_and_commit_repo_frontier_route(&store, "2026-07-15T09:00:06Z")?; + assert_eq!(route.adopted_plan.as_ref(), Some(adopted)); + assert_eq!(route.source_scope, adopted.safe_paths); + let grant = crate::substrate_gate_coordinator_implementation_grant( + "adopt-hands-grant".into(), + "adopt-hands-job".into(), + adopted.safe_paths.clone(), + "2026-07-15T09:00:08Z".into(), + ); + crate::put_substrate_gate_repo_access_grant_receipt(&store, &grant)?; + let intent = crate::HandsActionIntent { + schema_version: crate::HANDS_ACTION_INTENT_SCHEMA_VERSION.into(), + intent_id: "adopt-hands-intent".into(), + runtime_job_id: grant.runtime_job_id.clone(), + binding_id: grant.binding_id.clone(), + role: grant.role.clone(), + authority_scope: grant.authority_scope.clone(), + requested_action: "continueImplementation".into(), + requested_paths: adopted.safe_paths.clone(), + substrate_gate_grant_receipt_id: grant.receipt_id.clone(), + requested_at: "2026-07-15T09:00:09Z".into(), + contract: "Hands intent executes only the admitted plan.".into(), + frontier_route_id: route.route_id.clone(), + plan_candidate_sha256: adopted.candidate_sha256.clone(), + plan_action: adopted.action.clone(), + }; + crate::put_hands_action_intent(&store, &intent)?; + let review = crate::hands_action_review_for_intent( + "adopt-hands-review".into(), + &intent, + "approved".into(), + vec!["patch".into(), "command".into(), "commit".into()], + vec!["Exact admitted plan binding is present.".into()], + "2026-07-15T09:00:10Z".into(), + ); + crate::put_hands_action_review(&store, &review)?; + let authority = crate::RepoFrontierHandsAuthority { + schema_version: crate::REPO_FRONTIER_HANDS_AUTHORITY_SCHEMA_VERSION.into(), + authority_id: "adopt-hands-authority".into(), + route_id: route.route_id.clone(), + model_revision: route.model_revision, + model_hash: route.model_hash.clone(), + frontier_item_id: route.frontier_item_id.clone(), + frontier_item_hash: route.frontier_item_hash.clone(), + hands_intent_id: intent.intent_id.clone(), + hands_review_id: review.review_id.clone(), + substrate_grant_receipt_id: grant.receipt_id.clone(), + requested_paths: adopted.safe_paths.clone(), + granted_at: "2026-07-15T09:00:11Z".into(), + contract: crate::REPO_FRONTIER_HANDS_AUTHORITY_CONTRACT.into(), + }; + crate::put_repo_frontier_hands_authority(&store, &authority)?; + let patch_receipt = crate::hands_patch_receipt_for_review( + "adopt-hands-patch".into(), + &intent, + &review, + adopted.safe_paths.clone(), + "bounded patch".into(), + "2026-07-15T09:00:12Z".into(), + ); + crate::put_hands_patch_receipt(&store, &patch_receipt)?; + let wrong_command = crate::hands_command_receipt_for_review( + "adopt-hands-command-wrong".into(), + &intent, + &review, + "cargo test unrelated".into(), + "0".into(), + "stdout".into(), + "stderr".into(), + "wrong command".into(), + "2026-07-15T09:00:13Z".into(), + ); + let commit_receipt = crate::hands_commit_receipt_for_review( + "adopt-hands-commit".into(), + &intent, + &review, + "abc123".into(), + "main".into(), + adopted.safe_paths.clone(), + adopted.commit_message.clone(), + "2026-07-15T09:00:14Z".into(), + ); + crate::put_hands_commit_receipt(&store, &commit_receipt)?; + let hostile_request = crate::RepoFrontierVerificationRequest { + schema_version: crate::REPO_FRONTIER_VERIFICATION_REQUEST_SCHEMA_VERSION.into(), + request_id: "adopt-verification-hostile".into(), + route_id: route.route_id.clone(), + model_revision: route.model_revision, + model_hash: route.model_hash.clone(), + frontier_item_id: route.frontier_item_id.clone(), + frontier_item_hash: route.frontier_item_hash.clone(), + hands_intent_id: intent.intent_id.clone(), + hands_review_id: review.review_id.clone(), + hands_patch_receipt_id: patch_receipt.receipt_id.clone(), + hands_command_receipt_id: wrong_command.receipt_id.clone(), + hands_commit_receipt_id: commit_receipt.receipt_id.clone(), + requested_at: "2026-07-15T09:00:15Z".into(), + contract: crate::REPO_FRONTIER_VERIFICATION_REQUEST_CONTRACT.into(), + }; + assert!(crate::put_repo_frontier_verification_request(&store, &hostile_request).is_err()); + + // Prove the whole admitted-plan nerve, not merely its neighboring joints. The + // rejected command above remains immutable evidence, while the exact planned + // command becomes the only chain Soul is allowed to verify. + let exact_command = crate::hands_command_receipt_for_review( + "adopt-hands-command-exact".into(), + &intent, + &review, + adopted.command.clone(), + "0".into(), + "stdout-exact".into(), + "stderr-exact".into(), + "exact admitted command passed".into(), + "2026-07-15T09:00:13Z".into(), + ); + crate::put_hands_command_receipt(&store, &exact_command)?; + let chain = + crate::runtime_latest_hands_receipt_chain_after(&store, "2026-07-15T09:00:11Z")? + .expect("exact admitted Hands chain"); + assert_eq!(chain.patch_receipt_id, patch_receipt.receipt_id); + assert_eq!(chain.command_receipt_id, exact_command.receipt_id); + assert_eq!(chain.commit_receipt_id, commit_receipt.receipt_id); + let verification_request = crate::commit_repo_frontier_verification_request_for_chain( + &store, + &chain, + "2026-07-15T09:00:17Z", + )?; + assert_eq!(verification_request.route_id, route.route_id); + + let verification_result = crate::EpiphanyRuntimeRoleWorkerResult { + schema_version: crate::RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION.into(), + result_id: "adopt-verification-result".into(), + job_id: "adopt-verification-job".into(), + role_id: "verification".into(), + verdict: "pass".into(), + summary: "Exact adopted plan passed Soul verification.".into(), + next_safe_move: "Modeling incorporates the verdict.".into(), + checkpoint_summary: None, + scratch_summary: None, + files_inspected: adopted.safe_paths.clone(), + frontier_node_ids: vec![route.frontier_item_id.clone()], + evidence_ids: vec![verification_request.request_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: Default::default(), + repo_model_patch_msgpack: None, + verification_request_id: Some(verification_request.request_id.clone()), + frontier_route_id: Some(route.route_id.clone()), + 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, + frontier_plan_mind_request_id: None, + frontier_plan_mind_decision_msgpack: None, + }; + crate::put_runtime_role_worker_result(&store, &verification_result)?; + let soul_verdict = crate::SoulVerdictReceipt { + schema_version: crate::SOUL_VERDICT_RECEIPT_SCHEMA_VERSION.into(), + receipt_id: "adopt-soul-verdict".into(), + source_result_id: verification_result.result_id.clone(), + source_job_id: verification_result.job_id.clone(), + verdict: "pass".into(), + summary: verification_result.summary.clone(), + evidence_ids: verification_result.evidence_ids.clone(), + risks: Vec::new(), + emitted_at: "2026-07-15T09:00:18Z".into(), + contract: "Soul judges the exact routed consequence chain.".into(), + verification_request_id: verification_request.request_id.clone(), + frontier_route_id: route.route_id.clone(), + }; + crate::put_soul_verdict_receipt(&store, &soul_verdict)?; + let acceptance = epiphany_state_model::EpiphanyAcceptanceReceipt { + id: "adopt-verification-acceptance".into(), + result_id: verification_result.result_id.clone(), + job_id: verification_result.job_id.clone(), + binding_id: "verification-worker".into(), + surface: "roleAccept".into(), + role_id: "verification".into(), + status: "accepted".into(), + accepted_at: "2026-07-15T09:00:18Z".into(), + ..Default::default() + }; + let accepted_state = epiphany_state_model::EpiphanyThreadState { + revision: 1, + acceptance_receipts: vec![acceptance.clone()], + ..Default::default() + }; + let mut state_cache = crate::runtime_spine_cache(&store)?; + state_cache.put( + crate::THREAD_STATE_KEY, + &crate::EpiphanyThreadStateEntry::from_state("mind-adopt-thread", &accepted_state)?, + )?; + let modeling_request = crate::commit_repo_frontier_modeling_request(&store, &acceptance)?; + + let pre_incorporation = + crate::runtime_current_repo_model(&store)?.expect("model before verdict incorporation"); + let mut resolved_item = pre_incorporation + .frontier + .iter() + .find(|candidate| candidate.id == route.frontier_item_id) + .expect("routed adopted item before incorporation") + .clone(); + let adopted_before = rmp_serde::to_vec_named( + resolved_item + .adopted_plan + .as_ref() + .expect("adopted plan before closure"), + )?; + resolved_item.status = crate::RepoFrontierStatus::Resolved; + resolved_item.updated_at = Some("2026-07-15T09:00:19Z".into()); + resolved_item + .evidence_refs + .push(verification_request.request_id.clone()); + resolved_item + .evidence_refs + .push(soul_verdict.receipt_id.clone()); + resolved_item.evidence_refs.sort(); + resolved_item.evidence_refs.dedup(); + let incorporation_patch = crate::RepoModelPatch { + patch_id: "adopt-verdict-incorporation-patch".into(), + base_revision: pre_incorporation.model_revision, + base_hash: crate::memory_graph_model_hash(&pre_incorporation)?, + applied_at: "2026-07-15T09:00:19Z".into(), + purpose: crate::RepoModelPatchPurpose::IncorporateFrontierVerdict { + route_id: route.route_id.clone(), + soul_verdict_receipt_id: soul_verdict.receipt_id.clone(), + }, + operations: vec![crate::RepoModelPatchOperation::ReviseFrontier { + item: resolved_item, + }], + }; + let incorporation_bytes = rmp_serde::to_vec_named(&incorporation_patch)?; + let modeling_result = crate::EpiphanyRuntimeRoleWorkerResult { + schema_version: crate::RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION.into(), + result_id: "adopt-incorporation-result".into(), + job_id: "adopt-incorporation-job".into(), + role_id: "modeling".into(), + verdict: "checkpoint-ready".into(), + summary: "Close the verified adopted frontier.".into(), + next_safe_move: "Mind admits the exact Modeling transition.".into(), + checkpoint_summary: None, + scratch_summary: None, + files_inspected: Vec::new(), + frontier_node_ids: vec![route.frontier_item_id.clone()], + evidence_ids: vec![soul_verdict.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: Default::default(), + repo_model_patch_msgpack: Some(incorporation_bytes.clone()), + verification_request_id: None, + frontier_route_id: None, + repo_frontier_modeling_request_id: Some(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, + frontier_plan_mind_request_id: None, + frontier_plan_mind_decision_msgpack: None, + }; + crate::put_runtime_role_worker_result(&store, &modeling_result)?; + let incorporation_review = crate::RepoModelAdmissionReview { + schema_version: crate::REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION.into(), + review_id: "adopt-incorporation-review".into(), + result_id: modeling_result.result_id.clone(), + job_id: modeling_result.job_id.clone(), + patch_id: incorporation_patch.patch_id.clone(), + patch_sha256: format!("{:x}", Sha256::digest(&incorporation_bytes)), + base_revision: incorporation_patch.base_revision, + base_hash: incorporation_patch.base_hash.clone(), + decision: crate::MindGatewayDecision::Accept, + evidence_ids: modeling_result.evidence_ids.clone(), + reviewed_at: "2026-07-15T09:00:20Z".into(), + contract: crate::REPO_MODEL_ADMISSION_CONTRACT.into(), + }; + let incorporation = crate::commit_repo_model_admission( + &store, + &modeling_result.result_id, + &incorporation_review, + )?; + assert_eq!(incorporation.frontier_route_id, route.route_id); + assert_eq!( + incorporation.frontier_modeling_request_id, + modeling_request.request_id + ); + let closed = crate::runtime_current_repo_model(&store)?.expect("closed model"); + let closed_item = closed + .frontier + .iter() + .find(|candidate| candidate.id == route.frontier_item_id) + .expect("closed adopted frontier"); + assert_eq!(closed_item.status, crate::RepoFrontierStatus::Resolved); + assert_eq!( + rmp_serde::to_vec_named( + closed_item + .adopted_plan + .as_ref() + .expect("plan survives closure") + )?, + adopted_before, + "verdict incorporation must preserve the adopted plan byte-for-byte" + ); + let cache = coordinator_acceptance_cache(&store)?; + let admission = cache + .get::(&decision.model_admission_receipt_id)? + .expect("Adopt admission receipt"); + assert_eq!(admission.frontier_plan_decision_id, decision.decision_id); + assert_eq!(admission.admitted_hash, memory_graph_model_hash(¤t)?); + assert_eq!( + commit_repo_frontier_plan_decision(&store, &mind_result.result_id)?, + decision + ); + Ok(()) + } + + #[test] + fn mind_refuse_and_hold_are_terminal_inert_receipts() -> Result<()> { + let root = tempfile::tempdir()?; + for (suffix, terminal) in [ + ("refuse", RepoFrontierPlanDecision::Refuse), + ("hold", RepoFrontierPlanDecision::Hold), + ] { + let (store, state, launch, planning) = + frontier_planning_launch_fixture(root.path(), suffix)?; + let job_id = format!("backend-{suffix}"); + let plan = plan_coordinator_job_launch( + &state, + &launch, + &store, + format!("launcher-{suffix}"), + job_id.clone(), + )?; + commit_coordinator_job_launch( + &store, + &planning.thread_id, + &state, + &launch, + &plan, + "2026-07-15T09:00:03Z".into(), + )?; + let result = frontier_planning_result(&planning, &job_id, "2026-07-15T09:00:04Z")?; + put_runtime_role_worker_result(&store, &result)?; + let mind_result = launch_frontier_mind_result(&store, &result, terminal, suffix)?; + let before = std::fs::read(&store)?; + let before_model = runtime_current_repo_model(&store)?.unwrap(); + let receipt = commit_repo_frontier_plan_decision(&store, &mind_result.result_id)?; + assert!(receipt.model_admission_receipt_id.is_empty()); + assert_eq!(runtime_current_repo_model(&store)?.unwrap(), before_model); + assert!(select_and_commit_repo_frontier_route(&store, "2026-07-15T09:00:06Z").is_err()); + assert!(std::fs::read(&store)?.len() > before.len()); + assert!( + select_and_commit_repo_frontier_planning_request(&store, "2026-07-15T09:00:07Z") + .is_err() + ); + } + Ok(()) + } + + #[test] + fn concurrent_mind_admission_retries_converge_on_one_terminal_receipt() -> Result<()> { + let root = tempfile::tempdir()?; + let (store, state, launch, planning) = + frontier_planning_launch_fixture(root.path(), "mind-race")?; + let plan = plan_coordinator_job_launch( + &state, + &launch, + &store, + "launcher-mind-race".into(), + "backend-mind-race".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-mind-race", "2026-07-15T09:00:04Z")?; + put_runtime_role_worker_result(&store, &result)?; + let mind_result = + launch_frontier_mind_result(&store, &result, RepoFrontierPlanDecision::Adopt, "race")?; + let left_store = store.clone(); + let right_store = store.clone(); + let left_id = mind_result.result_id.clone(); + let right_id = mind_result.result_id.clone(); + let barrier = std::sync::Arc::new(std::sync::Barrier::new(2)); + let left_barrier = barrier.clone(); + let right_barrier = barrier.clone(); + let left = std::thread::spawn(move || { + crate::runtime_spine::commit_repo_frontier_plan_decision_with_pre_cas( + left_store, + &left_id, + &move || { + left_barrier.wait(); + }, + ) + }); + let right = std::thread::spawn(move || { + crate::runtime_spine::commit_repo_frontier_plan_decision_with_pre_cas( + right_store, + &right_id, + &move || { + right_barrier.wait(); + }, + ) + }); + let outcomes = [left.join().unwrap(), right.join().unwrap()]; + assert_eq!(outcomes.iter().filter(|outcome| outcome.is_ok()).count(), 2); + let mut cache = coordinator_acceptance_cache(&store)?; + cache.pull_all_backing_stores()?; + let decisions = cache + .get_all::()? + .into_iter() + .filter(|receipt| receipt.planning_request_id == planning.request_id) + .collect::>(); + assert_eq!(decisions.len(), 1); + let current = runtime_current_repo_model(&store)?.unwrap(); + if decisions[0].decision == RepoFrontierPlanDecision::Adopt { + assert_eq!(current.model_revision, planning.model_revision + 1); + assert!(!decisions[0].model_admission_receipt_id.is_empty()); + } else { + assert_eq!(current.model_revision, planning.model_revision); + assert!(decisions[0].model_admission_receipt_id.is_empty()); + } + Ok(()) + } + + #[test] + fn immutable_mind_result_rejects_causal_substitution_and_foreign_cargo() -> Result<()> { + let root = tempfile::tempdir()?; + for mutation in 0..7 { + let suffix = format!("mind-hostile-{mutation}"); + let (store, state, launch, planning) = + frontier_planning_launch_fixture(root.path(), &suffix)?; + let imagination_job = format!("backend-imagination-{suffix}"); + let plan = plan_coordinator_job_launch( + &state, + &launch, + &store, + format!("launcher-imagination-{suffix}"), + imagination_job.clone(), + )?; + commit_coordinator_job_launch( + &store, + &planning.thread_id, + &state, + &launch, + &plan, + "2026-07-15T09:00:03Z".into(), + )?; + let imagination = + frontier_planning_result(&planning, &imagination_job, "2026-07-15T09:00:04Z")?; + put_runtime_role_worker_result(&store, &imagination)?; + let mut hostile = launch_frontier_mind_result( + &store, + &imagination, + RepoFrontierPlanDecision::Adopt, + &suffix, + )?; + match mutation { + 0 => hostile.frontier_plan_mind_request_id = Some("swapped-request".into()), + 1 => hostile.job_id = "swapped-job".into(), + 2 => hostile.role_id = "imagination".into(), + 3 => hostile.state_patch_msgpack = Some(vec![0]), + 4 => hostile.repo_model_patch_msgpack = Some(vec![0]), + 5 => { + let mut payload = hostile.frontier_plan_mind_decision()?.unwrap(); + payload.candidate_sha256 = "swapped-candidate-hash".into(); + hostile.frontier_plan_mind_decision_msgpack = + Some(rmp_serde::to_vec_named(&payload)?); + } + _ => hostile.item_error = Some("Mind failed after emitting judgment".into()), + } + let before = std::fs::read(&store)?; + assert!(put_runtime_role_worker_result(&store, &hostile).is_err()); + assert_eq!(std::fs::read(&store)?, before); + } + 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 2f812d4e..f1860875 100644 --- a/epiphany-core/src/coordinator_launch_context.rs +++ b/epiphany-core/src/coordinator_launch_context.rs @@ -172,6 +172,18 @@ pub fn append_verification_hands_receipt_context( route.question, route.gap, )); + if let Some(plan) = route.adopted_plan.as_ref() { + context.push_str(&format!( + "adoptedCandidateSha256: {}\nadoptedAction: {}\nadoptedCommand: {}\nadoptedChecks: {}\nadoptedStopConditions: {}\nadoptedRollbackSteps: {}\nadoptedCommitMessage: {}\n", + plan.candidate_sha256, + plan.action, + plan.command, + plan.checks.join(" | "), + plan.stop_conditions.join(" | "), + plan.rollback_steps.join(" | "), + plan.commit_message, + )); + } context.push_str( "Soul is reviewing typed internal CultMesh telemetry for concrete Hands consequence evidence produced after the latest accepted Verification finding.\n", ); @@ -940,6 +952,8 @@ mod tests { claim_repair_request_id: None, frontier_planning_request_id: None, frontier_plan_candidate_msgpack: None, + frontier_plan_mind_request_id: None, + frontier_plan_mind_decision_msgpack: None, }; crate::put_runtime_role_worker_result(store, &result)?; crate::commit_repo_model_admission( @@ -1136,6 +1150,7 @@ mod tests { proposal_modeling_request_id: None, claim_repair_request_id: None, frontier_planning_request_id: None, + frontier_plan_mind_request_id: None, created_at: "2026-06-02T00:00:00Z".to_string(), }, )?; @@ -1250,6 +1265,9 @@ mod tests { substrate_gate_grant_receipt_id: "substrate-grant-context".to_string(), requested_at: "2026-06-12T00:00:01Z".to_string(), contract: "Test Hands intent.".to_string(), + frontier_route_id: String::new(), + plan_candidate_sha256: String::new(), + plan_action: String::new(), }; crate::put_substrate_gate_repo_access_grant_receipt( &runtime_store, @@ -1423,6 +1441,8 @@ mod tests { claim_repair_request_id: None, frontier_planning_request_id: None, frontier_plan_candidate_msgpack: None, + frontier_plan_mind_request_id: None, + frontier_plan_mind_decision_msgpack: None, }; crate::put_runtime_role_worker_result(&runtime_store, &verification_result)?; crate::put_soul_verdict_receipt( diff --git a/epiphany-core/src/hands_gateway.rs b/epiphany-core/src/hands_gateway.rs index bd2df4b3..2dad5c48 100644 --- a/epiphany-core/src/hands_gateway.rs +++ b/epiphany-core/src/hands_gateway.rs @@ -10,7 +10,7 @@ pub const HANDS_COMMIT_RECEIPT_TYPE: &str = "epiphany.hands.commit_receipt"; pub const HANDS_PR_RECEIPT_TYPE: &str = "epiphany.hands.pr_receipt"; pub const HANDS_ROLLBACK_RECEIPT_TYPE: &str = "epiphany.hands.rollback_receipt"; pub const HANDS_ACTION_REFUSAL_RECEIPT_TYPE: &str = "epiphany.hands.action_refusal_receipt"; -pub const HANDS_ACTION_INTENT_SCHEMA_VERSION: &str = "epiphany.hands.action_intent.v0"; +pub const HANDS_ACTION_INTENT_SCHEMA_VERSION: &str = "epiphany.hands.action_intent.v1"; pub const HANDS_ACTION_REVIEW_SCHEMA_VERSION: &str = "epiphany.hands.action_review.v0"; pub const HANDS_COMMAND_RECEIPT_SCHEMA_VERSION: &str = "epiphany.hands.command_receipt.v0"; pub const HANDS_PATCH_RECEIPT_SCHEMA_VERSION: &str = "epiphany.hands.patch_receipt.v0"; @@ -45,6 +45,12 @@ pub struct HandsActionIntent { pub requested_at: String, #[cultcache(key = 10)] pub contract: String, + #[cultcache(key = 11, default)] + pub frontier_route_id: String, + #[cultcache(key = 12, default)] + pub plan_candidate_sha256: String, + #[cultcache(key = 13, default)] + pub plan_action: String, } #[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] @@ -475,6 +481,9 @@ mod tests { substrate_gate_grant_receipt_id: "substrate-grant-1".to_string(), requested_at: "2026-06-02T00:00:00Z".to_string(), contract: "Hands action intent is bounded by Substrate Gate.".to_string(), + frontier_route_id: String::new(), + plan_candidate_sha256: String::new(), + plan_action: String::new(), }; let review = hands_action_review_for_intent( "hands-review-1".to_string(), diff --git a/epiphany-core/src/lib.rs b/epiphany-core/src/lib.rs index 336bc920..f728303d 100644 --- a/epiphany-core/src/lib.rs +++ b/epiphany-core/src/lib.rs @@ -37,6 +37,8 @@ mod weksa_interlingua; pub use agent_launch::EPIPHANY_IMAGINATION_OWNER_ROLE; pub use agent_launch::EPIPHANY_IMAGINATION_ROLE_BINDING_ID; +pub use agent_launch::EPIPHANY_MIND_OWNER_ROLE; +pub use agent_launch::EPIPHANY_MIND_ROLE_BINDING_ID; pub use agent_launch::EPIPHANY_MODELING_OWNER_ROLE; pub use agent_launch::EPIPHANY_MODELING_ROLE_BINDING_ID; pub use agent_launch::EPIPHANY_REORIENT_LAUNCH_BINDING_ID; @@ -52,6 +54,7 @@ pub use agent_launch::EpiphanyReorientationPromptConfig; pub use agent_launch::EpiphanyRolePromptConfig; pub use agent_launch::EpiphanySharedPromptConfig; pub use agent_launch::EpiphanySpecialistPromptConfig; +pub use agent_launch::build_epiphany_frontier_plan_mind_launch_request; pub use agent_launch::build_epiphany_job_launch_request; pub use agent_launch::build_epiphany_reorient_launch_instruction; pub use agent_launch::build_epiphany_reorient_launch_request; @@ -59,6 +62,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_plan_mind_output_schema; 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; @@ -614,6 +618,7 @@ pub use memory_graph::EpiphanyMemoryPatchCandidate; pub use memory_graph::MEMORY_GRAPH_KEY; pub use memory_graph::MEMORY_GRAPH_SCHEMA_VERSION; pub use memory_graph::MEMORY_GRAPH_TYPE; +pub use memory_graph::RepoFrontierAdoptedPlan; pub use memory_graph::RepoFrontierItem; pub use memory_graph::RepoFrontierStatus; pub use memory_graph::RepoMemoryGraphRefresh; @@ -780,6 +785,8 @@ 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_plan_decision; +pub use runtime_spine::commit_repo_frontier_plan_mind_request; pub use runtime_spine::commit_repo_frontier_verification_request_for_chain; pub use runtime_spine::commit_repo_model_admission; pub use runtime_spine::commit_repo_model_claim_challenge; @@ -978,6 +985,8 @@ pub use surfaces::EpiphanyTokenUsageSnapshot; pub use surfaces::EpiphanyViewLens; pub use surfaces::EpiphanyWorkerLaunchDocument; pub use surfaces::REORIENT_WORKER_OUTPUT_CONTRACT_ID; +pub use surfaces::REPO_FRONTIER_PLAN_MIND_CONTEXT_CONTRACT; +pub use surfaces::REPO_FRONTIER_PLAN_MIND_CONTEXT_SCHEMA_VERSION; pub use surfaces::REPO_FRONTIER_PLANNING_CONTEXT_CONTRACT; pub use surfaces::REPO_FRONTIER_PLANNING_CONTEXT_SCHEMA_VERSION; pub use surfaces::REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_CONTRACT; @@ -985,6 +994,7 @@ pub use surfaces::REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_SCHEMA_VERSION; pub use surfaces::REPO_MODEL_CLAIM_REPAIR_CONTEXT_CONTRACT; pub use surfaces::REPO_MODEL_CLAIM_REPAIR_CONTEXT_SCHEMA_VERSION; pub use surfaces::ROLE_WORKER_OUTPUT_CONTRACT_ID; +pub use surfaces::RepoFrontierPlanMindContextProjection; pub use surfaces::RepoFrontierPlanningContextProjection; pub use surfaces::RepoFrontierProposalModelingContextProjection; pub use surfaces::RepoModelClaimRepairContextProjection; diff --git a/epiphany-core/src/memory_graph/mod.rs b/epiphany-core/src/memory_graph/mod.rs index ca4f8646..c8b89237 100644 --- a/epiphany-core/src/memory_graph/mod.rs +++ b/epiphany-core/src/memory_graph/mod.rs @@ -24,6 +24,7 @@ pub use epiphany_state_model::EpiphanyMemoryNodeKind; pub use epiphany_state_model::EpiphanyMemoryPatchCandidate; pub use epiphany_state_model::EpiphanyMemoryProfile; pub use epiphany_state_model::EpiphanyMemorySummary; +pub use epiphany_state_model::RepoFrontierAdoptedPlan; pub use epiphany_state_model::RepoFrontierItem; pub use epiphany_state_model::RepoFrontierStatus; pub use epiphany_state_model::RepoModelPatch; diff --git a/epiphany-core/src/memory_graph/store.rs b/epiphany-core/src/memory_graph/store.rs index 96c7cc36..86c140a3 100644 --- a/epiphany-core/src/memory_graph/store.rs +++ b/epiphany-core/src/memory_graph/store.rs @@ -222,6 +222,43 @@ pub fn derive_repo_model_patch( current_hash )); } + match &patch.purpose { + epiphany_state_model::RepoModelPatchPurpose::AdoptFrontierPlan { + planning_request_id, + result_id, + candidate_id, + } => { + if patch.operations.len() != 1 { + return Err(anyhow!( + "AdoptFrontierPlan purpose requires exactly one dedicated operation" + )); + } + let RepoModelPatchOperation::AdoptFrontierPlan { adopted_plan, .. } = + &patch.operations[0] + else { + return Err(anyhow!( + "AdoptFrontierPlan purpose requires its dedicated operation" + )); + }; + if adopted_plan.planning_request_id != *planning_request_id + || adopted_plan.result_id != *result_id + || adopted_plan.candidate_id != *candidate_id + { + return Err(anyhow!( + "AdoptFrontierPlan purpose and operation provenance mismatch" + )); + } + } + _ if patch.operations.iter().any(|operation| { + matches!(operation, RepoModelPatchOperation::AdoptFrontierPlan { .. }) + }) => + { + return Err(anyhow!( + "only AdoptFrontierPlan purpose may carry the dedicated adoption operation" + )); + } + _ => {} + } let mut next = current.clone(); if next .lifecycle_receipts @@ -231,7 +268,7 @@ pub fn derive_repo_model_patch( return Err(anyhow!("repo model patch_id has already been applied")); } for operation in &patch.operations { - apply_operation(&mut next, operation, &patch.applied_at)?; + apply_operation(&mut next, operation, &patch.purpose, &patch.applied_at)?; } next.model_revision = next .model_revision @@ -288,6 +325,7 @@ pub fn derive_repo_model_patch( fn apply_operation( snapshot: &mut EpiphanyMemoryGraphSnapshot, operation: &RepoModelPatchOperation, + purpose: &epiphany_state_model::RepoModelPatchPurpose, applied_at: &str, ) -> Result<()> { match operation { @@ -320,18 +358,55 @@ fn apply_operation( .ok_or_else(|| anyhow!("cannot retire missing edge {edge_id}"))?; edge.lifecycle = EpiphanyMemoryLifecycle::Retired; } - RepoModelPatchOperation::UpsertFrontier { item } => insert_new( - &mut snapshot.frontier, - item.clone(), - |value| &value.id, - "frontier item", - )?, - RepoModelPatchOperation::ReviseFrontier { item } => revise( - &mut snapshot.frontier, - item.clone(), - |value| &value.id, - "frontier item", - )?, + RepoModelPatchOperation::UpsertFrontier { item } => { + if item.adopted_plan.is_some() { + return Err(anyhow!( + "generic frontier upsert cannot author an adopted plan" + )); + } + insert_new( + &mut snapshot.frontier, + item.clone(), + |value| &value.id, + "frontier item", + )? + } + RepoModelPatchOperation::ReviseFrontier { item } => { + let existing = snapshot + .frontier + .iter() + .find(|existing| existing.id == item.id) + .ok_or_else(|| anyhow!("cannot revise missing frontier item {}", item.id))?; + let verdict_may_close_adopted = matches!( + purpose, + epiphany_state_model::RepoModelPatchPurpose::IncorporateFrontierVerdict { .. } + ) && existing.adopted_plan.is_some() + && item.adopted_plan == existing.adopted_plan + && item.migration_body == existing.migration_body + && item.question == existing.question + && item.target_claim_ids == existing.target_claim_ids + && item.source_scope == existing.source_scope + && item.recommended_next_organ == existing.recommended_next_organ + && item.dependency_item_ids == existing.dependency_item_ids + && item.created_at == existing.created_at + && item.retired_at == existing.retired_at + && item.superseded_by == existing.superseded_by; + if (existing.adopted_plan.is_some() && !verdict_may_close_adopted) + || item.adopted_plan != existing.adopted_plan + || (existing.recommended_next_organ == "Imagination" + && item.recommended_next_organ == "Hands") + { + return Err(anyhow!( + "generic frontier revision cannot alter adopted execution anatomy or own plan adoption" + )); + } + revise( + &mut snapshot.frontier, + item.clone(), + |value| &value.id, + "frontier item", + )? + } RepoModelPatchOperation::RetireFrontier { item_id, retired_at, @@ -351,6 +426,56 @@ fn apply_operation( item.updated_at = Some(applied_at.to_string()); item.superseded_by = superseded_by.clone(); } + RepoModelPatchOperation::AdoptFrontierPlan { + frontier_item_id, + expected_frontier_item_hash, + adopted_plan, + } => { + let item = snapshot + .frontier + .iter_mut() + .find(|item| &item.id == frontier_item_id) + .ok_or_else(|| { + anyhow!("cannot adopt plan for missing frontier item {frontier_item_id}") + })?; + let current_hash = format!("{:x}", Sha256::digest(rmp_serde::to_vec_named(&*item)?)); + let safe_paths_are_bounded = !adopted_plan.safe_paths.is_empty() + && adopted_plan + .safe_paths + .windows(2) + .all(|pair| pair[0] < pair[1]) + && adopted_plan.safe_paths.iter().all(|path| { + item.source_scope.iter().any(|scope| { + path == scope + || path + .starts_with(&format!("{}/", scope.trim_end_matches(['/', '\\']))) + }) + }); + if current_hash != *expected_frontier_item_hash + || item.status != RepoFrontierStatus::Active + || item.recommended_next_organ != "Imagination" + || item.adopted_plan.is_some() + || adopted_plan.planning_request_id.trim().is_empty() + || adopted_plan.result_id.trim().is_empty() + || adopted_plan.job_id.trim().is_empty() + || adopted_plan.candidate_id.trim().is_empty() + || adopted_plan.candidate_sha256.trim().is_empty() + || adopted_plan.action.trim().is_empty() + || adopted_plan.command.trim().is_empty() + || adopted_plan.checks.is_empty() + || adopted_plan.stop_conditions.is_empty() + || adopted_plan.rollback_steps.is_empty() + || adopted_plan.commit_message.trim().is_empty() + || !safe_paths_are_bounded + { + return Err(anyhow!( + "frontier plan adoption requires the exact active Imagination frontier and complete bounded plan" + )); + } + item.adopted_plan = Some(adopted_plan.clone()); + item.recommended_next_organ = "Hands".to_string(); + item.updated_at = Some(applied_at.to_string()); + } } Ok(()) } diff --git a/epiphany-core/src/memory_graph/tests.rs b/epiphany-core/src/memory_graph/tests.rs index 5bb7c308..d013003b 100644 --- a/epiphany-core/src/memory_graph/tests.rs +++ b/epiphany-core/src/memory_graph/tests.rs @@ -714,6 +714,60 @@ fn retiring_frontier_target_requires_resolving_frontier_in_same_patch() -> Resul Ok(()) } +#[test] +fn verdict_incorporation_closes_adopted_frontier_without_rewriting_execution_plan() -> Result<()> { + let mut snapshot = fixture_snapshot(); + let claim = snapshot.nodes[0].id.clone(); + let mut item = frontier_item("adopted-work", &claim); + item.recommended_next_organ = "Hands".into(); + item.adopted_plan = Some(epiphany_state_model::RepoFrontierAdoptedPlan { + planning_request_id: "planning-1".into(), + result_id: "imagination-result-1".into(), + job_id: "imagination-job-1".into(), + candidate_id: "candidate-1".into(), + candidate_sha256: "candidate-hash-1".into(), + safe_paths: item.source_scope.clone(), + action: "Implement exact bounded change".into(), + command: "cargo test --lib".into(), + checks: vec!["focused tests pass".into()], + stop_conditions: vec!["scope changes".into()], + rollback_steps: vec!["revert commit".into()], + commit_message: "Implement exact bounded change".into(), + }); + snapshot.frontier = vec![item.clone()]; + make_canonical(&mut snapshot, 1); + let mut resolved = item.clone(); + resolved.status = RepoFrontierStatus::Resolved; + resolved.evidence_refs = vec!["verification-request-1".into(), "soul-verdict-1".into()]; + resolved.updated_at = Some("2026-07-15T12:00:00Z".into()); + let patch = RepoModelPatch { + patch_id: "incorporate-adopted-verdict".into(), + base_revision: snapshot.model_revision, + base_hash: memory_graph_model_hash(&snapshot)?, + applied_at: "2026-07-15T12:00:00Z".into(), + purpose: RepoModelPatchPurpose::IncorporateFrontierVerdict { + route_id: "route-1".into(), + soul_verdict_receipt_id: "soul-verdict-1".into(), + }, + operations: vec![RepoModelPatchOperation::ReviseFrontier { + item: resolved.clone(), + }], + }; + let next = derive_repo_model_patch(&snapshot, &patch)?; + assert_eq!(next.frontier[0].status, RepoFrontierStatus::Resolved); + assert_eq!(next.frontier[0].adopted_plan, item.adopted_plan); + + let mut substituted = resolved; + substituted.adopted_plan.as_mut().unwrap().command = "cargo test unrelated".into(); + let hostile = RepoModelPatch { + patch_id: "rewrite-adopted-plan".into(), + operations: vec![RepoModelPatchOperation::ReviseFrontier { item: substituted }], + ..patch + }; + assert!(derive_repo_model_patch(&snapshot, &hostile).is_err()); + Ok(()) +} + #[test] fn memory_context_projects_frontier_prerequisites_first_under_budget() { let mut snapshot = fixture_snapshot(); diff --git a/epiphany-core/src/prompts/epiphany_specialists.toml b/epiphany-core/src/prompts/epiphany_specialists.toml index e3c2855b..a01a0cee 100644 --- a/epiphany-core/src/prompts/epiphany_specialists.toml +++ b/epiphany-core/src/prompts/epiphany_specialists.toml @@ -67,6 +67,12 @@ Heartbeat: every lane, including the Self, can be woken by the harness initiativ ''' [roles] +mind = ''' +Act as one bounded admission-review procedure serving Epiphany Mind for one frontier-plan judgment. +You are not an embodied lane, not Mind itself, not a second planner, and not an implementation worker. Mind remains persistent state and admission authority. Inspect only the exact typed review context: the immutable Imagination result, its embedded candidate, the planning request, and the current authority bindings carried there. +Choose `adopt` only when the candidate is coherent, bounded by its safe paths, executable, falsifiable through its checks, honest about stop and rollback conditions, and suitable to become canonical Hands cargo. Choose `refuse` when it is wrong or unsafe. Choose `hold` when a specific missing fact prevents judgment. Give a concrete rationale grounded in the typed candidate. +Echo every causal identifier and hash exactly. Do not emit state patches, repo-model patches, self patches, replacement plans, or authority belonging to another organ. Your immutable typed result is a judgment proposal; the admission transaction will independently replay its full causal chain before changing persistent state. +''' imagination = ''' Act as the Epiphany planning synthesis specialist for one bounded pass. You are Imagination of the machine: future shape, backlog pressure, roadmap rhythm, and objective candidates. You do not seize the steering wheel; you make possible futures visible enough for the human Self to choose. diff --git a/epiphany-core/src/repo_model_gateway.rs b/epiphany-core/src/repo_model_gateway.rs index 65a19c1d..684a9a75 100644 --- a/epiphany-core/src/repo_model_gateway.rs +++ b/epiphany-core/src/repo_model_gateway.rs @@ -7,18 +7,18 @@ pub const REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION: &str = "epiphany.mind.repo_model_admission_review.v0"; pub const REPO_MODEL_ADMISSION_RECEIPT_TYPE: &str = "epiphany.mind.repo_model_admission_receipt"; pub const REPO_MODEL_ADMISSION_RECEIPT_SCHEMA_VERSION: &str = - "epiphany.mind.repo_model_admission_receipt.v3"; + "epiphany.mind.repo_model_admission_receipt.v4"; pub const REPO_MODEL_MIGRATION_RECEIPT_TYPE: &str = "epiphany.mind.repo_model_migration_receipt"; pub const REPO_MODEL_MIGRATION_RECEIPT_SCHEMA_VERSION: &str = "epiphany.mind.repo_model_migration_receipt.v0"; -pub const REPO_MODEL_ADMISSION_CONTRACT: &str = "epiphany.repo_model_admission.v3"; +pub const REPO_MODEL_ADMISSION_CONTRACT: &str = "epiphany.repo_model_admission.v4"; pub const REPO_MODEL_MIGRATION_CONTRACT: &str = "epiphany.repo_model_migration.v0"; pub const REPO_FRONTIER_ROUTE_TYPE: &str = "epiphany.self.repo_frontier_route"; -pub const REPO_FRONTIER_ROUTE_SCHEMA_VERSION: &str = "epiphany.self.repo_frontier_route.v0"; +pub const REPO_FRONTIER_ROUTE_SCHEMA_VERSION: &str = "epiphany.self.repo_frontier_route.v1"; pub const REPO_FRONTIER_HANDS_AUTHORITY_TYPE: &str = "epiphany.hands.repo_frontier_authority"; pub const REPO_FRONTIER_HANDS_AUTHORITY_SCHEMA_VERSION: &str = "epiphany.hands.repo_frontier_authority.v0"; -pub const REPO_FRONTIER_ROUTE_CONTRACT: &str = "epiphany.repo_frontier_route.v0"; +pub const REPO_FRONTIER_ROUTE_CONTRACT: &str = "epiphany.repo_frontier_route.v1"; pub const REPO_FRONTIER_HANDS_AUTHORITY_CONTRACT: &str = "epiphany.repo_frontier_hands_authority.v0"; pub const REPO_FRONTIER_MODELING_REQUEST_TYPE: &str = @@ -33,8 +33,9 @@ pub const REPO_FRONTIER_PLANNING_REQUEST_SCHEMA_VERSION: &str = "epiphany.self.repo_frontier_planning_request.v1"; pub const REPO_FRONTIER_PLAN_CANDIDATE_SCHEMA_VERSION: &str = "epiphany.imagination.repo_frontier_plan_candidate.v0"; -pub const REPO_FRONTIER_PLAN_ADOPTION_SCHEMA_VERSION: &str = - "epiphany.mind.repo_frontier_plan_adoption.v0"; +pub const REPO_FRONTIER_PLAN_DECISION_RECEIPT_SCHEMA_VERSION: &str = + "epiphany.mind.repo_frontier_plan_decision_receipt.v0"; +pub const REPO_FRONTIER_PLAN_DECISION_CONTRACT: &str = "epiphany.repo_frontier_plan_decision.v0"; pub const REPO_FRONTIER_PLANNING_CONTRACT: &str = "epiphany.repo_frontier_planning.v1"; pub const REPO_FRONTIER_WORK_PROPOSAL_CONTRACT: &str = "epiphany.repo_frontier_work_proposal.inert.v0"; @@ -61,6 +62,14 @@ pub const REPO_FRONTIER_PLANNING_LAUNCH_BINDING_SCHEMA_VERSION: &str = "epiphany.coordinator.repo_frontier_planning_launch_binding.v0"; pub const REPO_FRONTIER_PLANNING_LAUNCH_BINDING_CONTRACT: &str = "epiphany.repo_frontier_planning_launch_binding.v0"; +pub const REPO_FRONTIER_PLAN_MIND_REQUEST_SCHEMA_VERSION: &str = + "epiphany.self.repo_frontier_plan_mind_request.v0"; +pub const REPO_FRONTIER_PLAN_MIND_REQUEST_CONTRACT: &str = + "epiphany.repo_frontier_plan_mind_request.v0"; +pub const REPO_FRONTIER_PLAN_MIND_LAUNCH_BINDING_SCHEMA_VERSION: &str = + "epiphany.coordinator.repo_frontier_plan_mind_launch_binding.v0"; +pub const REPO_FRONTIER_PLAN_MIND_LAUNCH_BINDING_CONTRACT: &str = + "epiphany.repo_frontier_plan_mind_launch_binding.v0"; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -443,35 +452,111 @@ pub enum RepoFrontierPlanDecision { #[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] #[cultcache( - type = "epiphany.mind.repo_frontier_plan_adoption", - schema = "RepoFrontierPlanAdoption" + type = "epiphany.self.repo_frontier_plan_mind_request", + schema = "RepoFrontierPlanMindRequest" )] -pub struct RepoFrontierPlanAdoption { +pub struct RepoFrontierPlanMindRequest { #[cultcache(key = 0)] pub schema_version: String, #[cultcache(key = 1)] - pub adoption_id: String, + pub request_id: String, #[cultcache(key = 2)] - pub candidate_id: String, - #[cultcache(key = 3)] - pub candidate_sha256: String, - #[cultcache(key = 4)] pub planning_request_id: String, + #[cultcache(key = 3)] + pub imagination_result_id: String, + #[cultcache(key = 4)] + pub imagination_job_id: String, #[cultcache(key = 5)] - pub model_revision: u64, + pub candidate_id: String, #[cultcache(key = 6)] - pub model_hash: String, + pub candidate_sha256: String, #[cultcache(key = 7)] - pub frontier_item_id: String, + pub runtime_id: String, #[cultcache(key = 8)] - pub frontier_item_hash: String, + pub thread_id: String, #[cultcache(key = 9)] - pub decision: RepoFrontierPlanDecision, + pub requested_at: String, #[cultcache(key = 10)] + pub contract: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.coordinator.repo_frontier_plan_mind_launch_binding", + schema = "RepoFrontierPlanMindLaunchBinding" +)] +pub struct RepoFrontierPlanMindLaunchBinding { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub binding_record_id: String, + #[cultcache(key = 2)] + pub mind_request_id: String, + #[cultcache(key = 3)] + pub job_id: String, + #[cultcache(key = 4)] + pub binding_id: String, + #[cultcache(key = 5)] + pub runtime_id: String, + #[cultcache(key = 6)] + pub thread_id: String, + #[cultcache(key = 7)] + pub launched_at: String, + #[cultcache(key = 8)] + pub worker_launch_document_sha256: String, + #[cultcache(key = 9)] + pub contract: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RepoFrontierPlanMindDecision { + pub mind_request_id: String, + pub planning_request_id: String, + pub imagination_result_id: String, + pub candidate_id: String, + pub candidate_sha256: String, + pub decision: RepoFrontierPlanDecision, pub rationale: String, - #[cultcache(key = 11)] pub decided_at: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.mind.repo_frontier_plan_decision_receipt", + schema = "RepoFrontierPlanDecisionReceipt" +)] +pub struct RepoFrontierPlanDecisionReceipt { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub decision_id: String, + #[cultcache(key = 2)] + pub planning_request_id: String, + #[cultcache(key = 3)] + pub source_result_id: String, + #[cultcache(key = 4)] + pub source_job_id: String, + #[cultcache(key = 5)] + pub candidate_id: String, + #[cultcache(key = 6)] + pub candidate_sha256: String, + #[cultcache(key = 7)] + pub model_revision: u64, + #[cultcache(key = 8)] + pub model_hash: String, + #[cultcache(key = 9)] + pub frontier_item_id: String, + #[cultcache(key = 10)] + pub frontier_item_hash: String, + #[cultcache(key = 11)] + pub decision: RepoFrontierPlanDecision, #[cultcache(key = 12)] + pub rationale: String, + #[cultcache(key = 13)] + pub decided_at: String, + #[cultcache(key = 14, default)] + pub model_admission_receipt_id: String, + #[cultcache(key = 15)] pub contract: String, } @@ -558,9 +643,11 @@ pub struct RepoFrontierRoute { pub target_claim_ids: Vec, #[cultcache(key = 12)] pub source_scope: Vec, - #[cultcache(key = 13)] - pub selected_at: String, + #[cultcache(key = 13, default)] + pub adopted_plan: Option, #[cultcache(key = 14)] + pub selected_at: String, + #[cultcache(key = 15)] pub contract: String, } @@ -674,6 +761,8 @@ pub struct RepoModelAdmissionReceipt { pub proposal_modeling_request_id: String, #[cultcache(key = 18, default)] pub claim_repair_request_id: String, + #[cultcache(key = 19, default)] + pub frontier_plan_decision_id: String, } #[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] diff --git a/epiphany-core/src/runtime_spine.rs b/epiphany-core/src/runtime_spine.rs index ec5b61a8..afc3bce7 100644 --- a/epiphany-core/src/runtime_spine.rs +++ b/epiphany-core/src/runtime_spine.rs @@ -1,7 +1,9 @@ use crate::EpiphanyWorkerLaunchDocument; +use crate::RepoFrontierPlanMindContextProjection; use crate::agent_launch::{ EPIPHANY_IMAGINATION_OWNER_ROLE, EPIPHANY_IMAGINATION_ROLE_BINDING_ID, - EPIPHANY_MODELING_OWNER_ROLE, EPIPHANY_MODELING_ROLE_BINDING_ID, + EPIPHANY_MIND_OWNER_ROLE, EPIPHANY_MIND_ROLE_BINDING_ID, EPIPHANY_MODELING_OWNER_ROLE, + EPIPHANY_MODELING_ROLE_BINDING_ID, }; use crate::agent_memory::AGENT_MEMORY_TYPE; use crate::continuity_gateway::ContinuityRecoveryReceipt; @@ -47,7 +49,11 @@ use crate::organ_dependencies::EpiphanyLaunchOrganContract; 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_PLAN_CANDIDATE_SCHEMA_VERSION, REPO_FRONTIER_PLAN_DECISION_CONTRACT, + REPO_FRONTIER_PLAN_DECISION_RECEIPT_SCHEMA_VERSION, + REPO_FRONTIER_PLAN_MIND_LAUNCH_BINDING_CONTRACT, + REPO_FRONTIER_PLAN_MIND_LAUNCH_BINDING_SCHEMA_VERSION, + REPO_FRONTIER_PLAN_MIND_REQUEST_CONTRACT, REPO_FRONTIER_PLAN_MIND_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, @@ -63,8 +69,10 @@ use crate::repo_model_gateway::{ REPO_MODEL_CLAIM_REPAIR_REQUEST_CONTRACT, REPO_MODEL_CLAIM_REPAIR_REQUEST_SCHEMA_VERSION, REPO_MODEL_MIGRATION_CONTRACT, REPO_MODEL_MIGRATION_RECEIPT_SCHEMA_VERSION, REPO_MODEL_MIGRATION_RECEIPT_TYPE, RepoFrontierHandsAuthority, RepoFrontierModelingRequest, - RepoFrontierNextOrgan, RepoFrontierPlanAdoption, RepoFrontierPlanCandidate, - RepoFrontierPlanDecision, RepoFrontierPlanningLaunchBinding, RepoFrontierPlanningRequest, + RepoFrontierNextOrgan, RepoFrontierPlanCandidate, RepoFrontierPlanDecision, + RepoFrontierPlanDecisionReceipt, RepoFrontierPlanMindDecision, + RepoFrontierPlanMindLaunchBinding, RepoFrontierPlanMindRequest, + RepoFrontierPlanningLaunchBinding, RepoFrontierPlanningRequest, RepoFrontierProposalModelingLaunchBinding, RepoFrontierProposalModelingRequest, RepoFrontierRoute, RepoFrontierVerdictDisposition, RepoFrontierWorkProposal, RepoModelAdmissionReceipt, RepoModelAdmissionReview, RepoModelClaimChallenge, @@ -335,6 +343,8 @@ pub struct EpiphanyRuntimeWorkerLaunchRequest { pub claim_repair_request_id: Option, #[cultcache(key = 13, default)] pub frontier_planning_request_id: Option, + #[cultcache(key = 14, default)] + pub frontier_plan_mind_request_id: Option, } impl EpiphanyRuntimeWorkerLaunchRequest { @@ -416,6 +426,10 @@ pub struct EpiphanyRuntimeRoleWorkerResult { pub frontier_planning_request_id: Option, #[cultcache(key = 27, default)] pub frontier_plan_candidate_msgpack: Option>, + #[cultcache(key = 28, default)] + pub frontier_plan_mind_request_id: Option, + #[cultcache(key = 29, default)] + pub frontier_plan_mind_decision_msgpack: Option>, } impl EpiphanyRuntimeRoleWorkerResult { @@ -443,6 +457,13 @@ impl EpiphanyRuntimeRoleWorkerResult { "role worker frontierPlanCandidate", ) } + + pub fn frontier_plan_mind_decision(&self) -> Result> { + decode_optional_msgpack( + self.frontier_plan_mind_decision_msgpack.as_deref(), + "role worker frontierPlanMindDecision", + ) + } } #[derive(Clone, Debug, PartialEq, DatabaseEntry)] @@ -702,6 +723,7 @@ pub struct RuntimeSpineHeartbeatJobOptions { pub proposal_modeling_request_id: Option, pub claim_repair_request_id: Option, pub frontier_planning_request_id: Option, + pub frontier_plan_mind_request_id: Option, pub created_at: String, } @@ -752,6 +774,7 @@ pub struct EpiphanyJobLaunchRequest { pub proposal_modeling_request_id: Option, pub claim_repair_request_id: Option, pub frontier_planning_request_id: Option, + pub frontier_plan_mind_request_id: Option, } #[derive(Debug, Clone, PartialEq)] @@ -808,7 +831,9 @@ pub fn runtime_spine_cache(store_path: impl AsRef) -> Result { cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; - cache.register_entry_type::()?; + cache.register_entry_type::()?; + cache.register_entry_type::()?; + cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; @@ -1167,6 +1192,7 @@ pub fn open_runtime_spine_heartbeat_job( proposal_modeling_request_id: options.proposal_modeling_request_id, claim_repair_request_id: options.claim_repair_request_id, frontier_planning_request_id: options.frontier_planning_request_id, + frontier_plan_mind_request_id: options.frontier_plan_mind_request_id, }; cache.put(&job_id, &request)?; Ok(job) @@ -1312,6 +1338,7 @@ pub fn prepare_runtime_spine_heartbeat_job( proposal_modeling_request_id: options.proposal_modeling_request_id, claim_repair_request_id: options.claim_repair_request_id, frontier_planning_request_id: options.frontier_planning_request_id, + frontier_plan_mind_request_id: options.frontier_plan_mind_request_id, }; let envelopes = vec![ cache.prepare_entry(RUNTIME_IDENTITY_KEY, &identity)?.0, @@ -1584,6 +1611,11 @@ pub fn put_runtime_role_worker_result( "only Imagination results may echo a frontier planning request" )); } + if result.item_error.is_some() { + return Err(anyhow!( + "frontier planning result with an item error cannot carry an executable candidate" + )); + } if result.state_patch_msgpack.is_some() || result.self_patch_msgpack.is_some() || result.repo_model_patch_msgpack.is_some() @@ -1659,6 +1691,89 @@ pub fn put_runtime_role_worker_result( )); } } + let has_mind_echo = result.frontier_plan_mind_request_id.is_some(); + let has_mind_decision = result.frontier_plan_mind_decision_msgpack.is_some(); + if has_mind_echo != has_mind_decision { + return Err(anyhow!( + "Mind result requires both exact request echo and typed decision" + )); + } + if has_mind_echo { + if !result.role_id.eq_ignore_ascii_case("mindAdmissionReview") + || result.item_error.is_some() + || result.state_patch_msgpack.is_some() + || result.self_patch_msgpack.is_some() + || result.repo_model_patch_msgpack.is_some() + || result.frontier_planning_request_id.is_some() + || result.frontier_plan_candidate_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!( + "Mind decision result carries foreign authority cargo" + )); + } + let request_id = result.frontier_plan_mind_request_id.as_deref().unwrap(); + let request = cache + .get::(request_id)? + .ok_or_else(|| anyhow!("Mind result request disappeared"))?; + let (planning, candidate) = validate_repo_frontier_plan_mind_request(&cache, &request)?; + let decision = result + .frontier_plan_mind_decision()? + .ok_or_else(|| anyhow!("Mind decision disappeared"))?; + if decision.mind_request_id != request.request_id + || decision.planning_request_id != planning.request_id + || decision.imagination_result_id != request.imagination_result_id + || decision.candidate_id != candidate.candidate_id + || decision.candidate_sha256 != request.candidate_sha256 + || decision.rationale.trim().is_empty() + || chrono::DateTime::parse_from_rfc3339(&decision.decided_at).is_err() + { + return Err(anyhow!( + "Mind decision substituted request echo or immutable candidate identity" + )); + } + let bindings = cache + .get_all::()? + .into_iter() + .filter(|b| b.mind_request_id == request.request_id) + .collect::>(); + if bindings.len() != 1 { + return Err(anyhow!( + "Mind result requires exactly one coordinator launch binding" + )); + } + let binding = &bindings[0]; + let launch = cache + .get::(&result.job_id)? + .ok_or_else(|| anyhow!("Mind result launch disappeared"))?; + let document = launch.launch_document()?; + let projection = match &document { + EpiphanyWorkerLaunchDocument::Role(d) => d.frontier_plan_mind_context.as_ref(), + _ => None, + }; + let expected = RepoFrontierPlanMindContextProjection::new(&request, &planning, &candidate); + let hash = format!("{:x}", Sha256::digest(&launch.launch_document_msgpack)); + if binding.schema_version != REPO_FRONTIER_PLAN_MIND_LAUNCH_BINDING_SCHEMA_VERSION + || binding.contract != REPO_FRONTIER_PLAN_MIND_LAUNCH_BINDING_CONTRACT + || binding.job_id != result.job_id + || binding.binding_id != EPIPHANY_MIND_ROLE_BINDING_ID + || binding.runtime_id != request.runtime_id + || binding.thread_id != request.thread_id + || binding.worker_launch_document_sha256 != hash + || launch.role != EPIPHANY_MIND_OWNER_ROLE + || launch.binding_id != EPIPHANY_MIND_ROLE_BINDING_ID + || launch.frontier_plan_mind_request_id.as_deref() != Some(request.request_id.as_str()) + || projection != Some(&expected) + { + return Err(anyhow!( + "Mind 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(()) @@ -1868,6 +1983,7 @@ pub fn commit_repo_model_admission( != REPO_MODEL_ADMISSION_RECEIPT_SCHEMA_VERSION || existing_receipt.purpose != patch.purpose || existing_receipt.claim_repair_request_id != repair_request_id + || !existing_receipt.frontier_plan_decision_id.is_empty() || existing_receipt.previous_revision != repair_request.model_revision || existing_receipt.previous_hash != repair_request.model_hash || existing_receipt.admitted_revision != current_model.model_revision @@ -1898,6 +2014,11 @@ pub fn commit_repo_model_admission( proposal_modeling_request_id, claim_repair_request_id, ) = match &patch.purpose { + crate::RepoModelPatchPurpose::AdoptFrontierPlan { .. } => { + return Err(anyhow!( + "frontier plan adoption is Mind-owned and cannot enter through generic repo model admission" + )); + } crate::RepoModelPatchPurpose::RepairClaim => { if result.repo_frontier_modeling_request_id.is_some() || result.proposal_modeling_request_id.is_some() @@ -2515,6 +2636,7 @@ pub fn commit_repo_model_admission( || existing_receipt.frontier_modeling_request_id != frontier_modeling_request_id || existing_receipt.proposal_modeling_request_id != proposal_modeling_request_id || existing_receipt.claim_repair_request_id != claim_repair_request_id + || !existing_receipt.frontier_plan_decision_id.is_empty() { return Err(anyhow!("repo model admission receipt identity collision")); } @@ -2634,6 +2756,7 @@ pub fn commit_repo_model_admission( frontier_modeling_request_id, proposal_modeling_request_id, claim_repair_request_id, + frontier_plan_decision_id: String::new(), }; let (next_model_envelope, _) = cache.prepare_entry(crate::MEMORY_GRAPH_KEY, &next_entry)?; let (review_envelope, _) = cache.prepare_entry(&review.review_id, review)?; @@ -2941,18 +3064,6 @@ pub fn select_and_commit_repo_frontier_planning_request( let item = actionable_imagination_frontier_item(&model, &challenges) .ok_or_else(|| anyhow!("planning requires an actionable Imagination frontier"))?; let item_hash = format!("{:x}", Sha256::digest(rmp_serde::to_vec_named(item)?)); - if cache - .get_all::()? - .iter() - .any(|a| { - a.decision == RepoFrontierPlanDecision::Adopt - && a.model_hash == model_hash - && a.frontier_item_id == item.id - && a.frontier_item_hash == item_hash - }) - { - return Err(anyhow!("current frontier already has an adopted plan")); - } let request_id = format!( "repo-frontier-planning-{:x}", Sha256::digest( @@ -2963,6 +3074,15 @@ pub fn select_and_commit_repo_frontier_planning_request( .as_bytes(), ) ); + if cache + .get_all::()? + .iter() + .any(|decision| decision.planning_request_id == request_id) + { + return Err(anyhow!( + "current frontier planning request already has a terminal Mind decision" + )); + } let request = RepoFrontierPlanningRequest { schema_version: REPO_FRONTIER_PLANNING_REQUEST_SCHEMA_VERSION.into(), request_id: request_id.clone(), @@ -3071,6 +3191,151 @@ pub(crate) fn validate_current_repo_frontier_planning_request( Ok(()) } +pub fn commit_repo_frontier_plan_mind_request( + runtime_store: impl AsRef, + imagination_result_id: &str, + requested_at: &str, +) -> Result { + chrono::DateTime::parse_from_rfc3339(requested_at) + .map_err(|_| anyhow!("Mind request timestamp must be RFC3339"))?; + let runtime_store = runtime_store.as_ref(); + let mut cache = runtime_spine_cache(runtime_store)?; + cache.pull_all_backing_stores()?; + let results = cache + .get_all::()? + .into_iter() + .filter(|result| result.result_id == imagination_result_id) + .collect::>(); + if results.len() != 1 { + return Err(anyhow!( + "Mind request requires exactly one immutable Imagination result" + )); + } + let result = &results[0]; + put_runtime_role_worker_result(runtime_store, result)?; + let planning_request_id = result + .frontier_planning_request_id + .as_deref() + .ok_or_else(|| anyhow!("Mind request source lacks planning request echo"))?; + let planning = cache + .get::(planning_request_id)? + .ok_or_else(|| anyhow!("Mind request source planning request disappeared"))?; + let candidate = result + .frontier_plan_candidate()? + .ok_or_else(|| anyhow!("Mind request source candidate disappeared"))?; + validate_current_repo_frontier_planning_request(&cache, &planning)?; + validate_repo_frontier_plan_candidate_against_request(&cache, &candidate, &planning)?; + let candidate_sha256 = format!("{:x}", Sha256::digest(rmp_serde::to_vec_named(&candidate)?)); + let request_id = format!( + "repo-frontier-plan-mind-{:x}", + Sha256::digest( + format!( + "{}:{}:{}:{}", + planning.runtime_id, planning.thread_id, result.result_id, candidate_sha256 + ) + .as_bytes() + ) + ); + let request = RepoFrontierPlanMindRequest { + schema_version: REPO_FRONTIER_PLAN_MIND_REQUEST_SCHEMA_VERSION.into(), + request_id: request_id.clone(), + planning_request_id: planning.request_id, + imagination_result_id: result.result_id.clone(), + imagination_job_id: result.job_id.clone(), + candidate_id: candidate.candidate_id, + candidate_sha256, + runtime_id: planning.runtime_id, + thread_id: planning.thread_id, + requested_at: requested_at.into(), + contract: REPO_FRONTIER_PLAN_MIND_REQUEST_CONTRACT.into(), + }; + if let Some(existing) = cache.get::(&request_id)? { + let mut retry = request.clone(); + retry.requested_at = existing.requested_at.clone(); + return if existing == retry { + Ok(existing) + } else { + Err(anyhow!("Mind request identity collision")) + }; + } + let (envelope, _) = cache.prepare_entry(&request_id, &request)?; + let backing = SingleFileMessagePackBackingStore::new(runtime_store); + let current_envelope = backing + .pull_all()? + .into_iter() + .find(|entry| { + entry.r#type == crate::MEMORY_GRAPH_TYPE && entry.key == crate::MEMORY_GRAPH_KEY + }) + .ok_or_else(|| anyhow!("Mind request requires canonical RepoModel"))?; + let current_entry: crate::EpiphanyMemoryGraphEntry = + rmp_serde::from_slice(¤t_envelope.payload)?; + crate::validate_memory_graph_entry(¤t_entry)?; + let current = current_entry.snapshot()?; + if current.model_revision != planning.model_revision + || crate::memory_graph_model_hash(¤t)? != planning.model_hash + { + return Err(anyhow!( + "Mind request model changed before immutable insert" + )); + } + if !backing.compare_and_swap_batch( + std::slice::from_ref(¤t_envelope), + vec![current_envelope.clone(), envelope], + )? { + return Err(anyhow!( + "Mind request lost current-model CAS or immutable request claim" + )); + } + Ok(request) +} + +pub(crate) fn validate_repo_frontier_plan_mind_request( + cache: &CultCache, + request: &RepoFrontierPlanMindRequest, +) -> Result<(RepoFrontierPlanningRequest, RepoFrontierPlanCandidate)> { + let (planning, candidate) = validate_repo_frontier_plan_mind_request_identity(cache, request)?; + validate_current_repo_frontier_planning_request(cache, &planning)?; + validate_repo_frontier_plan_candidate_against_request(cache, &candidate, &planning)?; + Ok((planning, candidate)) +} + +fn validate_repo_frontier_plan_mind_request_identity( + cache: &CultCache, + request: &RepoFrontierPlanMindRequest, +) -> Result<(RepoFrontierPlanningRequest, RepoFrontierPlanCandidate)> { + if request.schema_version != REPO_FRONTIER_PLAN_MIND_REQUEST_SCHEMA_VERSION + || request.contract != REPO_FRONTIER_PLAN_MIND_REQUEST_CONTRACT + || chrono::DateTime::parse_from_rfc3339(&request.requested_at).is_err() + { + return Err(anyhow!("invalid typed Mind request")); + } + let result = cache + .get_all::()? + .into_iter() + .find(|r| r.result_id == request.imagination_result_id) + .ok_or_else(|| anyhow!("Mind request source result disappeared"))?; + let planning = cache + .get::(&request.planning_request_id)? + .ok_or_else(|| anyhow!("Mind request planning request disappeared"))?; + let candidate = result + .frontier_plan_candidate()? + .ok_or_else(|| anyhow!("Mind request candidate disappeared"))?; + let hash = format!("{:x}", Sha256::digest(rmp_serde::to_vec_named(&candidate)?)); + if result.job_id != request.imagination_job_id + || result.frontier_planning_request_id.as_deref() + != Some(request.planning_request_id.as_str()) + || candidate.candidate_id != request.candidate_id + || hash != request.candidate_sha256 + || request.runtime_id != planning.runtime_id + || request.thread_id != planning.thread_id + { + return Err(anyhow!( + "Mind request substituted immutable Imagination causal identity" + )); + } + Ok((planning, candidate)) +} + fn actionable_imagination_frontier_item<'a>( model: &'a crate::EpiphanyMemoryGraphSnapshot, challenges: &[RepoModelClaimChallenge], @@ -3571,79 +3836,6 @@ pub(crate) fn validate_current_repo_model_claim_repair_request( Ok(()) } -pub(crate) fn put_repo_frontier_plan_candidate( - store_path: impl AsRef, - candidate: &RepoFrontierPlanCandidate, -) -> Result<()> { - if candidate.schema_version != REPO_FRONTIER_PLAN_CANDIDATE_SCHEMA_VERSION - || candidate.contract != REPO_FRONTIER_PLANNING_CONTRACT - || candidate.selected_fields_invalid() - { - return Err(anyhow!("invalid repo frontier plan candidate")); - } - let store_path = store_path.as_ref(); - let mut cache = runtime_spine_cache(store_path)?; - cache.pull_all_backing_stores()?; - let request = cache - .get::(&candidate.planning_request_id)? - .ok_or_else(|| anyhow!("candidate requires persisted planning request"))?; - if request.schema_version != REPO_FRONTIER_PLANNING_REQUEST_SCHEMA_VERSION - || request.contract != REPO_FRONTIER_PLANNING_CONTRACT - || request.selected_organ != "Imagination" - { - return Err(anyhow!("candidate planning request contract is invalid")); - } - if 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!("candidate substituted planning identity")); - } - let admission = cache - .get::(&request.admission_receipt_id)? - .ok_or_else(|| anyhow!("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!("candidate admission binding mismatch")); - } - let backing = SingleFileMessagePackBackingStore::new(store_path); - let model_envelope = backing - .pull_all()? - .into_iter() - .find(|e| e.r#type == crate::MEMORY_GRAPH_TYPE && e.key == crate::MEMORY_GRAPH_KEY) - .ok_or_else(|| anyhow!("candidate requires canonical model"))?; - let entry: crate::EpiphanyMemoryGraphEntry = rmp_serde::from_slice(&model_envelope.payload)?; - crate::validate_memory_graph_entry(&entry)?; - let model = entry.snapshot()?; - if model.model_revision != request.model_revision - || crate::memory_graph_model_hash(&model)? != request.model_hash - { - return Err(anyhow!("candidate model is stale")); - } - let item = model - .frontier - .iter() - .find(|i| i.id == request.frontier_item_id) - .ok_or_else(|| anyhow!("candidate frontier missing"))?; - if format!("{:x}", Sha256::digest(rmp_serde::to_vec_named(item)?)) != request.frontier_item_hash - || item.source_scope != request.source_scope - { - return Err(anyhow!("candidate frontier identity mismatch")); - } - if !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!("candidate paths exceed frontier source scope")); - } - put_immutable_planning_entry(store_path, &candidate.candidate_id, candidate) -} - fn validate_repo_frontier_plan_candidate_against_request( cache: &CultCache, candidate: &RepoFrontierPlanCandidate, @@ -3732,6 +3924,319 @@ pub fn canonical_repo_frontier_plan_candidate_id( )) } +pub fn commit_repo_frontier_plan_decision( + runtime_store: impl AsRef, + mind_result_id: &str, +) -> Result { + commit_repo_frontier_plan_decision_inner(runtime_store, mind_result_id, None) +} + +fn commit_repo_frontier_plan_decision_inner( + runtime_store: impl AsRef, + mind_result_id: &str, + pre_cas: Option<&(dyn Fn() + Sync)>, +) -> Result { + validate_non_empty(mind_result_id, "frontier plan Mind result id")?; + let runtime_store = runtime_store.as_ref(); + let mut cache = runtime_spine_cache(runtime_store)?; + cache.pull_all_backing_stores()?; + let results = cache + .get_all::()? + .into_iter() + .filter(|result| result.result_id == mind_result_id) + .collect::>(); + if results.len() != 1 { + return Err(anyhow!( + "frontier plan decision requires exactly one immutable Mind result" + )); + } + let mind_result = &results[0]; + let mind_request_id = mind_result + .frontier_plan_mind_request_id + .as_deref() + .ok_or_else(|| anyhow!("frontier plan decision Mind result lacks request echo"))?; + let mind_request = cache + .get::(mind_request_id)? + .ok_or_else(|| anyhow!("frontier plan decision requires typed Mind request"))?; + let (request, candidate) = + validate_repo_frontier_plan_mind_request_identity(&cache, &mind_request)?; + let typed_decision = mind_result + .frontier_plan_mind_decision()? + .ok_or_else(|| anyhow!("frontier plan decision requires typed immutable Mind decision"))?; + let decision = typed_decision.decision; + let rationale = typed_decision.rationale.as_str(); + let decided_at = typed_decision.decided_at.as_str(); + let result_id = mind_request.imagination_result_id.as_str(); + let source_results = cache + .get_all::()? + .into_iter() + .filter(|r| r.result_id == result_id) + .collect::>(); + if source_results.len() != 1 { + return Err(anyhow!("Mind decision lost its exact Imagination source")); + } + let result = &source_results[0]; + let request_id = result + .frontier_planning_request_id + .as_deref() + .ok_or_else(|| anyhow!("frontier plan decision result lacks its planning request echo"))?; + if request.request_id != request_id { + return Err(anyhow!("Mind request planning echo mismatch")); + } + let candidate_bytes = rmp_serde::to_vec_named(&candidate)?; + let candidate_sha256 = format!("{:x}", Sha256::digest(&candidate_bytes)); + let decision_id = format!( + "repo-frontier-plan-decision-{:x}", + Sha256::digest(request.request_id.as_bytes()) + ); + let existing_for_request = cache + .get_all::()? + .into_iter() + .filter(|receipt| receipt.planning_request_id == request.request_id) + .collect::>(); + if !existing_for_request.is_empty() { + if existing_for_request.len() != 1 { + return Err(anyhow!( + "frontier planning request has multiple terminal Mind decisions" + )); + } + let existing = &existing_for_request[0]; + let expected_admission_id = if decision == RepoFrontierPlanDecision::Adopt { + format!("repo-model-admission-{decision_id}") + } else { + String::new() + }; + if existing.schema_version != REPO_FRONTIER_PLAN_DECISION_RECEIPT_SCHEMA_VERSION + || existing.contract != REPO_FRONTIER_PLAN_DECISION_CONTRACT + || existing.decision_id != decision_id + || existing.source_result_id != mind_result.result_id + || existing.source_job_id != mind_result.job_id + || existing.candidate_id != candidate.candidate_id + || existing.candidate_sha256 != candidate_sha256 + || existing.model_revision != request.model_revision + || existing.model_hash != request.model_hash + || existing.frontier_item_id != request.frontier_item_id + || existing.frontier_item_hash != request.frontier_item_hash + || existing.decision != decision + || existing.rationale != rationale + || existing.decided_at != decided_at + || existing.model_admission_receipt_id != expected_admission_id + { + return Err(anyhow!("frontier plan decision identity collision")); + } + if decision == RepoFrontierPlanDecision::Adopt { + let admission = cache + .get::(&expected_admission_id)? + .ok_or_else(|| anyhow!("Adopt retry lost its model admission receipt"))?; + let expected_review_id = format!("repo-model-review-{decision_id}"); + let expected_patch_id = + format!("repo-frontier-plan-adopt-patch-{}", request.request_id); + let review = cache + .get::(&expected_review_id)? + .ok_or_else(|| anyhow!("Adopt retry lost its Mind admission review"))?; + if admission.schema_version != REPO_MODEL_ADMISSION_RECEIPT_SCHEMA_VERSION + || admission.contract != REPO_MODEL_ADMISSION_CONTRACT + || admission.receipt_id != expected_admission_id + || admission.review_id != expected_review_id + || admission.result_id != mind_result.result_id + || admission.patch_id != expected_patch_id + || admission.patch_sha256.trim().is_empty() + || admission.previous_revision != request.model_revision + || admission.previous_hash != request.model_hash + || admission.admitted_revision != request.model_revision + 1 + || admission.admitted_hash.trim().is_empty() + || admission.admitted_at != decided_at + || admission.purpose + != (crate::RepoModelPatchPurpose::AdoptFrontierPlan { + planning_request_id: request.request_id.clone(), + result_id: result.result_id.clone(), + candidate_id: candidate.candidate_id.clone(), + }) + || admission.frontier_route_id != "" + || admission.verification_request_id != "" + || admission.soul_verdict_receipt_id != "" + || admission.frontier_modeling_request_id != "" + || admission.proposal_modeling_request_id != "" + || admission.claim_repair_request_id != "" + || admission.frontier_plan_decision_id != decision_id + || review.schema_version != REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION + || review.contract != REPO_MODEL_ADMISSION_CONTRACT + || review.review_id != expected_review_id + || review.result_id != mind_result.result_id + || review.job_id != mind_result.job_id + || review.patch_id != expected_patch_id + || review.patch_sha256 != admission.patch_sha256 + || review.base_revision != request.model_revision + || review.base_hash != request.model_hash + || review.decision != MindGatewayDecision::Accept + || review.evidence_ids + != vec![ + request.request_id.clone(), + result.result_id.clone(), + mind_result.result_id.clone(), + ] + || review.reviewed_at != decided_at + { + return Err(anyhow!("Adopt retry model admission chain mismatch")); + } + } + return Ok(existing.clone()); + } + + // Exact retry through the immutable result validator also replays request, + // launch binding, launch bytes, runtime/thread, model, admission, frontier, + // and candidate scope immediately before Mind decides. + put_runtime_role_worker_result(runtime_store, mind_result)?; + put_runtime_role_worker_result(runtime_store, result)?; + validate_current_repo_frontier_planning_request(&cache, &request)?; + validate_repo_frontier_plan_candidate_against_request(&cache, &candidate, &request)?; + let mut receipt = RepoFrontierPlanDecisionReceipt { + schema_version: REPO_FRONTIER_PLAN_DECISION_RECEIPT_SCHEMA_VERSION.into(), + decision_id: decision_id.clone(), + planning_request_id: request.request_id.clone(), + source_result_id: mind_result.result_id.clone(), + source_job_id: mind_result.job_id.clone(), + candidate_id: candidate.candidate_id.clone(), + candidate_sha256: candidate_sha256.clone(), + model_revision: request.model_revision, + model_hash: request.model_hash.clone(), + frontier_item_id: request.frontier_item_id.clone(), + frontier_item_hash: request.frontier_item_hash.clone(), + decision, + rationale: rationale.to_string(), + decided_at: decided_at.to_string(), + model_admission_receipt_id: String::new(), + contract: REPO_FRONTIER_PLAN_DECISION_CONTRACT.into(), + }; + let backing = SingleFileMessagePackBackingStore::new(runtime_store); + let current_envelope = backing + .pull_all()? + .into_iter() + .find(|entry| { + entry.r#type == crate::MEMORY_GRAPH_TYPE && entry.key == crate::MEMORY_GRAPH_KEY + }) + .ok_or_else(|| anyhow!("frontier plan decision requires canonical RepoModel"))?; + let current_entry: crate::EpiphanyMemoryGraphEntry = + rmp_serde::from_slice(¤t_envelope.payload)?; + crate::validate_memory_graph_entry(¤t_entry)?; + let current = current_entry.snapshot()?; + if current.model_revision != request.model_revision + || crate::memory_graph_model_hash(¤t)? != request.model_hash + { + return Err(anyhow!( + "frontier plan decision model changed before commit" + )); + } + let mut companions = Vec::new(); + let replacement = if decision == RepoFrontierPlanDecision::Adopt { + let adopted_plan = crate::RepoFrontierAdoptedPlan { + planning_request_id: request.request_id.clone(), + result_id: result.result_id.clone(), + job_id: result.job_id.clone(), + candidate_id: candidate.candidate_id.clone(), + candidate_sha256: candidate_sha256.clone(), + safe_paths: candidate.safe_paths.clone(), + action: candidate.action.clone(), + command: candidate.command.clone(), + checks: candidate.checks.clone(), + stop_conditions: candidate.stop_conditions.clone(), + rollback_steps: candidate.rollback_steps.clone(), + commit_message: candidate.commit_message.clone(), + }; + let patch = crate::RepoModelPatch { + patch_id: format!("repo-frontier-plan-adopt-patch-{}", request.request_id), + base_revision: request.model_revision, + base_hash: request.model_hash.clone(), + applied_at: decided_at.to_string(), + purpose: crate::RepoModelPatchPurpose::AdoptFrontierPlan { + planning_request_id: request.request_id.clone(), + result_id: result.result_id.clone(), + candidate_id: candidate.candidate_id.clone(), + }, + operations: vec![crate::RepoModelPatchOperation::AdoptFrontierPlan { + frontier_item_id: request.frontier_item_id.clone(), + expected_frontier_item_hash: request.frontier_item_hash.clone(), + adopted_plan, + }], + }; + let patch_bytes = rmp_serde::to_vec_named(&patch)?; + let patch_sha256 = format!("{:x}", Sha256::digest(&patch_bytes)); + let next = crate::derive_repo_model_patch(¤t, &patch)?; + let next_entry = crate::EpiphanyMemoryGraphEntry::from_snapshot(&next)?; + let review_id = format!("repo-model-review-{decision_id}"); + let admission_id = format!("repo-model-admission-{decision_id}"); + let review = RepoModelAdmissionReview { + schema_version: REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION.into(), + review_id: review_id.clone(), + result_id: mind_result.result_id.clone(), + job_id: mind_result.job_id.clone(), + patch_id: patch.patch_id.clone(), + patch_sha256: patch_sha256.clone(), + base_revision: request.model_revision, + base_hash: request.model_hash.clone(), + decision: MindGatewayDecision::Accept, + evidence_ids: vec![ + request.request_id.clone(), + result.result_id.clone(), + mind_result.result_id.clone(), + ], + reviewed_at: decided_at.to_string(), + contract: REPO_MODEL_ADMISSION_CONTRACT.into(), + }; + let admission = RepoModelAdmissionReceipt { + schema_version: REPO_MODEL_ADMISSION_RECEIPT_SCHEMA_VERSION.into(), + receipt_id: admission_id.clone(), + review_id, + result_id: mind_result.result_id.clone(), + patch_id: patch.patch_id, + patch_sha256, + previous_revision: request.model_revision, + previous_hash: request.model_hash.clone(), + admitted_revision: next.model_revision, + admitted_hash: next.model_hash.clone(), + admitted_at: decided_at.to_string(), + contract: REPO_MODEL_ADMISSION_CONTRACT.into(), + purpose: patch.purpose, + frontier_route_id: String::new(), + verification_request_id: String::new(), + soul_verdict_receipt_id: String::new(), + frontier_modeling_request_id: String::new(), + proposal_modeling_request_id: String::new(), + claim_repair_request_id: String::new(), + frontier_plan_decision_id: decision_id.clone(), + }; + receipt.model_admission_receipt_id = admission_id; + companions.push(cache.prepare_entry(&review.review_id, &review)?.0); + companions.push(cache.prepare_entry(&admission.receipt_id, &admission)?.0); + cache.prepare_entry(crate::MEMORY_GRAPH_KEY, &next_entry)?.0 + } else { + current_envelope.clone() + }; + companions.push(cache.prepare_entry(&receipt.decision_id, &receipt)?.0); + let mut writes = vec![replacement]; + writes.extend(companions); + if let Some(pre_cas) = pre_cas { + pre_cas(); + } + if !backing.compare_and_swap_batch(&[current_envelope], writes)? { + // A concurrent exact retry may have installed the same immutable + // decision and admission chain. Reload through the public validator: + // it returns that winner only when every identity field matches, while + // a competing decision or unrelated model change remains an error. + return commit_repo_frontier_plan_decision(runtime_store, mind_result_id); + } + Ok(receipt) +} + +#[cfg(test)] +pub(crate) fn commit_repo_frontier_plan_decision_with_pre_cas( + runtime_store: impl AsRef, + mind_result_id: &str, + pre_cas: &(dyn Fn() + Sync), +) -> Result { + commit_repo_frontier_plan_decision_inner(runtime_store, mind_result_id, Some(pre_cas)) +} + impl RepoFrontierPlanCandidate { fn selected_fields_invalid(&self) -> bool { self.candidate_id.trim().is_empty() @@ -3750,94 +4255,6 @@ impl RepoFrontierPlanCandidate { } } -pub(crate) fn put_repo_frontier_plan_adoption( - store_path: impl AsRef, - adoption: &RepoFrontierPlanAdoption, -) -> Result<()> { - if adoption.schema_version != REPO_FRONTIER_PLAN_ADOPTION_SCHEMA_VERSION - || adoption.contract != REPO_FRONTIER_PLANNING_CONTRACT - || adoption.adoption_id.trim().is_empty() - || adoption.rationale.trim().is_empty() - || chrono::DateTime::parse_from_rfc3339(&adoption.decided_at).is_err() - { - return Err(anyhow!("invalid repo frontier plan adoption")); - } - let store_path = store_path.as_ref(); - let mut cache = runtime_spine_cache(store_path)?; - cache.pull_all_backing_stores()?; - let candidate = cache - .get::(&adoption.candidate_id)? - .ok_or_else(|| anyhow!("adoption requires persisted candidate"))?; - let bytes = rmp_serde::to_vec_named(&candidate)?; - if adoption.candidate_sha256 != format!("{:x}", Sha256::digest(bytes)) - || adoption.planning_request_id != candidate.planning_request_id - || adoption.model_revision != candidate.model_revision - || adoption.model_hash != candidate.model_hash - || adoption.frontier_item_id != candidate.frontier_item_id - || adoption.frontier_item_hash != candidate.frontier_item_hash - { - return Err(anyhow!("adoption substituted candidate identity")); - } - let backing = SingleFileMessagePackBackingStore::new(store_path); - let model_envelope = backing - .pull_all()? - .into_iter() - .find(|e| e.r#type == crate::MEMORY_GRAPH_TYPE && e.key == crate::MEMORY_GRAPH_KEY) - .ok_or_else(|| anyhow!("adoption requires current model"))?; - let entry: crate::EpiphanyMemoryGraphEntry = rmp_serde::from_slice(&model_envelope.payload)?; - crate::validate_memory_graph_entry(&entry)?; - let model = entry.snapshot()?; - if model.model_revision != adoption.model_revision - || crate::memory_graph_model_hash(&model)? != adoption.model_hash - { - return Err(anyhow!("adoption model is stale")); - } - let item = model - .frontier - .iter() - .find(|i| i.id == adoption.frontier_item_id) - .ok_or_else(|| anyhow!("adoption frontier missing"))?; - if format!("{:x}", Sha256::digest(rmp_serde::to_vec_named(item)?)) - != adoption.frontier_item_hash - { - return Err(anyhow!("adoption frontier identity is stale")); - } - let claim_id = format!( - "repo-frontier-plan-adopt-{:x}", - Sha256::digest(format!("{}:{}", adoption.model_hash, adoption.frontier_item_id).as_bytes()) - ); - if adoption.decision == RepoFrontierPlanDecision::Adopt { - if adoption.adoption_id != claim_id { - return Err(anyhow!("Adopt must use deterministic frontier claim id")); - } - } else if adoption - .adoption_id - .starts_with("repo-frontier-plan-adopt-") - { - return Err(anyhow!( - "Hold/Refuse cannot occupy the frontier Adopt claim" - )); - } - if let Some(existing) = cache.get::(&adoption.adoption_id)? { - return if existing == *adoption { - Ok(()) - } else { - Err(anyhow!("plan adoption ids are immutable")) - }; - } - let (envelope, _) = cache.prepare_entry(&adoption.adoption_id, adoption)?; - if !backing.compare_and_swap_batch(&[model_envelope.clone()], vec![model_envelope, envelope])? { - let mut reloaded = runtime_spine_cache(store_path)?; - reloaded.pull_all_backing_stores()?; - return match reloaded.get::(&adoption.adoption_id)? { - Some(existing) if existing == *adoption => Ok(()), - Some(_) => Err(anyhow!("frontier Adopt claim already owned")), - None => Err(anyhow!("plan adoption lost current-model CAS")), - }; - } - Ok(()) -} - pub fn select_and_commit_repo_frontier_route( runtime_store: impl AsRef, at: &str, @@ -3904,7 +4321,12 @@ pub fn select_and_commit_repo_frontier_route( question: item.question.clone(), gap: item.gap.clone(), target_claim_ids: item.target_claim_ids.clone(), - source_scope: item.source_scope.clone(), + source_scope: item + .adopted_plan + .as_ref() + .map(|plan| plan.safe_paths.clone()) + .unwrap_or_else(|| item.source_scope.clone()), + adopted_plan: item.adopted_plan.clone(), selected_at: at.to_string(), contract: REPO_FRONTIER_ROUTE_CONTRACT.to_string(), }; @@ -4282,6 +4704,11 @@ fn validate_repo_frontier_hands_authority_chain( .ok_or_else(|| anyhow!("Hands authority requires the current model"))?; crate::validate_memory_graph_entry(¤t_entry)?; let current = current_entry.snapshot()?; + let current_item = current + .frontier + .iter() + .find(|item| item.id == route.frontier_item_id) + .ok_or_else(|| anyhow!("Hands authority lost its model frontier"))?; let intent = cache .get::(&authority.hands_intent_id)? .ok_or_else(|| anyhow!("Hands authority requires its persisted intent"))?; @@ -4305,6 +4732,18 @@ fn validate_repo_frontier_hands_authority_chain( )); } }; + let adopted_plan_binding_is_exact = match route.adopted_plan.as_ref() { + Some(plan) => { + intent.frontier_route_id == route.route_id + && intent.plan_candidate_sha256 == plan.candidate_sha256 + && intent.plan_action == plan.action + } + None => { + intent.frontier_route_id.is_empty() + && intent.plan_candidate_sha256.is_empty() + && intent.plan_action.is_empty() + } + }; if route.schema_version != REPO_FRONTIER_ROUTE_SCHEMA_VERSION || route.contract != REPO_FRONTIER_ROUTE_CONTRACT || intent.schema_version != HANDS_ACTION_INTENT_SCHEMA_VERSION @@ -4323,6 +4762,12 @@ fn validate_repo_frontier_hands_authority_chain( || authority.frontier_item_hash != route.frontier_item_hash || current.model_revision != route.model_revision || crate::memory_graph_model_hash(¤t)? != route.model_hash + || current_item.adopted_plan != route.adopted_plan + || route + .adopted_plan + .as_ref() + .is_some_and(|plan| route.source_scope != plan.safe_paths) + || !adopted_plan_binding_is_exact || review.intent_id != intent.intent_id || review.decision != "approved" || !requested_operations.iter().all(|required| { @@ -4503,6 +4948,12 @@ pub fn put_repo_frontier_verification_request( let commit = cache .get::(&request.hands_commit_receipt_id)? .ok_or_else(|| anyhow!("verification request requires its exact commit receipt"))?; + let adopted_plan_consequence_is_exact = route.adopted_plan.as_ref().is_none_or(|plan| { + intent.frontier_route_id == route.route_id + && intent.plan_candidate_sha256 == plan.candidate_sha256 + && intent.plan_action == plan.action + && command.command == plan.command + }); if request.model_revision != route.model_revision || request.model_hash != route.model_hash || request.frontier_item_id != route.frontier_item_id @@ -4529,6 +4980,7 @@ pub fn put_repo_frontier_verification_request( || commit.runtime_job_id != intent.runtime_job_id || patch.changed_paths != commit.changed_paths || patch.changed_paths != authority.requested_paths + || !adopted_plan_consequence_is_exact { return Err(anyhow!( "verification request does not exactly bind route, model, Hands authority, and complete receipts" @@ -7174,6 +7626,8 @@ pub(crate) mod tests { claim_repair_request_id: None, frontier_planning_request_id: None, frontier_plan_candidate_msgpack: None, + frontier_plan_mind_request_id: None, + frontier_plan_mind_decision_msgpack: None, }; let review = RepoModelAdmissionReview { schema_version: REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION.to_string(), @@ -7305,6 +7759,8 @@ pub(crate) mod tests { claim_repair_request_id: Some(repair.request_id.clone()), frontier_planning_request_id: None, frontier_plan_candidate_msgpack: None, + frontier_plan_mind_request_id: None, + frontier_plan_mind_decision_msgpack: None, }; let review = RepoModelAdmissionReview { schema_version: REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION.into(), @@ -7420,6 +7876,7 @@ pub(crate) mod tests { frontier_modeling_request_id: String::new(), proposal_modeling_request_id: String::new(), claim_repair_request_id: repair.request_id.clone(), + frontier_plan_decision_id: String::new(), }, )?; } @@ -7884,6 +8341,7 @@ pub(crate) mod tests { frontier_modeling_request_id: String::new(), proposal_modeling_request_id: "wrong-proposal-request".into(), claim_repair_request_id: String::new(), + frontier_plan_decision_id: String::new(), }; overwrite_test_entry(&store, &review.review_id, &review)?; overwrite_test_entry(&store, &receipt_id, &receipt)?; @@ -7901,207 +8359,6 @@ pub(crate) mod tests { Ok(()) } - #[test] - fn typed_frontier_planning_chain_is_exact_immutable_and_non_routing() -> Result<()> { - let temp = tempdir()?; - let store = temp.path().join("planning.ccmp"); - initialize_runtime_spine( - &store, - RuntimeSpineInitOptions { - runtime_id: "planning-test".into(), - display_name: "Planning Test".into(), - created_at: "2026-07-13T03:00:00Z".into(), - }, - )?; - let mut state_cache = runtime_spine_cache(&store)?; - state_cache.put( - crate::THREAD_STATE_KEY, - &crate::EpiphanyThreadStateEntry::from_state( - "thread-1", - &epiphany_state_model::EpiphanyThreadState::default(), - )?, - )?; - let proposal_content = rmp_serde::to_vec_named(&( - "Plan typed frontier work", - "Build the inert planning foundation.", - "Produce typed planning documents.", - vec!["No execution route"], - vec!["epiphany-core/src"], - vec!["evidence-user-request"], - ))?; - let proposal = RepoFrontierWorkProposal { - schema_version: REPO_FRONTIER_WORK_PROPOSAL_SCHEMA_VERSION.into(), - proposal_id: "proposal-1".into(), - source_kind: crate::RepoFrontierProposalSourceKind::User, - source_actor: "operator".into(), - source_ref: "thread-message-1".into(), - repository: "EpiphanyAgent".into(), - workspace: "E:/Projects/EpiphanyAgent".into(), - thread_id: "thread-1".into(), - runtime_id: "planning-test".into(), - payload_sha256: format!("{:x}", Sha256::digest(proposal_content)), - title: "Plan typed frontier work".into(), - body: "Build the inert planning foundation.".into(), - desired_outcome: "Produce typed planning documents.".into(), - constraints: vec!["No execution route".into()], - scope_hints: vec!["epiphany-core/src".into()], - evidence_refs: vec!["evidence-user-request".into()], - private_state_included: false, - proposed_at: "2026-07-13T03:01:00Z".into(), - contract: REPO_FRONTIER_WORK_PROPOSAL_CONTRACT.into(), - }; - put_repo_frontier_work_proposal(&store, &proposal)?; - put_repo_frontier_work_proposal(&store, &proposal)?; - let mut corrupt_proposal = proposal.clone(); - corrupt_proposal.proposal_id = "proposal-corrupt".into(); - corrupt_proposal.body = "substituted".into(); - assert!(put_repo_frontier_work_proposal(&store, &corrupt_proposal).is_err()); - let bootstrap = repo_model_bootstrap(); - let legacy = temp.path().join("legacy.msgpack"); - let (current, _) = - ensure_runtime_repo_model(&store, &legacy, &bootstrap, "2026-07-13T04:00:00Z")?; - let (mut result, mut review) = repo_model_result_and_review( - &store, - "planning-result", - "planning-job", - ¤t, - "planning-review", - )?; - let mut patch: crate::RepoModelPatch = - rmp_serde::from_slice(result.repo_model_patch_msgpack.as_deref().unwrap())?; - let crate::RepoModelPatchOperation::UpsertFrontier { item } = &mut patch.operations[0] - else { - unreachable!() - }; - item.recommended_next_organ = "Imagination".into(); - let bytes = rmp_serde::to_vec_named(&patch)?; - result.repo_model_patch_msgpack = Some(bytes.clone()); - review.patch_sha256 = format!("{:x}", Sha256::digest(&bytes)); - put_runtime_role_worker_result(&store, &result)?; - commit_repo_model_admission(&store, &result.result_id, &review)?; - - let request_store_one = store.clone(); - let request_store_two = store.clone(); - let left = std::thread::spawn(move || { - select_and_commit_repo_frontier_planning_request( - request_store_one, - "2026-07-13T05:00:00Z", - ) - }); - let right = std::thread::spawn(move || { - select_and_commit_repo_frontier_planning_request( - request_store_two, - "2026-07-13T05:01:00Z", - ) - }); - let request = left.join().unwrap()?; - let concurrent = right.join().unwrap()?; - assert_eq!(request.selected_organ, "Imagination"); - assert_eq!(concurrent, request); - assert!( - runtime_spine_cache(&store)? - .get_all::()? - .is_empty() - ); - - let candidate = RepoFrontierPlanCandidate { - schema_version: REPO_FRONTIER_PLAN_CANDIDATE_SCHEMA_VERSION.into(), - candidate_id: "candidate-1".into(), - planning_request_id: request.request_id.clone(), - model_revision: request.model_revision, - model_hash: request.model_hash.clone(), - frontier_item_id: request.frontier_item_id.clone(), - frontier_item_hash: request.frontier_item_hash.clone(), - safe_paths: vec!["epiphany-core/src/runtime_spine.rs".into()], - action: "Add typed planning state".into(), - command: "cargo test".into(), - checks: vec!["focused tests pass".into()], - stop_conditions: vec!["model identity changes".into()], - rollback_steps: vec!["remove candidate".into()], - commit_message: "Add typed planning foundation".into(), - proposed_at: "2026-07-13T05:02:00Z".into(), - contract: REPO_FRONTIER_PLANNING_CONTRACT.into(), - }; - put_repo_frontier_plan_candidate(&store, &candidate)?; - put_repo_frontier_plan_candidate(&store, &candidate)?; - let mut swapped = candidate.clone(); - swapped.candidate_id = "candidate-swapped".into(); - swapped.frontier_item_hash = "0".repeat(64); - assert!(put_repo_frontier_plan_candidate(&store, &swapped).is_err()); - let mut unsafe_paths = candidate.clone(); - unsafe_paths.candidate_id = "candidate-unsafe".into(); - unsafe_paths.safe_paths = vec!["z".into(), "a".into()]; - assert!(put_repo_frontier_plan_candidate(&store, &unsafe_paths).is_err()); - let mut escaped = candidate.clone(); - escaped.candidate_id = "candidate-escaped".into(); - escaped.safe_paths = vec!["epiphany-core/src/lib.rs".into()]; - assert!(put_repo_frontier_plan_candidate(&store, &escaped).is_err()); - let mut blank_check = candidate.clone(); - blank_check.candidate_id = "candidate-blank".into(); - blank_check.checks = vec![" ".into()]; - assert!(put_repo_frontier_plan_candidate(&store, &blank_check).is_err()); - - let mut candidate_two = candidate.clone(); - candidate_two.candidate_id = "candidate-2".into(); - candidate_two.action = "Alternative exact plan".into(); - put_repo_frontier_plan_candidate(&store, &candidate_two)?; - - let adoption = RepoFrontierPlanAdoption { - schema_version: REPO_FRONTIER_PLAN_ADOPTION_SCHEMA_VERSION.into(), - adoption_id: format!( - "repo-frontier-plan-adopt-{:x}", - Sha256::digest( - format!("{}:{}", request.model_hash, request.frontier_item_id).as_bytes() - ) - ), - candidate_id: candidate.candidate_id.clone(), - candidate_sha256: format!("{:x}", Sha256::digest(rmp_serde::to_vec_named(&candidate)?)), - planning_request_id: request.request_id.clone(), - model_revision: request.model_revision, - model_hash: request.model_hash.clone(), - frontier_item_id: request.frontier_item_id.clone(), - frontier_item_hash: request.frontier_item_hash.clone(), - decision: RepoFrontierPlanDecision::Adopt, - rationale: "Exact bounded plan".into(), - decided_at: "2026-07-13T05:03:00Z".into(), - contract: REPO_FRONTIER_PLANNING_CONTRACT.into(), - }; - let mut adoption_two = adoption.clone(); - adoption_two.candidate_id = candidate_two.candidate_id.clone(); - adoption_two.candidate_sha256 = format!( - "{:x}", - Sha256::digest(rmp_serde::to_vec_named(&candidate_two)?) - ); - adoption_two.rationale = "Competing exact plan".into(); - let mut claim_squatter = adoption.clone(); - claim_squatter.decision = RepoFrontierPlanDecision::Hold; - claim_squatter.rationale = "Attempt to squat the reserved claim".into(); - assert!(put_repo_frontier_plan_adoption(&store, &claim_squatter).is_err()); - let store_one = store.clone(); - let store_two = store.clone(); - let first = adoption.clone(); - let second = adoption_two.clone(); - let left = std::thread::spawn(move || put_repo_frontier_plan_adoption(store_one, &first)); - let right = std::thread::spawn(move || put_repo_frontier_plan_adoption(store_two, &second)); - let outcomes = [left.join().unwrap(), right.join().unwrap()]; - assert_eq!(outcomes.iter().filter(|result| result.is_ok()).count(), 1); - let mut loaded = runtime_spine_cache(&store)?; - loaded.pull_all_backing_stores()?; - let winner = loaded - .get::(&adoption.adoption_id)? - .unwrap(); - put_repo_frontier_plan_adoption(&store, &winner)?; - let mut counterfeit = adoption.clone(); - counterfeit.adoption_id = "adoption-counterfeit".into(); - counterfeit.candidate_sha256 = "f".repeat(64); - assert!(put_repo_frontier_plan_adoption(&store, &counterfeit).is_err()); - assert!( - select_and_commit_repo_frontier_planning_request(&store, "2026-07-13T06:00:00Z") - .is_err() - ); - Ok(()) - } - #[test] fn imagination_frontier_selection_is_deterministic_and_dependency_aware() { let mut model = repo_model_bootstrap(); @@ -8496,6 +8753,9 @@ pub(crate) mod tests { substrate_gate_grant_receipt_id: grant.receipt_id.clone(), requested_at: "2026-07-13T06:00:02Z".to_string(), contract: "test Hands intent".to_string(), + frontier_route_id: String::new(), + plan_candidate_sha256: String::new(), + plan_action: String::new(), }; put_hands_action_intent(&store, &intent)?; let review = crate::hands_action_review_for_intent( @@ -8580,6 +8840,8 @@ pub(crate) mod tests { claim_repair_request_id: None, frontier_planning_request_id: None, frontier_plan_candidate_msgpack: None, + frontier_plan_mind_request_id: None, + frontier_plan_mind_decision_msgpack: None, }; put_runtime_role_worker_result(&store, &verification_result)?; let verdict = SoulVerdictReceipt { @@ -8701,6 +8963,8 @@ pub(crate) mod tests { claim_repair_request_id: None, frontier_planning_request_id: None, frontier_plan_candidate_msgpack: None, + frontier_plan_mind_request_id: None, + frontier_plan_mind_decision_msgpack: None, }; let review = RepoModelAdmissionReview { schema_version: REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION.to_string(), @@ -8979,6 +9243,8 @@ pub(crate) mod tests { claim_repair_request_id: None, frontier_planning_request_id: None, frontier_plan_candidate_msgpack: None, + frontier_plan_mind_request_id: None, + frontier_plan_mind_decision_msgpack: None, }; put_runtime_role_worker_result(&fixture.store, &adjacent)?; put_soul_verdict_receipt( @@ -9036,6 +9302,9 @@ pub(crate) mod tests { } => vec![soul_verdict_receipt_id.clone()], crate::RepoModelPatchPurpose::Evolution => vec!["evolution".to_string()], crate::RepoModelPatchPurpose::RepairClaim => vec!["repair".to_string()], + crate::RepoModelPatchPurpose::AdoptFrontierPlan { .. } => { + vec!["adopt".to_string()] + } }; review.patch_id = patch.patch_id.clone(); review.patch_sha256 = format!("{:x}", Sha256::digest(&bytes)); @@ -9345,6 +9614,9 @@ pub(crate) mod tests { substrate_gate_grant_receipt_id: winner.receipt_id.clone(), requested_at: "2026-07-13T05:30:01Z".to_string(), contract: "race intent".to_string(), + frontier_route_id: String::new(), + plan_candidate_sha256: String::new(), + plan_action: String::new(), }; let mut other_intent = base_intent.clone(); other_intent.requested_action = "continueImplementation".to_string(); @@ -9438,6 +9710,9 @@ pub(crate) mod tests { substrate_gate_grant_receipt_id: grant.receipt_id.clone(), requested_at: "2026-07-13T06:00:01Z".to_string(), contract: "test".to_string(), + frontier_route_id: String::new(), + plan_candidate_sha256: String::new(), + plan_action: String::new(), }; put_hands_action_intent(&store, &intent)?; let review = hands_action_review_for_intent( @@ -9801,6 +10076,9 @@ pub(crate) mod tests { substrate_gate_grant_receipt_id: "substrate-grant-hands-1".to_string(), requested_at: "2026-05-06T00:06:30Z".to_string(), contract: "Hands action intent persists as runtime-spine proof.".to_string(), + frontier_route_id: String::new(), + plan_candidate_sha256: String::new(), + plan_action: String::new(), }; put_hands_action_intent(&store, &hands_intent)?; let hands_review = crate::hands_action_review_for_intent( @@ -9986,6 +10264,9 @@ pub(crate) mod tests { substrate_gate_grant_receipt_id: "missing-grant".to_string(), requested_at: "2026-07-12T00:00:01Z".to_string(), contract: "Negative grant resolution proof.".to_string(), + frontier_route_id: String::new(), + plan_candidate_sha256: String::new(), + plan_action: String::new(), }; assert!(put_hands_action_intent(&store, &intent).is_err()); @@ -10046,6 +10327,9 @@ pub(crate) mod tests { substrate_gate_grant_receipt_id: "substrate-grant-reused".to_string(), requested_at: "2026-06-13T00:00:01Z".to_string(), contract: "Test reused Hands gate.".to_string(), + frontier_route_id: String::new(), + plan_candidate_sha256: String::new(), + plan_action: String::new(), }; put_substrate_gate_repo_access_grant_receipt( &store, @@ -10189,6 +10473,7 @@ pub(crate) mod tests { proposal_modeling_context: None, claim_repair_context: None, frontier_planning_context: None, + frontier_plan_mind_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: vec!["node-model".to_string()], @@ -10213,6 +10498,7 @@ pub(crate) mod tests { proposal_modeling_request_id: None, claim_repair_request_id: None, frontier_planning_request_id: None, + frontier_plan_mind_request_id: None, created_at: "2026-05-06T00:02:00Z".to_string(), }, )?; @@ -10284,6 +10570,7 @@ pub(crate) mod tests { proposal_modeling_context: None, claim_repair_context: None, frontier_planning_context: None, + frontier_plan_mind_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: vec!["runtime-spine".to_string()], @@ -10371,6 +10658,7 @@ pub(crate) mod tests { proposal_modeling_context: None, claim_repair_context: None, frontier_planning_context: None, + frontier_plan_mind_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: Vec::new(), diff --git a/epiphany-core/src/substrate_gate.rs b/epiphany-core/src/substrate_gate.rs index c6dbecb8..c41b54b7 100644 --- a/epiphany-core/src/substrate_gate.rs +++ b/epiphany-core/src/substrate_gate.rs @@ -252,6 +252,7 @@ mod tests { proposal_modeling_context: None, claim_repair_context: None, frontier_planning_context: None, + frontier_plan_mind_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: Vec::new(), @@ -277,6 +278,7 @@ mod tests { proposal_modeling_request_id: None, claim_repair_request_id: None, frontier_planning_request_id: None, + frontier_plan_mind_request_id: None, }; let grant = substrate_gate_repo_access_grant_for_launch( "grant-1".to_string(), diff --git a/epiphany-core/src/surfaces/mod.rs b/epiphany-core/src/surfaces/mod.rs index a5b4c781..d421db7a 100644 --- a/epiphany-core/src/surfaces/mod.rs +++ b/epiphany-core/src/surfaces/mod.rs @@ -144,6 +144,8 @@ pub use worker_launch::EpiphanyReorientWorkerLaunchDocument; pub use worker_launch::EpiphanyRoleWorkerLaunchDocument; pub use worker_launch::EpiphanyWorkerLaunchDocument; pub use worker_launch::REORIENT_WORKER_OUTPUT_CONTRACT_ID; +pub use worker_launch::REPO_FRONTIER_PLAN_MIND_CONTEXT_CONTRACT; +pub use worker_launch::REPO_FRONTIER_PLAN_MIND_CONTEXT_SCHEMA_VERSION; pub use worker_launch::REPO_FRONTIER_PLANNING_CONTEXT_CONTRACT; pub use worker_launch::REPO_FRONTIER_PLANNING_CONTEXT_SCHEMA_VERSION; pub use worker_launch::REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_CONTRACT; @@ -151,6 +153,7 @@ pub use worker_launch::REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_SCHEMA_VERSION; pub use worker_launch::REPO_MODEL_CLAIM_REPAIR_CONTEXT_CONTRACT; pub use worker_launch::REPO_MODEL_CLAIM_REPAIR_CONTEXT_SCHEMA_VERSION; pub use worker_launch::ROLE_WORKER_OUTPUT_CONTRACT_ID; +pub use worker_launch::RepoFrontierPlanMindContextProjection; pub use worker_launch::RepoFrontierPlanningContextProjection; pub use worker_launch::RepoFrontierProposalModelingContextProjection; pub use worker_launch::RepoModelClaimRepairContextProjection; diff --git a/epiphany-core/src/surfaces/worker_launch.rs b/epiphany-core/src/surfaces/worker_launch.rs index 5bddb549..c7134b77 100644 --- a/epiphany-core/src/surfaces/worker_launch.rs +++ b/epiphany-core/src/surfaces/worker_launch.rs @@ -38,6 +38,10 @@ pub const REPO_FRONTIER_PLANNING_CONTEXT_SCHEMA_VERSION: &str = "epiphany.worker.repo_frontier_planning_context.v0"; pub const REPO_FRONTIER_PLANNING_CONTEXT_CONTRACT: &str = "epiphany.repo_frontier_planning_context.v0"; +pub const REPO_FRONTIER_PLAN_MIND_CONTEXT_SCHEMA_VERSION: &str = + "epiphany.worker.repo_frontier_plan_mind_context.v0"; +pub const REPO_FRONTIER_PLAN_MIND_CONTEXT_CONTRACT: &str = + "epiphany.repo_frontier_plan_mind_context.v0"; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", tag = "documentKind")] @@ -95,6 +99,8 @@ pub struct EpiphanyRoleWorkerLaunchDocument { #[serde(default, skip_serializing_if = "Option::is_none")] pub frontier_planning_context: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub frontier_plan_mind_context: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub active_subgoal_id: Option, #[serde(default)] pub active_subgoals: Vec, @@ -301,6 +307,32 @@ impl RepoFrontierPlanningContextProjection { } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RepoFrontierPlanMindContextProjection { + pub schema_version: String, + pub contract: String, + pub request: crate::RepoFrontierPlanMindRequest, + pub planning_request: crate::RepoFrontierPlanningRequest, + pub candidate: crate::RepoFrontierPlanCandidate, +} + +impl RepoFrontierPlanMindContextProjection { + pub(crate) fn new( + request: &crate::RepoFrontierPlanMindRequest, + planning_request: &crate::RepoFrontierPlanningRequest, + candidate: &crate::RepoFrontierPlanCandidate, + ) -> Self { + Self { + schema_version: REPO_FRONTIER_PLAN_MIND_CONTEXT_SCHEMA_VERSION.into(), + contract: REPO_FRONTIER_PLAN_MIND_CONTEXT_CONTRACT.into(), + request: request.clone(), + planning_request: planning_request.clone(), + candidate: candidate.clone(), + } + } +} + pub fn build_reorient_job_launch_request( input: EpiphanyReorientLaunchRequestInput<'_>, ) -> EpiphanyJobLaunchRequest { @@ -344,6 +376,7 @@ pub fn build_reorient_job_launch_request( proposal_modeling_request_id: None, claim_repair_request_id: None, frontier_planning_request_id: None, + frontier_plan_mind_request_id: None, } } diff --git a/epiphany-openai-runtime/src/bin/epiphany-openai-runtime.rs b/epiphany-openai-runtime/src/bin/epiphany-openai-runtime.rs index deaa0401..a1fdbcfa 100644 --- a/epiphany-openai-runtime/src/bin/epiphany-openai-runtime.rs +++ b/epiphany-openai-runtime/src/bin/epiphany-openai-runtime.rs @@ -974,6 +974,7 @@ mod tests { proposal_modeling_context: None, claim_repair_context: None, frontier_planning_context: None, + frontier_plan_mind_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: Vec::new(), @@ -998,6 +999,7 @@ mod tests { proposal_modeling_request_id: None, claim_repair_request_id: None, frontier_planning_request_id: None, + frontier_plan_mind_request_id: None, created_at: now(), }, )?; @@ -1060,6 +1062,7 @@ mod tests { proposal_modeling_context: None, claim_repair_context: None, frontier_planning_context: None, + frontier_plan_mind_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: Vec::new(), @@ -1084,6 +1087,7 @@ mod tests { proposal_modeling_request_id: None, claim_repair_request_id: None, frontier_planning_request_id: None, + frontier_plan_mind_request_id: None, created_at: now(), }, )?; @@ -1134,6 +1138,7 @@ mod tests { proposal_modeling_context: None, claim_repair_context: None, frontier_planning_context: None, + frontier_plan_mind_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: Vec::new(), @@ -1158,6 +1163,7 @@ mod tests { proposal_modeling_request_id: None, claim_repair_request_id: None, frontier_planning_request_id: None, + frontier_plan_mind_request_id: None, created_at: now(), }, )?; diff --git a/epiphany-openai-runtime/src/lib.rs b/epiphany-openai-runtime/src/lib.rs index 564d41a8..873deafb 100644 --- a/epiphany-openai-runtime/src/lib.rs +++ b/epiphany-openai-runtime/src/lib.rs @@ -1296,6 +1296,12 @@ fn worker_instructions( fn worker_output_schema_json(document: &EpiphanyWorkerLaunchDocument) -> Result { let schema = match document { EpiphanyWorkerLaunchDocument::Role(document) => { + if document.frontier_plan_mind_context.is_some() { + return serde_json::to_string_pretty( + &epiphany_core::epiphany_frontier_plan_mind_output_schema(), + ) + .context("failed to render worker output schema"); + } let role_id = role_result_id_for_launch_role(&document.role_id) .with_context(|| format!("unknown role launch id {:?}", document.role_id))?; if document.frontier_planning_context.is_some() { @@ -1327,6 +1333,11 @@ fn role_result_id_for_launch_role( fn worker_output_contract_text(document: &EpiphanyWorkerLaunchDocument) -> &'static str { match document { + EpiphanyWorkerLaunchDocument::Role(document) + if document.frontier_plan_mind_context.is_some() => + { + "Required Mind admission-review fields: roleId=mindAdmissionReview, verdict, summary, nextSafeMove, filesInspected, frontierPlanMindRequestId, frontierPlanMindDecision. Echo every causal identifier from the typed context exactly and choose adopt, refuse, or hold with a concrete rationale. This bounded procedure serves Mind; it is not an embodied lane and emits no patches or other organ authority." + } EpiphanyWorkerLaunchDocument::Role(document) if document.frontier_planning_context.is_some() => { @@ -1367,6 +1378,21 @@ struct RoleWorkerResultIngress { claim_repair_request_id: Option, frontier_planning_request_id: Option, frontier_plan_candidate: Option, + frontier_plan_mind_request_id: Option, + frontier_plan_mind_decision: Option, +} + +#[derive(Debug, Clone, Default, serde::Deserialize)] +#[serde(rename_all = "camelCase", default)] +struct RepoFrontierPlanMindDecisionIngress { + mind_request_id: String, + planning_request_id: String, + imagination_result_id: String, + candidate_id: String, + candidate_sha256: String, + decision: Option, + rationale: String, + decided_at: String, } #[derive(Debug, Clone, Default, serde::Deserialize)] @@ -1511,6 +1537,31 @@ fn role_worker_result_from_ingress( } else { (None, None) }; + let (frontier_plan_mind_decision_msgpack, frontier_plan_mind_decision_error) = + if let Some(ingress) = result.frontier_plan_mind_decision.as_ref() { + if let Some(decision) = ingress.decision { + encode_optional_document( + &Some(epiphany_core::RepoFrontierPlanMindDecision { + mind_request_id: ingress.mind_request_id.trim().into(), + planning_request_id: ingress.planning_request_id.trim().into(), + imagination_result_id: ingress.imagination_result_id.trim().into(), + candidate_id: ingress.candidate_id.trim().into(), + candidate_sha256: ingress.candidate_sha256.trim().into(), + decision, + rationale: ingress.rationale.trim().into(), + decided_at: ingress.decided_at.trim().into(), + }), + "frontierPlanMindDecision", + ) + } else { + ( + None, + Some("frontierPlanMindDecision: missing decision".into()), + ) + } + } else { + (None, None) + }; EpiphanyRuntimeRoleWorkerResult { schema_version: epiphany_core::RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION.to_string(), result_id: result_id.to_string(), @@ -1540,7 +1591,10 @@ fn role_worker_result_from_ingress( merge_optional_errors(state_patch_error, self_patch_error), repo_model_patch_error, ), - frontier_plan_candidate_error, + merge_optional_errors( + frontier_plan_candidate_error, + frontier_plan_mind_decision_error, + ), ), metadata: std::collections::BTreeMap::new(), repo_model_patch_msgpack, @@ -1557,6 +1611,10 @@ fn role_worker_result_from_ingress( result.frontier_planning_request_id.as_deref(), ), frontier_plan_candidate_msgpack, + frontier_plan_mind_request_id: clean_optional_string( + result.frontier_plan_mind_request_id.as_deref(), + ), + frontier_plan_mind_decision_msgpack, } } @@ -1697,6 +1755,7 @@ mod tests { proposal_modeling_request_id: None, claim_repair_request_id: None, frontier_planning_request_id: None, + frontier_plan_mind_request_id: None, }; let result = role_worker_result_from_ingress( &launch, @@ -1761,6 +1820,7 @@ mod tests { proposal_modeling_request_id: None, claim_repair_request_id: None, frontier_planning_request_id: Some("planning-request-1".into()), + frontier_plan_mind_request_id: None, }; let result = role_worker_result_from_ingress( &launch, @@ -1790,6 +1850,56 @@ mod tests { Ok(()) } + #[test] + fn mind_ingress_preserves_exact_typed_judgment() -> Result<()> { + let parsed = parse_assistant_json::( + r#"{"roleId":"mindAdmissionReview","verdict":"adopt","summary":"bounded","nextSafeMove":"admit","filesInspected":[],"frontierPlanMindRequestId":"mind-request-1","frontierPlanMindDecision":{"mindRequestId":"mind-request-1","planningRequestId":"planning-request-1","imaginationResultId":"imagination-result-1","candidateId":"candidate-1","candidateSha256":"candidate-hash-1","decision":"adopt","rationale":"Exact candidate is bounded and falsifiable.","decidedAt":"2026-07-15T10:00:00Z"}}"#, + )?; + let launch = EpiphanyRuntimeWorkerLaunchRequest { + schema_version: epiphany_core::RUNTIME_WORKER_LAUNCH_REQUEST_SCHEMA_VERSION.into(), + job_id: "mind-job-1".into(), + binding_id: epiphany_core::EPIPHANY_MIND_ROLE_BINDING_ID.into(), + role: epiphany_core::EPIPHANY_MIND_OWNER_ROLE.into(), + authority_scope: "epiphany.procedure.mind_admission_review".into(), + instruction: "judge".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.procedure.mind_admission_review", + "role", + epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID, + ), + proposal_modeling_request_id: None, + claim_repair_request_id: None, + frontier_planning_request_id: None, + frontier_plan_mind_request_id: Some("mind-request-1".into()), + }; + let result = role_worker_result_from_ingress( + &launch, + "mindAdmissionReview", + "mind-result-1", + &parsed, + Vec::new(), + ); + assert_eq!( + result.frontier_plan_mind_request_id.as_deref(), + Some("mind-request-1") + ); + let decision = result + .frontier_plan_mind_decision()? + .expect("typed Mind decision"); + assert_eq!( + decision.decision, + epiphany_core::RepoFrontierPlanDecision::Adopt + ); + assert_eq!(decision.candidate_sha256, "candidate-hash-1"); + assert!(result.state_patch_msgpack.is_none()); + assert!(result.repo_model_patch_msgpack.is_none()); + Ok(()) + } + fn test_openai_event( request_id: &str, sequence: u64, @@ -1965,6 +2075,7 @@ mod tests { proposal_modeling_context: None, claim_repair_context: None, frontier_planning_context: None, + frontier_plan_mind_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: Vec::new(), @@ -1987,8 +2098,9 @@ mod tests { epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID, ), proposal_modeling_request_id: None, - claim_repair_request_id: None, - frontier_planning_request_id: None, + claim_repair_request_id: None, + frontier_planning_request_id: None, + frontier_plan_mind_request_id: None, created_at: now(), }, )?; @@ -2096,6 +2208,7 @@ mod tests { proposal_modeling_context: None, claim_repair_context: None, frontier_planning_context: None, + frontier_plan_mind_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: Vec::new(), @@ -2120,6 +2233,7 @@ mod tests { proposal_modeling_request_id: None, claim_repair_request_id: None, frontier_planning_request_id: None, + frontier_plan_mind_request_id: None, created_at: now(), }, )?; diff --git a/epiphany-state-model/src/lib.rs b/epiphany-state-model/src/lib.rs index 11f9fdfe..27177875 100644 --- a/epiphany-state-model/src/lib.rs +++ b/epiphany-state-model/src/lib.rs @@ -778,6 +778,12 @@ pub struct RepoFrontierItem { #[ts(type = "Array")] pub source_scope: Vec, pub recommended_next_organ: String, + /// The exact plan admitted by Mind for this frontier item. Imagination may + /// propose this payload, but only the dedicated model transition may make + /// it part of Modeling-owned truth. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(type = "RepoFrontierAdoptedPlan | null")] + pub adopted_plan: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] #[ts(type = "Array")] pub dependency_item_ids: Vec, @@ -800,6 +806,30 @@ pub struct RepoFrontierItem { pub superseded_by: Option, } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)] +pub struct RepoFrontierAdoptedPlan { + pub planning_request_id: String, + pub result_id: String, + pub job_id: String, + pub candidate_id: String, + pub candidate_sha256: String, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[ts(type = "Array")] + pub safe_paths: Vec, + pub action: String, + pub command: String, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[ts(type = "Array")] + pub checks: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[ts(type = "Array")] + pub stop_conditions: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[ts(type = "Array")] + pub rollback_steps: Vec, + pub commit_message: String, +} + #[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema, TS, Default)] #[serde(rename_all = "snake_case")] #[ts(rename_all = "snake_case")] @@ -836,6 +866,11 @@ pub enum RepoModelPatchPurpose { soul_verdict_receipt_id: String, }, RepairClaim, + AdoptFrontierPlan { + planning_request_id: String, + result_id: String, + candidate_id: String, + }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)] @@ -873,6 +908,11 @@ pub enum RepoModelPatchOperation { #[serde(default, skip_serializing_if = "Option::is_none")] superseded_by: Option, }, + AdoptFrontierPlan { + frontier_item_id: String, + expected_frontier_item_hash: String, + adopted_plan: RepoFrontierAdoptedPlan, + }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)] diff --git a/notes/epiphany-anatomy.md b/notes/epiphany-anatomy.md index 9376d1ba..38f2561c 100644 --- a/notes/epiphany-anatomy.md +++ b/notes/epiphany-anatomy.md @@ -45,6 +45,11 @@ embodiment titles, role-memory diaries, or Persona state. - Body is sensed and changed; it does not decide. - Mind admits durable state; it is not a chatty lane. +- Mind may invoke a bounded, stateless admission-review procedure when a typed + proposal requires model judgment. That procedure serves Mind; it is not an + embodied sub-agent, has no heartbeat or lane memory, cannot `roleAccept` or + `selfPatch`, and its immutable judgment has no consequence until Mind replays + the causal chain and admits it. - Continuity preserves/rebuilds context across rupture; it is protocol machinery, not an embodied identity. - Substrate Gate grants substrate access; it does not inspect evidence, mutate diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 59877ffd..bb587fc8 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -982,3 +982,54 @@ 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. + +## Prompted frontier admission review and canonical adoption (2026-07-15) + +The repo-frontier planning nerve is complete and the preceding “Next implement +Mind” sentence is superseded. Self binds one exact admitted model/frontier +request; coordinator atomically binds the Imagination launch; Imagination +persists one immutable result with its sole embedded candidate; the runtime +derives a CAS-bound Mind review request; coordinator atomically launches a +bounded non-embodied admission-review procedure; that procedure returns one +immutable typed Adopt/Refuse/Hold judgment; and Mind's admission transaction +replays the entire chain before consequence. + +Refuse and Hold are inert terminal receipts. Adopt atomically installs the exact +candidate and provenance on the canonical frontier, emits specialized review +and admission receipts, and alone changes Imagination to Hands. Self derives the +route from that model. Hands intent binds route, candidate hash, plan action, +and safe paths; Soul admission requires the exact plan command and receives the +checks, stop conditions, rollback steps, and commit message. Modeling may close +the frontier after Soul verdict only while preserving adopted execution anatomy. +There is no standalone candidate/adoption truth and no caller-authored decision. + +Mind remains persistent state and admission authority, not an embodied chatty +lane. The prompted model call is explicitly a bounded admission-review +procedure serving Mind, with no heartbeat, lane memory, `roleAccept`, +`selfPatch`, or foreign organ patch mouths. + +Hostile coverage includes swapped causal ids/hashes/jobs, failed worker output, +foreign cargo, duplicate/racing launch and terminal claims, generic adoption +bypass, post-adoption execution-anatomy mutation, and same-path command +substitution. The composed happy path proves Adopt through exact Hands command, +Verification/Soul, accepted Modeling incorporation, Resolved closure, and +byte-identical plan survival. Final proof: core 330 passed with one intentional +cross-process helper ignored; OpenAI runtime library 10 passed, both runtime +binary suites passed 6 each, and all core binaries check. Soul's final landing +audit passed. Exact concurrent admission retries are forced through a two-party +pre-CAS barrier in test builds; the loser reloads and authenticates the winner's +entire immutable decision/admission chain. The synchronization hook is compiled +only for tests, so production exposes no executable seam at the commit boundary. +The next organ is the production semantic index over typed graph claims so +Modeling owns a live persistent searchable Body map instead of making Hands +rediscover the repository. + +The selected semantic substrate is Qdrant: available locally in Docker and +intended on the new Yggdrasil host (64 GB RAM, GTX 1080 for embedding/model +workers). It is a rebuildable projection, never canonical Mind or Modeling +truth. CultCache/CultMesh remains authoritative. Use separate typed Mind and +Modeling namespaces/collections or equivalently hard query partitions so +private doctrine/memory cannot leak into Hands Body retrieval and repository +claims cannot silently become doctrine. Every point must retain source document +id, schema version, canonical hash, provenance, authority/visibility scope, and +embedding-model version. diff --git a/notes/repo-frontier-planning-authority-map.md b/notes/repo-frontier-planning-authority-map.md index 117c7b4d..2683debd 100644 --- a/notes/repo-frontier-planning-authority-map.md +++ b/notes/repo-frontier-planning-authority-map.md @@ -1,87 +1,77 @@ # Repo Frontier Planning Authority Map -## Current state +## Current authority -The planning family now has a real Self-to-Imagination launch artery. Self's -`RepoFrontierPlanningRequest` binds the uniquely admitted current model, exact -actionable frontier, runtime, and authoritative thread. Coordinator commit -replays those bytes, injects the exact typed Imagination projection, and -atomically persists a request-keyed launch binding whose hash covers the stored -worker launch document. Caller-authored projections, mixed authority carriers, -stale requests, causal substitutions, and duplicate/racing launches are -refused without loser artifacts. - -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 -admission replace them. - -## Target authority - -- Owner — Self selects one dependency-ready, unchallenged, active Imagination - frontier from the uniquely admitted current RepoModel and emits one immutable - planning request. -- Inputs — current RepoModel envelope and hash, its unique Mind admission - receipt, exact frontier item and serialized hash, source scope, current claim - challenges, runtime identity, and authoritative thread. -- Coordinator output — one typed Imagination context projection and one - request-keyed immutable launch binding inside the atomic coordinator launch - transaction. -- Imagination output — one immutable worker result that exclusively echoes the - planning request and contains one typed candidate. It proposes; it does not - route Hands or mutate RepoModel. -- Mind output — one fully correlated Adopt, Refuse, or Hold receipt. Mind alone - constructs and persists the decision after replaying request, launch binding, - worker launch document, immutable result, candidate, current model, and exact - frontier bytes. -- Adopt effect — a narrow RepoModel transition must make the selected frontier - executable without allowing the adoption receipt to become a shadow model. - The downstream Hands route remains derived from admitted model bytes and may - reference the exact adopted plan for bounded action/check/rollback cargo. -- Refuse/Hold effect — immutable decision receipts only. They do not mutate the - model, claim the deterministic Adopt identity, or grant Hands authority. +- Self selects one dependency-ready, unchallenged, active Imagination frontier + from the uniquely admitted current RepoModel. It writes one immutable + `RepoFrontierPlanningRequest` binding the model revision/hash, admission, + frontier bytes, source scope, runtime, and thread. +- Coordinator commit replays that request, injects the exact typed Imagination + projection, and atomically writes a request-keyed launch binding whose hash + covers the stored worker launch document. +- Imagination writes one immutable runtime result with an exclusive planning + request echo and one embedded canonical `RepoFrontierPlanCandidate`. It does + not route Hands or mutate RepoModel. +- The runtime derives one `RepoFrontierPlanMindRequest` from the immutable + Imagination result and candidate hash. Its insert is CAS-bound to the exact + current model envelope. +- Coordinator commit launches one bounded, non-embodied admission-review + procedure serving Mind. It injects the exact request/planning/candidate + projection and atomically writes the request-keyed launch binding. This + procedure is not an embodied Mind lane; Mind remains persistent state and + admission authority. +- The admission reviewer writes one immutable typed Adopt, Refuse, or Hold + judgment. Result persistence replays the request, both worker results, both + launch bindings, stored launch bytes, runtime/thread, candidate identity, and + current model/frontier while rejecting foreign organ cargo. +- `commit_repo_frontier_plan_decision` consumes only that immutable review + result. Refuse and Hold write one inert terminal receipt. Adopt atomically + writes the next canonical RepoModel, its specialized Mind review/admission, + and the terminal decision receipt. +- The dedicated Adopt transition installs the exact candidate cargo and + provenance on `RepoFrontierItem.adopted_plan` and alone changes + Imagination to Hands. Generic frontier writers cannot author or rewrite it. +- Self derives the Hands route from the admitted model. The route copies the + exact plan and safe paths; Hands intent binds route, candidate hash, and plan + action; verification requires the exact admitted command and shows Soul the + checks, stop conditions, rollback steps, and commit message. +- Modeling may incorporate Soul's verdict by changing lifecycle/gap/evidence + fields while preserving the adopted execution anatomy byte-for-byte. ## Derived state -- Planning readiness is derived from current admitted model plus absence of a - current terminal Mind decision. +- Planning readiness is current admitted Imagination frontier plus absence of a + terminal decision receipt. - Candidate visibility is a projection of the immutable Imagination result. -- Hands readiness is derived from the post-Adopt admitted model and its exact - plan provenance, never from a candidate or adoption document alone. +- Hands readiness and execution cargo derive only from the post-Adopt canonical + model, never from a candidate or decision shadow store. +- Completion remains Soul verdict followed by Modeling verdict incorporation; + adoption is not completion. ## Forbidden writers and substitutions -- No external or generic caller may persist an Imagination candidate or Mind - decision directly. -- Generic Imagination `statePatch.planning.objective_drafts` is not repo - frontier planning authority. -- Schema namespace, prompt prose, result metadata, evidence coincidence, or - "the only current request" cannot substitute for an exact request echo and - launch binding. -- Adopt cannot merely suppress future planning while leaving the frontier and - Hands route unchanged. -- Candidate persistence must share a CAS with the current model expectation; - validation followed by an unrelated absent-only insert is stale-authority - leakage. -- No generic work-order registry or mode router. This is one explicit organ - nerve with one owner at each transition. +- No standalone candidate or adoption store exists in the production path. +- No caller supplies Mind's decision, rationale, or timestamp. +- No prompted admission reviewer may emit state, Self, RepoModel, Verification, + Modeling, or implementation authority cargo. +- Prompt prose, namespaces, metadata, evidence coincidence, or “the current + request” cannot substitute for exact typed echoes and launch bindings. +- Generic Upsert/Revise cannot create an adopted plan or perform + Imagination-to-Hands. Verdict incorporation cannot rewrite execution anatomy. +- Same-path Hands work cannot substitute a different action, candidate, or + command while retaining route authority. +- Mind is not a chatty embodied lane. The bounded admission-review procedure + serves Mind and has no heartbeat, lane memory, `roleAccept`, or `selfPatch`. -## Deletion line before build +## Verification layer -1. Public candidate/adoption persistence exports are gone. -2. Direct internal fixture writers die when result correlation and Mind - admission land. -3. Any Adopt path that does not change the model or downstream executable - truth is removed rather than retained as compatibility behavior. +- Hostile proofs cover stale/swapped model, frontier, request, result, job, + launch binding, projection, candidate hash, escaped paths, failed worker + output, foreign cargo, duplicate/racing launches, competing terminal writes, + generic adoption bypass, post-adoption anatomy mutation, and same-path command + substitution. +- Exact retry authenticates immutable terminal decision plus its historical + review/admission chain without requiring later canonical model revisions to + remain frozen. +- The next organ is the production semantic index over typed graph claims so + Modeling can maintain a live persistent searchable Body map. diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index e8ede157..0a989b2f 100644 Binary files a/state/ledgers.msgpack and b/state/ledgers.msgpack differ diff --git a/state/map.yaml b/state/map.yaml index 9f949902..50b6a1bb 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -714,4 +714,4 @@ current_status: - Service lifecycle receipt history is now event-only: `load_epiphany_cultmesh_daemon_service_lifecycle_receipts` filters out the `epiphany-local/daemon-service-lifecycle-receipt/latest` mirror, while `load_latest_epiphany_cultmesh_daemon_service_lifecycle_receipt` remains the latest-state reader. Focused unit test `service_lifecycle_receipt_history_excludes_latest_mirror` proves two per-receipt writes return exactly two history records and latest points at the second. Fresh smoke `verse-query-lifecycle-history-filter-smoke` and live overview/triage `local-20260618-145632-497-efac6f0d` / `local-20260618-145632-321-867947af` prove compact swarm sights still expose service lifecycle attention plus the present elevated runbook artifact without daemon pokes or service mutation. Do not use a broad unscoped cargo test filter for this check on Windows; it fans out across bin harnesses and hit a broken pipe during this pass. - `epiphany-verse-query seed-compact` is the compact seed receipt for wrappers and read-only rites. The older `seed` command still prints the full local Verse context for explicit debugging, but compact wrapper modes now avoid full `seed`/final `query` readback after the combined overview proof exposed Windows debug stack overflow from full-context serialization pressure. last_completed_repo_swarm_cut: Huginn live-fire has crossed the first real repo Body from Persona pressure to branch-local Hands commit and Soul/Mind closure, and now has repeatable closure and redacted-public-proof mouths. On `E:\Projects\Huginn` branch `epiphany/huginn/first-awakening`, `tools/epiphany_local_run.ps1 -Mode repo-persona-intake` accepted item `huginn-inspection-readback-livefire`, Weksa/Persona intake remained sealed, and `epiphany-work derive-plan` lowered it to model-authored safe family `repo.status_section` for `README.md` only. `tools/epiphany_local_run.ps1 -Mode repo-swarm-run` advanced four bounded pulses: run-from-plan, adopt-from-plan, execute-from-plan, and close-from-execute. Huginn commit `e9b07c38fceabbdc89ea35c2156326c80c0393a2` updated only `README.md`; closure receipt `.epiphany\work\work-close-huginn-inspection-readback-livefire.json` records status `closed`, Soul verdict `passed`, Mind state commit `repo-work-close-huginn-inspection-readback-livefire-mind-commit`, safe family `repo.status_section`, path scope `README.md`, Bifrost publication gate, and `privateStateExposed=false`. Verification also ran Huginn's TypeScript compiler through concrete Node path: `C:\Program Files\nodejs\node.exe E:\Projects\Huginn\node_modules\typescript\bin\tsc -p tsconfig.json` passed. Native verifier `epiphany-repo-livefire-closure-smoke --workspace E:\Projects\Huginn --item huginn-inspection-readback-livefire --expected-path README.md --expected-family repo.status_section --expected-gate awaiting-publication --expected-blocker bifrost-publication-missing` checks closure receipt, Mind map entry, overview gate/blocker, authority seals, and `git show` changed paths, returning `status=ok`, `soulVerdict=passed`, `publicationGate=Bifrost`, and `privateStateExposed=false`. Wrapper `tools/epiphany_local_run.ps1 -Mode repo-livefire-closure -Workspace E:\Projects\Huginn -RepoWorkItem huginn-inspection-readback-livefire -SkipBuild` exposes the same read-only proof through the local operator mouth; artifact `local-20260621-190156-803-2f85ba15` printed commit, path, gate, blocker, Soul verdict, Mind commit receipt, and `privateStateExposed=False` without push, merge, publication, or target mutation. New wrapper `tools/epiphany_local_run.ps1 -Mode repo-work-public-proof -Workspace E:\Projects\Huginn -RepoWorkItem huginn-inspection-readback-livefire -SkipBuild` exports the redacted proof bundle and repo-local Verse index without publishing; artifact `local-20260621-190645-345-4555b1dc` wrote `.epiphany\public\proof-bundles\repo-work-public-proof-huginn-inspection-readback-livefire.json`, proof id `repo-work-public-proof-huginn-inspection-readback-livefire`, SHA-256 `857545b199f3e37eb06c2e757e5cfc201958e46667bb1f8403fd333a22620f63`, nine artifact rows, zero publication rows, and `privateStateExposed=False`. swarm overview readback `local-20260621-190701-140-b9519ec6` sees the `REPO-WORK-PUBLIC-PROOF` row and action row from Huginn's repo-local Verse with the same SHA-256 while Bifrost publication remains missing. Launcher compact modes are now named compact operator-context modes instead of read-only modes, and `bifrost-public-proof` summaries print item, target public Verse, room, proof SHA-256, review receipts, and credit receipts so publication receipts are auditable without claiming upstream sync. - next_action: Continue the repo-frontier planning nerve from notes/repo-frontier-planning-authority-map.md. Self-to-Imagination launch and result arteries are real: request v1 binds model/frontier/runtime/thread; coordinator atomically injects and binds the exact launch; immutable worker result v2 exclusively echoes that request and embeds one canonical typed candidate after full request/launch/model/frontier replay, with generic patch mouths sealed. Next replace the internal candidate/adoption fixture writers with one Mind-only correlated Adopt/Refuse/Hold admission. Adopt must atomically install the exact candidate cargo and provenance into the canonical frontier, change only Imagination to Hands through a dedicated patch purpose/operation, and admit the new RepoModel; routes must copy execution cargo from that model, never consult a candidate/adoption shadow store. Refuse/Hold remain inert immutable receipts. After the nerve lands, add the production semantic index over typed graph claims so Modeling maintains a live searchable body map instead of forcing Hands to rediscover anatomy. Do not revive Proprioception, epiphany-work, metadata adapters, generic work-order routers, or inference from namespaces/prompt prose/evidence coincidence. Keep closure's zero-substring invariant. + next_action: Build the production Qdrant semantic projection over canonical typed state. Modeling needs a live persistent searchable Body map over repo claims/architecture/frontier; Mind needs semantic retrieval over doctrine, durable memory, evidence, decisions, and rehydration cues. CultCache/CultMesh remains authoritative and Qdrant stays rebuildable, with separate typed Mind/Modeling namespaces, provenance, visibility, schema/hash, and embedding-model version. Local Docker already hosts Qdrant; new Yggdrasil will provide the same substrate with 64GB RAM and a GTX 1080 for embedding/model workers. The repo-frontier planning nerve is complete: typed Self request, coordinator-bound Imagination launch, immutable embedded candidate, CAS-bound Mind review request, coordinator-bound non-embodied admission-review procedure, immutable Adopt/Refuse/Hold judgment, atomic canonical Adopt transition, model-derived Hands route, candidate/action/command-bound execution authority, Soul plan-contract visibility, and Modeling verdict closure without plan rewrite. Do not revive Proprioception, epiphany-work, metadata adapters, generic work-order routers, shadow candidate/adoption stores, embodied Mind lanes, or inference from namespaces/prompt prose/evidence coincidence. Keep closure's zero-substring invariant.