From 0c90e4471d158e71995cf1d1b15b1ebffeb532db Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 22:29:10 +0100 Subject: [PATCH] Close canonical RepoModel organ loop --- epiphany-core/src/agent_launch.rs | 123 +- .../src/bin/epiphany-mvp-coordinator.rs | 102 +- epiphany-core/src/bin/epiphany-mvp-status.rs | 4 + epiphany-core/src/coordinator_acceptance.rs | 322 ++- .../src/coordinator_launch_context.rs | 386 ++- epiphany-core/src/coordinator_results.rs | 3 + epiphany-core/src/eyes_gateway.rs | 3 + epiphany-core/src/lib.rs | 15 +- epiphany-core/src/memory_graph/context_cut.rs | 3 + epiphany-core/src/memory_graph/mod.rs | 5 +- .../src/memory_graph/profiles/repo.rs | 1 + epiphany-core/src/memory_graph/store.rs | 43 +- epiphany-core/src/memory_graph/tests.rs | 2 + epiphany-core/src/mind_gateway.rs | 3 + epiphany-core/src/prompt_context.rs | 4 + .../src/prompts/epiphany_specialists.toml | 7 +- epiphany-core/src/repo_model_gateway.rs | 250 ++ epiphany-core/src/runtime_spine.rs | 2501 ++++++++++++++++- epiphany-core/src/soul_gateway.rs | 55 +- epiphany-core/src/surfaces/coordinator.rs | 78 +- epiphany-core/src/surfaces/role_result.rs | 123 +- epiphany-openai-runtime/src/lib.rs | 97 +- epiphany-state-model/src/lib.rs | 17 + notes/fresh-workspace-handoff.md | 54 +- notes/repo-frontier-organ-loop.md | 91 + state/ledgers.msgpack | Bin 487714 -> 488197 bytes state/map.yaml | 2 +- vendor/cultcache-rs | 2 +- 28 files changed, 4044 insertions(+), 252 deletions(-) create mode 100644 epiphany-core/src/repo_model_gateway.rs create mode 100644 notes/repo-frontier-organ-loop.md diff --git a/epiphany-core/src/agent_launch.rs b/epiphany-core/src/agent_launch.rs index 202c524e..b5778dba 100644 --- a/epiphany-core/src/agent_launch.rs +++ b/epiphany-core/src/agent_launch.rs @@ -195,6 +195,10 @@ pub fn epiphany_role_launch_output_schema(role_id: EpiphanyRoleResultRoleId) -> "additionalProperties": false } }); + if role_id == EpiphanyRoleResultRoleId::Verification { + properties["verificationRequestId"] = serde_json::json!({"type": "string", "minLength": 1}); + properties["frontierRouteId"] = serde_json::json!({"type": "string", "minLength": 1}); + } let mut required = vec![ "roleId", "verdict", @@ -202,6 +206,10 @@ pub fn epiphany_role_launch_output_schema(role_id: EpiphanyRoleResultRoleId) -> "nextSafeMove", "filesInspected", ]; + if role_id == EpiphanyRoleResultRoleId::Verification { + required.push("verificationRequestId"); + required.push("frontierRouteId"); + } if role_id == EpiphanyRoleResultRoleId::Imagination { if let Some(map) = properties.as_object_mut() { map.insert( @@ -279,42 +287,88 @@ pub fn epiphany_role_launch_output_schema(role_id: EpiphanyRoleResultRoleId) -> } else if role_id == EpiphanyRoleResultRoleId::Modeling { if let Some(map) = properties.as_object_mut() { map.insert( - "statePatch".to_string(), + "repoModelPatch".to_string(), serde_json::json!({ "type": "object", - "description": "Required reviewable statePatch for Mind admission from Modeling. Use only graphs, graphFrontier, graphCheckpoint, scratch, investigationCheckpoint, observations, and evidence. The patch must include at least one durable modeling field, not observations/evidence alone.", - "anyOf": [ - {"required": ["graphs"]}, - {"required": ["graphFrontier"]}, - {"required": ["graphCheckpoint"]}, - {"required": ["scratch"]}, - {"required": ["investigationCheckpoint"]} - ], + "description": "Required typed proposal against the canonical repository model. This is ingress for later review, not admission authority.", + "required": ["patch_id", "base_revision", "base_hash", "applied_at", "purpose", "operations"], "properties": { - "investigationCheckpoint": { - "type": "object", - "properties": { - "disposition": { - "type": "string", - "enum": ["resume_ready", "regather_required"] + "patch_id": {"type": "string", "minLength": 1}, + "base_revision": {"type": "integer", "minimum": 0}, + "base_hash": {"type": "string", "minLength": 1}, + "applied_at": {"type": "string", "minLength": 1}, + "purpose": { + "description": "Use {kind: evolution} for ordinary Modeling output; frontier closure requires separately routed authority.", + "oneOf": [ + { + "type": "object", + "required": ["kind"], + "properties": {"kind": {"const": "evolution"}}, + "additionalProperties": false + }, + { + "type": "object", + "required": ["kind", "route_id", "soul_verdict_receipt_id"], + "properties": { + "kind": {"const": "incorporate_frontier_verdict"}, + "route_id": {"type": "string", "minLength": 1}, + "soul_verdict_receipt_id": {"type": "string", "minLength": 1} + }, + "additionalProperties": false } - }, - "additionalProperties": true - } + ] + }, + "operations": {"type": "array", "minItems": 1, "items": {"type": "object"}} }, "additionalProperties": true }), ); + map.insert( + "repoFrontierModelingRequestId".to_string(), + serde_json::json!({"type": "string", "minLength": 1}), + ); + map.insert( + "statePatch".to_string(), + serde_json::json!({ + "type": "object", + "description": "Optional generic Mind-reviewable observations/evidence only. Repository anatomy belongs exclusively in repoModelPatch.", + "properties": { + "observations": {"type": "array"}, + "evidence": {"type": "array"} + }, + "additionalProperties": false + }), + ); } required.push("frontierNodeIds"); - required.push("statePatch"); + required.push("repoModelPatch"); } - serde_json::json!({ + let mut schema = serde_json::json!({ "type": "object", "properties": properties, "required": required, "additionalProperties": true - }) + }); + if role_id == EpiphanyRoleResultRoleId::Modeling { + schema["allOf"] = serde_json::json!([{ + "if": { + "properties": { + "repoModelPatch": { + "properties": { + "purpose": { + "properties": {"kind": {"const": "incorporate_frontier_verdict"}}, + "required": ["kind"] + } + }, + "required": ["purpose"] + } + }, + "required": ["repoModelPatch"] + }, + "then": {"required": ["repoFrontierModelingRequestId"]} + }]); + } + schema } pub fn epiphany_reorient_launch_output_schema() -> serde_json::Value { @@ -358,7 +412,7 @@ const EPIPHANY_SPECIALIST_PROMPTS_TOML: &str = include_str!("prompts/epiphany_sp const EPIPHANY_WORKER_BOUNDARY_PROMPT: &str = r#"## Epiphany Worker Boundary You are one bounded Epiphany worker for this launch only. Your authority comes from the typed launch document, the role-local instruction, and the declared output contract. Do the role, name uncertainty, and return the required JSON object. Do not become the coordinator, do not accept or promote your own output, do not invent durable state outside an allowed statePatch, and do not treat model transport or Codex machinery as prompt authority. -If you learned a durable role-local habit, you may include a bounded selfPatch. Project truth belongs in statePatch or evidence, not memory."#; +If you learned a durable role-local habit, you may include a bounded selfPatch. Project truth belongs in the role's typed output artifact or evidence, not memory."#; #[derive(Debug, serde::Deserialize)] pub struct EpiphanySpecialistPromptConfig { @@ -822,6 +876,31 @@ mod tests { assert!(!prompt.contains("Heartbeat: every lane")); } + #[test] + fn modeling_schema_exposes_only_typed_evolution_or_verdict_incorporation() { + let schema = epiphany_role_launch_output_schema(EpiphanyRoleResultRoleId::Modeling); + let purposes = schema["properties"]["repoModelPatch"]["properties"]["purpose"]["oneOf"] + .as_array() + .expect("typed Modeling purpose alternatives"); + assert_eq!(purposes.len(), 2); + assert_eq!(purposes[0]["properties"]["kind"]["const"], "evolution"); + assert_eq!( + purposes[1]["properties"]["kind"]["const"], + "incorporate_frontier_verdict" + ); + assert!( + purposes[1]["required"] + .as_array() + .unwrap() + .iter() + .any(|value| value == "route_id") + ); + assert_eq!( + schema["allOf"][0]["then"]["required"][0], + "repoFrontierModelingRequestId" + ); + } + #[test] fn reorient_worker_prompt_is_bounded_not_full_persistent_memory() { let prompt = build_epiphany_reorient_launch_instruction(CoreEpiphanyReorientAction::Resume); diff --git a/epiphany-core/src/bin/epiphany-mvp-coordinator.rs b/epiphany-core/src/bin/epiphany-mvp-coordinator.rs index 097f46e0..fce34e6e 100644 --- a/epiphany-core/src/bin/epiphany-mvp-coordinator.rs +++ b/epiphany-core/src/bin/epiphany-mvp-coordinator.rs @@ -10,6 +10,9 @@ use epiphany_core::HANDS_COMMAND_RECEIPT_TYPE; use epiphany_core::HANDS_COMMIT_RECEIPT_TYPE; use epiphany_core::HANDS_PATCH_RECEIPT_TYPE; use epiphany_core::HandsActionIntent; +use epiphany_core::REPO_FRONTIER_HANDS_AUTHORITY_CONTRACT; +use epiphany_core::REPO_FRONTIER_HANDS_AUTHORITY_SCHEMA_VERSION; +use epiphany_core::RepoFrontierHandsAuthority; use epiphany_core::RuntimeSpineEventOptions; use epiphany_core::RuntimeSpineInitOptions; use epiphany_core::RuntimeSpineSessionOptions; @@ -21,8 +24,10 @@ use epiphany_core::load_epiphany_cultmesh_swarm_brake; use epiphany_core::put_coordinator_run_receipt; use epiphany_core::put_hands_action_intent; use epiphany_core::put_hands_action_review; +use epiphany_core::put_repo_frontier_hands_authority; use epiphany_core::put_substrate_gate_repo_access_grant_receipt; use epiphany_core::runtime_spine_status; +use epiphany_core::select_and_commit_repo_frontier_route; use epiphany_core::substrate_gate_coordinator_implementation_grant; use serde_json::Value; use serde_json::json; @@ -802,9 +807,10 @@ fn record_hands_implementation_gate( artifact_dir: &Path, thread_id: &str, step_index: usize, - status: &Value, + _status: &Value, ) -> Result { let requested_at = now(); + let route = select_and_commit_repo_frontier_route(runtime_store, &requested_at)?; let suffix = format!( "{}-{}-{}", sanitize_id(thread_id), @@ -813,7 +819,7 @@ fn record_hands_implementation_gate( ); let runtime_job_id = format!("hands-implementation-{suffix}"); let grant_id = format!("substrate-grant-{runtime_job_id}"); - let requested_paths = implementation_requested_paths(status); + let requested_paths = route.source_scope.clone(); let substrate_grant = substrate_gate_coordinator_implementation_grant( grant_id.clone(), @@ -862,6 +868,22 @@ fn record_hands_implementation_gate( HANDS_COMMIT_RECEIPT_TYPE.to_string(), ]; put_hands_action_review(runtime_store, &review)?; + let authority = RepoFrontierHandsAuthority { + schema_version: REPO_FRONTIER_HANDS_AUTHORITY_SCHEMA_VERSION.to_string(), + authority_id: format!("repo-frontier-hands-authority-{}", intent.intent_id), + 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_id.clone(), + requested_paths: requested_paths.clone(), + granted_at: review.reviewed_at.clone(), + contract: REPO_FRONTIER_HANDS_AUTHORITY_CONTRACT.to_string(), + }; + put_repo_frontier_hands_authority(runtime_store, &authority)?; Ok(json!({ "status": "ready", @@ -869,6 +891,10 @@ fn record_hands_implementation_gate( "substrateGateGrantReceiptId": grant_id, "intentId": intent.intent_id, "reviewId": review.review_id, + "routeId": route.route_id, + "modelRevision": route.model_revision, + "modelHash": route.model_hash, + "frontierItemId": route.frontier_item_id, "requestedPaths": requested_paths, "requiredReceipts": review.required_receipts, "recordPassCommand": hands_record_pass_command(runtime_store, artifact_dir), @@ -905,49 +931,6 @@ fn hands_record_pass_command(runtime_store: &Path, artifact_dir: &Path) -> Value }) } -fn implementation_requested_paths(status: &Value) -> Vec { - let mut paths = Vec::new(); - for pointer in [ - "/scene/scene/investigationCheckpoint/codeRefs", - "/scene/scene/investigationCheckpoint/code_refs", - "/scene/scene/graphFrontier/dirtyPaths", - "/scene/scene/graphFrontier/dirty_paths", - ] { - collect_requested_paths_at(status, pointer, &mut paths); - } - paths.sort(); - paths.dedup(); - if paths.is_empty() { - paths.push(".".to_string()); - } - paths -} - -fn collect_requested_paths_at(status: &Value, pointer: &str, paths: &mut Vec) { - match status.pointer(pointer) { - Some(Value::Array(items)) => { - for item in items { - if let Some(path) = item - .as_str() - .or_else(|| item.get("path").and_then(Value::as_str)) - { - push_requested_path(paths, path); - } - } - } - Some(Value::String(path)) => push_requested_path(paths, path), - _ => {} - } -} - -fn push_requested_path(paths: &mut Vec, path: &str) { - let normalized = path.trim().replace('\\', "/"); - if normalized.is_empty() { - return; - } - paths.push(normalized); -} - fn launch_role( runtime_store: &Path, thread_id: &str, @@ -1890,35 +1873,6 @@ mod tests { )); } - #[test] - fn implementation_requested_paths_use_checkpoint_and_frontier_scope() { - let status = json!({ - "scene": { - "scene": { - "investigationCheckpoint": { - "codeRefs": [ - {"path": "src\\main.rs"}, - {"path": "src/main.rs"}, - {"path": " notes/demo.md "} - ] - }, - "graphFrontier": { - "dirtyPaths": ["tests/demo.rs"] - } - } - } - }); - - assert_eq!( - implementation_requested_paths(&status), - vec![ - "notes/demo.md".to_string(), - "src/main.rs".to_string(), - "tests/demo.rs".to_string() - ] - ); - } - #[test] fn hands_implementation_gate_persists_grant_intent_and_review() -> Result<()> { let temp = tempfile::tempdir()?; diff --git a/epiphany-core/src/bin/epiphany-mvp-status.rs b/epiphany-core/src/bin/epiphany-mvp-status.rs index 32d2ccd5..00697509 100644 --- a/epiphany-core/src/bin/epiphany-mvp-status.rs +++ b/epiphany-core/src/bin/epiphany-mvp-status.rs @@ -40,6 +40,7 @@ use epiphany_core::interpret_runtime_role_worker_result; use epiphany_core::read_accepted_coordinator_state; use epiphany_core::recommend_crrc_action; use epiphany_core::recommend_reorientation; +use epiphany_core::runtime_has_actionable_hands_frontier; use epiphany_core::runtime_job_snapshot; use epiphany_core::runtime_role_worker_result; use epiphany_state_model::EpiphanyThreadState; @@ -408,6 +409,8 @@ fn run_native_status(args: &Args) -> Result { verification_finding.as_ref(), None, ); + let hands_frontier_ready = runtime_has_actionable_hands_frontier(&runtime_store_path) + .context("failed to derive actionable Hands frontier from runtime-spine state")?; let coordinator = derive_coordinator_status(EpiphanyCoordinatorStatusInput { state_status, checkpoint_present: state_ref @@ -441,6 +444,7 @@ fn run_native_status(args: &Args) -> Result { .verification_result_allows_implementation, verification_result_needs_evidence: finding_signals.verification_result_needs_evidence, reorient_finding_accepted: finding_signals.reorient_finding_accepted, + hands_frontier_ready, }); let coordinator_json = coordinator_status_json(&coordinator)?; let tool_invocations = native_tool_invocation_surface(&runtime_store_path)?; diff --git a/epiphany-core/src/coordinator_acceptance.rs b/epiphany-core/src/coordinator_acceptance.rs index 07164eba..e421fcae 100644 --- a/epiphany-core/src/coordinator_acceptance.rs +++ b/epiphany-core/src/coordinator_acceptance.rs @@ -2,6 +2,7 @@ use crate::coordinator_results::latest_runtime_link; use crate::coordinator_state::changed_fields; use crate::*; use epiphany_state_model::EpiphanyThreadState; +use sha2::{Digest, Sha256}; use std::path::Path; #[derive(Debug, Clone, PartialEq, Eq)] @@ -205,6 +206,27 @@ pub fn accept_coordinator_role_finding( available.push(SUBSTRATE_GATE_REPO_ACCESS_GRANT_RECEIPT_TYPE.to_string()); } } else if role_id == EpiphanyRoleResultRoleId::Verification { + let request_id = finding + .verification_request_id + .as_deref() + .ok_or_else(|| anyhow::anyhow!("Verification finding omitted verificationRequestId"))?; + finding + .frontier_route_id + .as_deref() + .ok_or_else(|| anyhow::anyhow!("Verification finding omitted frontierRouteId"))?; + let request = crate::runtime_repo_frontier_verification_request(store, request_id)? + .ok_or_else(|| anyhow::anyhow!("Verification finding names a missing request"))?; + crate::put_repo_frontier_verification_request(store, &request)?; + validate_verification_finding_binding(&finding, &request)?; + if request.hands_intent_id.trim().is_empty() + || request.hands_patch_receipt_id.trim().is_empty() + || request.hands_command_receipt_id.trim().is_empty() + || request.hands_commit_receipt_id.trim().is_empty() + { + return Err(anyhow::anyhow!( + "Verification finding does not echo its exact frontier request and route" + )); + } let verdict = soul_verdict_receipt_from_verification_finding( format!("soul-verdict-{}", update.accepted_receipt_id), &finding, @@ -225,6 +247,44 @@ pub fn accept_coordinator_role_finding( update.state_update.clone(), reference_turn_id, )?; + if role_id == EpiphanyRoleResultRoleId::Modeling { + let result_id = finding + .runtime_result_id + .as_deref() + .ok_or_else(|| anyhow::anyhow!("Modeling finding has no runtime result id"))?; + let job_id = finding + .runtime_job_id + .as_deref() + .ok_or_else(|| anyhow::anyhow!("Modeling finding has no runtime job id"))?; + let result = runtime_role_worker_result(store, job_id)? + .ok_or_else(|| anyhow::anyhow!("Modeling runtime result is missing"))?; + if result.result_id != result_id { + return Err(anyhow::anyhow!("Modeling finding/result identity mismatch")); + } + let patch_bytes = result + .repo_model_patch_msgpack + .as_deref() + .ok_or_else(|| anyhow::anyhow!("Modeling result has no repoModelPatch"))?; + let patch = result + .repo_model_patch()? + .ok_or_else(|| anyhow::anyhow!("Modeling repoModelPatch failed to decode"))?; + let candidate_review = RepoModelAdmissionReview { + schema_version: REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION.to_string(), + review_id: format!("repo-model-review-{result_id}"), + result_id: result_id.to_string(), + job_id: job_id.to_string(), + patch_id: patch.patch_id.clone(), + patch_sha256: format!("{:x}", Sha256::digest(patch_bytes)), + base_revision: patch.base_revision, + base_hash: patch.base_hash.clone(), + decision: MindGatewayDecision::Accept, + evidence_ids: finding.evidence_ids.clone(), + reviewed_at: accepted_at.clone(), + contract: REPO_MODEL_ADMISSION_CONTRACT.to_string(), + }; + let review = stable_repo_model_admission_review(store, candidate_review)?; + commit_repo_model_admission(store, result_id, &review)?; + } let commit = mind_state_commit_receipt( format!("mind-commit-{}", update.accepted_receipt_id), &update.mind_review, @@ -252,6 +312,48 @@ pub fn accept_coordinator_role_finding( }) } +fn stable_repo_model_admission_review( + store: &Path, + candidate: RepoModelAdmissionReview, +) -> anyhow::Result { + match coordinator_acceptance_cache(store)? + .get::(&candidate.review_id)? + { + Some(existing) + if existing.result_id == candidate.result_id + && existing.job_id == candidate.job_id + && existing.patch_id == candidate.patch_id + && existing.patch_sha256 == candidate.patch_sha256 + && existing.base_revision == candidate.base_revision + && existing.base_hash == candidate.base_hash + && existing.decision == candidate.decision + && existing.evidence_ids == candidate.evidence_ids + && existing.schema_version == candidate.schema_version + && existing.contract == candidate.contract => + { + Ok(existing) + } + Some(_) => Err(anyhow::anyhow!( + "stable repo model review id belongs to different admission bytes" + )), + None => Ok(candidate), + } +} + +fn validate_verification_finding_binding( + finding: &EpiphanyRoleFindingInterpretation, + request: &RepoFrontierVerificationRequest, +) -> anyhow::Result<()> { + if finding.verification_request_id.as_deref() != Some(request.request_id.as_str()) + || finding.frontier_route_id.as_deref() != Some(request.route_id.as_str()) + { + return Err(anyhow::anyhow!( + "Verification finding does not echo its exact frontier request and route" + )); + } + Ok(()) +} + pub fn completed_role_finding( runtime_store_path: Option<&Path>, state: &EpiphanyThreadState, @@ -563,7 +665,11 @@ pub fn build_native_role_acceptance_update( )); } }; - if role_id != EpiphanyRoleResultRoleId::Verification && finding.state_patch.is_none() { + if matches!( + role_id, + EpiphanyRoleResultRoleId::Imagination | EpiphanyRoleResultRoleId::Research + ) && finding.state_patch.is_none() + { return Err("completed role finding did not include a reviewable statePatch".to_string()); } if !errors.is_empty() { @@ -738,6 +844,57 @@ fn role_label_lower(role_id: EpiphanyRoleResultRoleId) -> &'static str { mod tests { use super::*; + #[test] + fn verification_finding_refuses_swapped_request_or_route() { + let request = RepoFrontierVerificationRequest { + schema_version: REPO_FRONTIER_VERIFICATION_REQUEST_SCHEMA_VERSION.to_string(), + request_id: "verification-request-1".to_string(), + route_id: "frontier-route-1".to_string(), + model_revision: 1, + model_hash: "model-hash".to_string(), + frontier_item_id: "frontier-1".to_string(), + frontier_item_hash: "frontier-hash".to_string(), + hands_intent_id: "intent-1".to_string(), + hands_review_id: "review-1".to_string(), + hands_patch_receipt_id: "patch-1".to_string(), + hands_command_receipt_id: "command-1".to_string(), + hands_commit_receipt_id: "commit-1".to_string(), + requested_at: "2026-07-13T00:00:00Z".to_string(), + contract: REPO_FRONTIER_VERIFICATION_REQUEST_CONTRACT.to_string(), + }; + let mut finding = EpiphanyRoleFindingInterpretation { + verdict: Some("pass".to_string()), + summary: Some("verified".to_string()), + next_safe_move: None, + checkpoint_summary: None, + scratch_summary: None, + files_inspected: Vec::new(), + frontier_node_ids: Vec::new(), + evidence_ids: Vec::new(), + artifact_refs: Vec::new(), + runtime_result_id: Some("result-1".to_string()), + runtime_job_id: Some("job-1".to_string()), + open_questions: Vec::new(), + evidence_gaps: Vec::new(), + risks: Vec::new(), + state_patch: None, + repo_model_patch: None, + self_patch: None, + self_persistence: None, + job_error: None, + item_error: None, + verification_request_id: Some(request.request_id.clone()), + frontier_route_id: Some(request.route_id.clone()), + }; + validate_verification_finding_binding(&finding, &request).expect("exact binding"); + + finding.frontier_route_id = Some("frontier-route-2".to_string()); + assert!(validate_verification_finding_binding(&finding, &request).is_err()); + finding.frontier_route_id = Some(request.route_id.clone()); + finding.verification_request_id = Some("verification-request-2".to_string()); + assert!(validate_verification_finding_binding(&finding, &request).is_err()); + } + #[test] fn completed_finding_admission_refuses_missing_binding() { let error = completed_role_finding( @@ -905,4 +1062,167 @@ mod tests { assert!(cache.get::("commit-9")?.is_some()); Ok(()) } + + #[test] + fn accepted_soul_verdict_preserves_exact_frontier_binding() -> anyhow::Result<()> { + let temp = tempfile::tempdir()?; + let store = temp.path().join("acceptance-soul.cc"); + let state = EpiphanyThreadState { + revision: 11, + ..Default::default() + }; + let review = MindGatewayReview { + schema_version: MIND_GATEWAY_REVIEW_SCHEMA_VERSION.to_string(), + gateway_id: "review-11".to_string(), + source_kind: "role".to_string(), + source_role_id: "verification".to_string(), + decision: MindGatewayDecision::Accept, + allowed_effects: Vec::new(), + refused_effects: Vec::new(), + reasons: Vec::new(), + contract: "test".to_string(), + }; + let commit = mind_state_commit_receipt( + "commit-11".to_string(), + &review, + 11, + Vec::new(), + "2026-07-13T00:00:00Z".to_string(), + ); + let soul = SoulVerdictReceipt { + schema_version: SOUL_VERDICT_RECEIPT_SCHEMA_VERSION.to_string(), + receipt_id: "soul-11".to_string(), + source_result_id: "result-11".to_string(), + source_job_id: "job-11".to_string(), + verdict: "pass".to_string(), + summary: "exact chain verified".to_string(), + evidence_ids: Vec::new(), + risks: Vec::new(), + emitted_at: "2026-07-13T00:00:00Z".to_string(), + contract: "test".to_string(), + verification_request_id: "verification-request-11".to_string(), + frontier_route_id: "frontier-route-11".to_string(), + }; + commit_state_with_mind_witness( + &store, + "thread-11", + &state, + &state, + &review, + &commit, + &[EpiphanyAcceptancePrerequisite::Soul(soul.clone())], + )?; + let cache = coordinator_acceptance_cache(&store)?; + let stored = cache.get_required::("soul-11")?; + assert_eq!(stored, soul); + assert_eq!(stored.verification_request_id, "verification-request-11"); + assert_eq!(stored.frontier_route_id, "frontier-route-11"); + Ok(()) + } + + #[test] + fn split_model_admission_retry_reuses_review_and_commits_fresh_thread_acceptance() + -> anyhow::Result<()> { + let temp = tempfile::tempdir()?; + let store = temp.path().join("split-model-retry.cc"); + crate::initialize_runtime_spine( + &store, + crate::RuntimeSpineInitOptions { + runtime_id: "split-model-retry".to_string(), + display_name: "Split model retry".to_string(), + created_at: "2026-07-13T09:00:00Z".to_string(), + }, + )?; + let existing = RepoModelAdmissionReview { + schema_version: REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION.to_string(), + review_id: "repo-model-review-result-split".to_string(), + result_id: "result-split".to_string(), + job_id: "job-split".to_string(), + patch_id: "patch-split".to_string(), + patch_sha256: "a".repeat(64), + base_revision: 4, + base_hash: "b".repeat(64), + decision: MindGatewayDecision::Accept, + evidence_ids: vec!["evidence-split".to_string()], + reviewed_at: "2026-07-13T09:00:01Z".to_string(), + contract: REPO_MODEL_ADMISSION_CONTRACT.to_string(), + }; + let mut cache = coordinator_acceptance_cache(&store)?; + cache.put(&existing.review_id, &existing)?; + cache.put( + "repo-model-admission-repo-model-review-result-split", + &crate::RepoModelAdmissionReceipt { + schema_version: crate::REPO_MODEL_ADMISSION_RECEIPT_SCHEMA_VERSION.to_string(), + receipt_id: "repo-model-admission-repo-model-review-result-split".to_string(), + review_id: existing.review_id.clone(), + result_id: existing.result_id.clone(), + patch_id: existing.patch_id.clone(), + patch_sha256: existing.patch_sha256.clone(), + previous_revision: 4, + previous_hash: existing.base_hash.clone(), + admitted_revision: 5, + admitted_hash: "c".repeat(64), + admitted_at: existing.reviewed_at.clone(), + contract: REPO_MODEL_ADMISSION_CONTRACT.to_string(), + purpose: crate::RepoModelPatchPurpose::Evolution, + frontier_route_id: String::new(), + verification_request_id: String::new(), + soul_verdict_receipt_id: String::new(), + frontier_modeling_request_id: String::new(), + }, + )?; + let mut fresh = existing.clone(); + fresh.reviewed_at = "2026-07-13T09:05:00Z".to_string(); + assert_eq!(stable_repo_model_admission_review(&store, fresh)?, existing); + + let state = EpiphanyThreadState::default(); + let next = EpiphanyThreadState { + revision: 1, + acceptance_receipts: vec![epiphany_state_model::EpiphanyAcceptanceReceipt { + id: "accept-modeling-fresh-nonce".to_string(), + result_id: "result-split".to_string(), + job_id: "job-split".to_string(), + binding_id: "modeling-worker".to_string(), + surface: "roleAccept".to_string(), + role_id: "modeling".to_string(), + status: "accepted".to_string(), + accepted_at: "2026-07-13T09:05:00Z".to_string(), + ..Default::default() + }], + ..Default::default() + }; + let mind = MindGatewayReview { + schema_version: MIND_GATEWAY_REVIEW_SCHEMA_VERSION.to_string(), + gateway_id: "mind-fresh-nonce".to_string(), + source_kind: "role".to_string(), + source_role_id: "modeling".to_string(), + decision: MindGatewayDecision::Accept, + allowed_effects: vec!["state".to_string()], + refused_effects: Vec::new(), + reasons: Vec::new(), + contract: "fresh retry".to_string(), + }; + let commit = mind_state_commit_receipt( + "mind-commit-fresh-nonce".to_string(), + &mind, + next.revision, + vec!["AcceptanceReceipts".to_string()], + "2026-07-13T09:05:00Z".to_string(), + ); + commit_state_with_mind_witness(&store, "thread-split", &state, &next, &mind, &commit, &[])?; + let cache = coordinator_acceptance_cache(&store)?; + assert_eq!( + cache + .get_required::(THREAD_STATE_KEY)? + .state()? + .acceptance_receipts[0] + .id, + "accept-modeling-fresh-nonce" + ); + assert_eq!( + cache.get_all::()?.len(), + 1 + ); + Ok(()) + } } diff --git a/epiphany-core/src/coordinator_launch_context.rs b/epiphany-core/src/coordinator_launch_context.rs index 38e854b9..d6da10c9 100644 --- a/epiphany-core/src/coordinator_launch_context.rs +++ b/epiphany-core/src/coordinator_launch_context.rs @@ -5,15 +5,14 @@ use crate::EpiphanyMemoryContextPacket; use crate::EpiphanyMemoryContextQuery; use crate::EpiphanyMemoryProfile; use crate::EpiphanyPromptContextInput; -use crate::RepoMemoryGraphRefresh; use crate::RuntimeHandsReceiptChainSummary; +use crate::ensure_runtime_repo_model; use crate::load_epiphany_cultmesh_cluster_topology; use crate::load_epiphany_cultmesh_status; use crate::load_latest_epiphany_cultmesh_work_loop_telemetry; -use crate::load_memory_graph_snapshot; +use crate::memory_graph_from_epiphany_graphs; use crate::plan_memory_graph_context_cut; use crate::query_epiphany_local_verse_context; -use crate::refresh_or_validate_repo_memory_graph; use crate::render_epiphany_prompt_context; use crate::runtime_hands_command_receipt; use crate::runtime_hands_commit_receipt; @@ -138,6 +137,16 @@ pub fn append_verification_hands_receipt_context( else { return Ok(context); }; + let verification_request = crate::commit_repo_frontier_verification_request_for_chain( + runtime_store_path, + &chain, + &Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true), + ) + .map_err(|error| format!("failed to commit Soul frontier verification request: {error}"))?; + let route = + crate::runtime_repo_frontier_route(runtime_store_path, &verification_request.route_id) + .map_err(|error| format!("failed to reload Soul frontier route: {error}"))? + .ok_or_else(|| "Soul frontier route disappeared after request commit".to_string())?; let telemetry = work_loop_telemetry_from_hands_chain( &chain, runtime_store_path, @@ -155,6 +164,14 @@ pub fn append_verification_hands_receipt_context( }, )?; context.push_str("\n\n\n"); + context.push_str(&format!( + "verificationRequestId: {}\nfrontierRouteId: {}\nfrontierItemId: {}\nrouteQuestion: {}\nrouteGap: {}\n", + verification_request.request_id, + route.route_id, + route.frontier_item_id, + route.question, + route.gap, + )); context.push_str( "Soul is reviewing typed internal CultMesh telemetry for concrete Hands consequence evidence produced after the latest accepted Verification finding.\n", ); @@ -227,6 +244,42 @@ pub fn append_modeling_work_loop_telemetry_context( runtime_store_path: &Path, state: &EpiphanyThreadState, ) -> Result { + let Some(accepted_verification) = latest_unique_accepted_verification(state)? else { + return Ok(context); + }; + let modeling_request = + crate::commit_repo_frontier_modeling_request(runtime_store_path, &accepted_verification) + .map_err(|error| { + format!("failed to commit typed frontier Modeling request: {error}") + })?; + let route = crate::runtime_repo_frontier_route(runtime_store_path, &modeling_request.route_id) + .map_err(|error| format!("failed to reload Modeling frontier route: {error}"))? + .ok_or_else(|| "Modeling frontier route disappeared after request commit".to_string())?; + let disposition = match modeling_request.allowed_disposition { + crate::RepoFrontierVerdictDisposition::Resolved => "resolved", + crate::RepoFrontierVerdictDisposition::Blocked => "blocked", + }; + context.push_str("\n\n\n"); + context.push_str(&format!( + "modelingRequestId: {}\nmodelRevision: {}\nmodelHash: {}\nfrontierRouteId: {}\nfrontierItemId: {}\nfrontierItemHash: {}\nverificationRequestId: {}\nsoulVerdictReceiptId: {}\nverificationResultId: {}\nverificationJobId: {}\nverificationAcceptanceReceiptId: {}\nallowedDisposition: {}\nrouteQuestion: {}\nrouteGap: {}\n", + modeling_request.request_id, + modeling_request.model_revision, + modeling_request.model_hash, + modeling_request.route_id, + modeling_request.frontier_item_id, + modeling_request.frontier_item_hash, + modeling_request.verification_request_id, + modeling_request.soul_verdict_receipt_id, + modeling_request.verification_result_id, + modeling_request.verification_job_id, + modeling_request.verification_acceptance_receipt_id, + disposition, + route.question, + route.gap, + )); + context.push_str("Echo repoFrontierModelingRequestId exactly. Use purpose kind incorporate_frontier_verdict with the exact frontierRouteId and soulVerdictReceiptId. Revise only the routed frontier item to allowedDisposition; this request grants no other model mutation.\n"); + context.push_str(""); + let store = local_verse_store_path(runtime_store_path); let Some(mut telemetry) = load_latest_epiphany_cultmesh_work_loop_telemetry(&store, EPIPHANY_LOCAL_VERSE_RUNTIME_ID) @@ -287,6 +340,35 @@ pub fn append_modeling_work_loop_telemetry_context( Ok(context) } +fn latest_unique_accepted_verification( + state: &EpiphanyThreadState, +) -> Result, String> { + let accepted = state + .acceptance_receipts + .iter() + .filter(|receipt| { + receipt.role_id == "verification" + && receipt.surface == "roleAccept" + && receipt.status == "accepted" + }) + .collect::>(); + let Some(latest_at) = accepted + .iter() + .map(|receipt| receipt.accepted_at.as_str()) + .max() + else { + return Ok(None); + }; + let latest = accepted + .into_iter() + .filter(|receipt| receipt.accepted_at == latest_at) + .collect::>(); + if latest.len() != 1 { + return Err("latest accepted Verification result is temporally ambiguous; refusing Modeling substitution".to_string()); + } + Ok(Some(latest[0].clone())) +} + fn latest_accepted_verification_timestamp(state: &EpiphanyThreadState) -> Option<&str> { state .acceptance_receipts @@ -640,37 +722,21 @@ fn launch_memory_context( .unwrap_or_else(|_| runtime_store_path.to_path_buf()) .to_string_lossy() .into_owned(); - let stored = load_memory_graph_snapshot(&memory_graph_store).map_err(|error| { - format!( - "failed to validate memory graph store {}: {error}", - memory_graph_store.display() - ) - })?; - let canonical = stored.as_ref().is_some_and(|snapshot| { - snapshot.model_revision > 0 - || !snapshot.model_hash.is_empty() - || !snapshot.frontier.is_empty() - }); - let (snapshot, refresh) = if canonical { - ( - stored.expect("canonical snapshot was present"), - RepoMemoryGraphRefresh::Reused, - ) - } else { - refresh_or_validate_repo_memory_graph( - &memory_graph_store, - &source_identity, - state, - repo_root, - format!("bridge-launch-state-rev-{}", state.revision), - ) - .map_err(|error| { - format!( - "failed to bootstrap memory graph store {}: {error}", - memory_graph_store.display() - ) - })? - }; + let bootstrap = memory_graph_from_epiphany_graphs( + format!("bridge-launch-state-rev-{}", state.revision), + &state.graphs, + &source_identity, + state.revision, + repo_root, + ) + .map_err(|error| format!("failed to derive initial runtime RepoModel: {error}"))?; + let (snapshot, migration) = ensure_runtime_repo_model( + runtime_store_path, + &memory_graph_store, + &bootstrap, + &Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true), + ) + .map_err(|error| format!("failed to load runtime-spine RepoModel: {error}"))?; let mut packet = plan_memory_graph_context_cut( &snapshot, @@ -682,12 +748,10 @@ fn launch_memory_context( ..Default::default() }, ); - if refresh == RepoMemoryGraphRefresh::Refreshed { - packet.warnings.push(format!( - "Memory graph store was refreshed from current thread-state repo graph at {}.", - memory_graph_store.display() - )); - } + packet.warnings.push(format!( + "RepoModel is runtime-spine authority via migration receipt {} (source {}).", + migration.receipt_id, migration.source_store + )); if packet.nodes.is_empty() && packet.summaries.is_empty() { packet.warnings.push( "Memory graph context is empty for this launch focus; the accepted repo graph may be thin or stale.".to_string(), @@ -718,6 +782,7 @@ mod tests { use crate::hands_commit_receipt_for_review; use crate::hands_patch_receipt_for_review; use crate::initialize_runtime_spine; + use crate::load_memory_graph_snapshot; use crate::memory_graph_model_hash; use crate::open_runtime_spine_heartbeat_job; use crate::put_hands_action_intent; @@ -729,9 +794,131 @@ mod tests { use crate::seed_epiphany_local_verse_context; use crate::write_memory_graph_snapshot; use epiphany_state_model::EpiphanyAcceptanceReceipt; + use sha2::Digest; use std::fs; use uuid::Uuid; + fn admit_and_authorize_context_hands( + store: &Path, + intent: &HandsActionIntent, + review: &crate::HandsActionReview, + ) -> anyhow::Result<()> { + let bootstrap = EpiphanyMemoryGraphSnapshot { + schema_version: Some(MEMORY_GRAPH_SCHEMA_VERSION.to_string()), + graph_id: "context-runtime-model".to_string(), + domains: vec![EpiphanyMemoryDomain { + id: "repo".to_string(), + profile: crate::EpiphanyMemoryProfile::RepoArchitecture, + title: "Repository".to_string(), + lifecycle: EpiphanyMemoryLifecycle::Accepted, + ..Default::default() + }], + nodes: vec![EpiphanyMemoryNode { + id: "claim-context-hands".to_string(), + domain_id: "repo".to_string(), + profile: crate::EpiphanyMemoryProfile::RepoArchitecture, + kind: EpiphanyMemoryNodeKind::RuntimeContract, + title: "Context Hands".to_string(), + claim: "Verification consumes a routed Hands chain.".to_string(), + question: "Is the chain exact?".to_string(), + action_implication: "Route the exact source scope.".to_string(), + source_hashes: vec!["anchor:missing".to_string()], + lifecycle: EpiphanyMemoryLifecycle::Accepted, + ..Default::default() + }], + ..Default::default() + }; + let legacy = store.with_extension("context-legacy.msgpack"); + let (current, _) = + crate::ensure_runtime_repo_model(store, legacy, &bootstrap, "2026-06-12T00:00:00Z")?; + let patch = crate::RepoModelPatch { + patch_id: "context-route-patch".to_string(), + base_revision: current.model_revision, + base_hash: memory_graph_model_hash(¤t)?, + applied_at: "2026-06-12T00:00:03Z".to_string(), + purpose: crate::RepoModelPatchPurpose::Evolution, + operations: vec![crate::RepoModelPatchOperation::UpsertFrontier { + item: RepoFrontierItem { + id: "context-hands-frontier".to_string(), + migration_body: "Verify the routed Hands chain.".to_string(), + question: "Does verification see the exact receipts?".to_string(), + gap: "Unrouted consequences are invalid.".to_string(), + target_claim_ids: vec!["claim-context-hands".to_string()], + source_scope: intent.requested_paths.clone(), + recommended_next_organ: "Hands".to_string(), + status: RepoFrontierStatus::Active, + ..Default::default() + }, + }], + }; + let patch_bytes = rmp_serde::to_vec_named(&patch)?; + let result = crate::EpiphanyRuntimeRoleWorkerResult { + schema_version: crate::RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION.to_string(), + result_id: "context-route-result".to_string(), + job_id: "context-route-job".to_string(), + role_id: "modeling".to_string(), + verdict: "checkpoint-ready".to_string(), + summary: "Context route".to_string(), + next_safe_move: "Mind admission".to_string(), + checkpoint_summary: None, + scratch_summary: None, + files_inspected: vec!["epiphany-core/src/runtime_spine.rs".to_string()], + frontier_node_ids: vec!["claim-context-hands".to_string()], + evidence_ids: vec!["context-route-evidence".to_string()], + 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: Some(patch_bytes.clone()), + verification_request_id: None, + frontier_route_id: None, + repo_frontier_modeling_request_id: None, + }; + crate::put_runtime_role_worker_result(store, &result)?; + crate::commit_repo_model_admission( + store, + &result.result_id, + &crate::RepoModelAdmissionReview { + schema_version: crate::REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION.to_string(), + review_id: "context-route-admission".to_string(), + result_id: result.result_id.clone(), + job_id: result.job_id.clone(), + patch_id: patch.patch_id, + patch_sha256: format!("{:x}", sha2::Sha256::digest(&patch_bytes)), + base_revision: patch.base_revision, + base_hash: patch.base_hash, + decision: crate::MindGatewayDecision::Accept, + evidence_ids: result.evidence_ids.clone(), + reviewed_at: "2026-06-12T00:00:04Z".to_string(), + contract: crate::REPO_MODEL_ADMISSION_CONTRACT.to_string(), + }, + )?; + let route = crate::select_and_commit_repo_frontier_route(store, "2026-06-12T00:00:05Z")?; + crate::put_repo_frontier_hands_authority( + store, + &crate::RepoFrontierHandsAuthority { + schema_version: crate::REPO_FRONTIER_HANDS_AUTHORITY_SCHEMA_VERSION.to_string(), + authority_id: "context-route-authority".to_string(), + route_id: route.route_id, + model_revision: route.model_revision, + model_hash: route.model_hash, + frontier_item_id: route.frontier_item_id, + frontier_item_hash: route.frontier_item_hash, + hands_intent_id: intent.intent_id.clone(), + hands_review_id: review.review_id.clone(), + substrate_grant_receipt_id: intent.substrate_gate_grant_receipt_id.clone(), + requested_paths: intent.requested_paths.clone(), + granted_at: "2026-06-12T00:00:06Z".to_string(), + contract: crate::REPO_FRONTIER_HANDS_AUTHORITY_CONTRACT.to_string(), + }, + )?; + Ok(()) + } + #[test] fn native_launch_context_has_no_bridge_or_host_dependency() { let source = include_str!("coordinator_launch_context.rs"); @@ -856,7 +1043,7 @@ mod tests { assert!(rendered.contains("Yggdrasil")); assert!(rendered.contains("Memory graph")); assert!(local_verse_store_path(&runtime_store).exists()); - assert!(memory_graph_store_path(&runtime_store).exists()); + assert!(runtime_store.exists()); let launch_request = build_epiphany_role_launch_request_with_dynamic_context( "thread-1", @@ -927,6 +1114,47 @@ mod tests { Ok(()) } + #[test] + fn ordinary_modeling_has_no_verdict_request_and_tied_acceptance_refuses_substitution() { + let missing_store = Path::new("definitely-missing-runtime.cc"); + let ordinary = EpiphanyThreadState::default(); + assert_eq!( + append_modeling_work_loop_telemetry_context( + "ordinary".to_string(), + missing_store, + &ordinary, + ) + .expect("ordinary Modeling does not touch verdict authority"), + "ordinary" + ); + + let receipt = |id: &str, result: &str| EpiphanyAcceptanceReceipt { + id: id.to_string(), + result_id: result.to_string(), + job_id: format!("job-{result}"), + binding_id: "verification-review-worker".to_string(), + surface: "roleAccept".to_string(), + role_id: "verification".to_string(), + status: "accepted".to_string(), + accepted_at: "2026-07-13T07:00:00Z".to_string(), + ..Default::default() + }; + let ambiguous = EpiphanyThreadState { + acceptance_receipts: vec![ + receipt("accept-a", "result-a"), + receipt("accept-b", "result-b"), + ], + ..Default::default() + }; + let error = append_modeling_work_loop_telemetry_context( + "ambiguous".to_string(), + missing_store, + &ambiguous, + ) + .expect_err("equal-time accepted results must not be selected by adjacency"); + assert!(error.contains("temporally ambiguous")); + } + #[test] fn verification_launch_context_includes_hands_receipt_chain() -> anyhow::Result<()> { let temp = std::env::temp_dir().join(format!("epiphany-hands-context-{}", Uuid::new_v4())); @@ -952,7 +1180,7 @@ mod tests { role: "epiphany-hands".to_string(), authority_scope: "epiphany.role.implementation".to_string(), requested_action: "continueImplementation".to_string(), - requested_paths: vec![".".to_string()], + requested_paths: vec!["epiphany-core/src/runtime_spine.rs".to_string()], 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(), @@ -962,7 +1190,7 @@ mod tests { &crate::substrate_gate_coordinator_implementation_grant( "substrate-grant-context".to_string(), "hands-job-context".to_string(), - vec![".".to_string()], + vec!["epiphany-core/src/runtime_spine.rs".to_string()], "2026-06-12T00:00:00Z".to_string(), ), )?; @@ -980,6 +1208,7 @@ mod tests { "2026-06-12T00:00:02Z".to_string(), ); put_hands_action_review(&runtime_store, &review)?; + admit_and_authorize_context_hands(&runtime_store, &intent, &review)?; let patch = hands_patch_receipt_for_review( "hands-patch-context".to_string(), &intent, @@ -1042,6 +1271,10 @@ mod tests { assert!(context.contains("hands-patch-context")); assert!(context.contains("hands-command-context")); assert!(context.contains("hands-commit-context")); + assert!(context.contains("verificationRequestId:")); + assert!(context.contains("frontierRouteId:")); + assert!(context.contains("Does verification see the exact receipts?")); + assert!(context.contains("Unrouted consequences are invalid.")); assert!(context.contains("resolvedReceiptPayloads")); assert!(context.contains("resolvedReceiptPayloadPreviews")); assert!(context.contains("test result: ok")); @@ -1086,6 +1319,65 @@ mod tests { assertion.contains("verification_launch_context_includes_hands_receipt_chain") })); + let verification_request_id = context + .lines() + .find_map(|line| line.strip_prefix("verificationRequestId: ")) + .expect("rendered exact verification request id"); + let verification_request = crate::runtime_repo_frontier_verification_request( + &runtime_store, + verification_request_id, + )? + .expect("persisted exact verification request"); + let verification_result = crate::EpiphanyRuntimeRoleWorkerResult { + schema_version: crate::RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION.to_string(), + result_id: "result-verification-context".to_string(), + job_id: "verification-job-context".to_string(), + role_id: "verification".to_string(), + verdict: "pass".to_string(), + summary: "accepted prior verification".to_string(), + next_safe_move: "Model the verified consequence.".to_string(), + checkpoint_summary: None, + scratch_summary: None, + files_inspected: Vec::new(), + frontier_node_ids: Vec::new(), + evidence_ids: vec!["verification-proof-context".to_string()], + 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: Some(verification_request.request_id.clone()), + frontier_route_id: Some(verification_request.route_id.clone()), + repo_frontier_modeling_request_id: None, + }; + crate::put_runtime_role_worker_result(&runtime_store, &verification_result)?; + crate::put_soul_verdict_receipt( + &runtime_store, + &crate::SoulVerdictReceipt { + schema_version: crate::SOUL_VERDICT_RECEIPT_SCHEMA_VERSION.to_string(), + receipt_id: "soul-verdict-context".to_string(), + source_result_id: verification_result.result_id.clone(), + source_job_id: verification_result.job_id.clone(), + verdict: verification_result.verdict.clone(), + summary: verification_result.summary.clone(), + evidence_ids: verification_result.evidence_ids.clone(), + risks: verification_result.risks.clone(), + emitted_at: "2026-06-12T00:00:07Z".to_string(), + contract: "accepted exact verification".to_string(), + verification_request_id: verification_request.request_id.clone(), + frontier_route_id: verification_request.route_id.clone(), + }, + )?; + let mut state_cache = crate::runtime_spine_cache(&runtime_store)?; + state_cache.put( + crate::THREAD_STATE_KEY, + &crate::EpiphanyThreadStateEntry::from_state("context-thread", &state)?, + )?; + let modeling_context = append_modeling_work_loop_telemetry_context( "".to_string(), &runtime_store, @@ -1096,6 +1388,14 @@ mod tests { assert!(modeling_context.contains("hands-patch-context")); assert!(modeling_context.contains("accept-verification-context")); assert!(modeling_context.contains("Soul acceptance telemetry")); + assert!(modeling_context.contains("")); + assert!(modeling_context.contains("soulVerdictReceiptId: soul-verdict-context")); + assert!(modeling_context.contains("verificationResultId: result-verification-context")); + assert!( + modeling_context + .contains("verificationAcceptanceReceiptId: accept-verification-context") + ); + assert!(modeling_context.contains("allowedDisposition: resolved")); let telemetry = load_latest_epiphany_cultmesh_work_loop_telemetry( local_verse_store_path(&runtime_store), EPIPHANY_LOCAL_VERSE_RUNTIME_ID, diff --git a/epiphany-core/src/coordinator_results.rs b/epiphany-core/src/coordinator_results.rs index 505a9398..84e01bd5 100644 --- a/epiphany-core/src/coordinator_results.rs +++ b/epiphany-core/src/coordinator_results.rs @@ -300,10 +300,13 @@ fn interpret_role_lifecycle_failure( evidence_gaps: Vec::new(), risks: Vec::new(), state_patch: None, + repo_model_patch: None, self_patch: None, self_persistence: None, job_error: Some(result.summary.clone()), item_error: None, + verification_request_id: None, + frontier_route_id: None, } } diff --git a/epiphany-core/src/eyes_gateway.rs b/epiphany-core/src/eyes_gateway.rs index c9bb01a5..0b9b6403 100644 --- a/epiphany-core/src/eyes_gateway.rs +++ b/epiphany-core/src/eyes_gateway.rs @@ -246,10 +246,13 @@ mod tests { evidence_gaps: Vec::new(), risks: Vec::new(), state_patch: None, + repo_model_patch: None, self_patch: None, self_persistence: None, job_error: None, item_error: None, + verification_request_id: None, + frontier_route_id: None, }; let mut patch = EpiphanyRoleStatePatchDocument::default(); patch diff --git a/epiphany-core/src/lib.rs b/epiphany-core/src/lib.rs index 1356ae39..d81a49fd 100644 --- a/epiphany-core/src/lib.rs +++ b/epiphany-core/src/lib.rs @@ -25,6 +25,7 @@ mod process_observation; mod promotion; mod prompt_context; mod proposal; +mod repo_model_gateway; mod retrieval; mod runtime_spine; mod soul_gateway; @@ -622,9 +623,10 @@ pub use memory_graph::RepoFrontierStatus; pub use memory_graph::RepoMemoryGraphRefresh; pub use memory_graph::RepoModelPatch; pub use memory_graph::RepoModelPatchOperation; -pub use memory_graph::apply_repo_model_patch; +pub use memory_graph::RepoModelPatchPurpose; pub use memory_graph::compose_memory_graph_snapshots; pub use memory_graph::derive_memory_graph_freshness; +pub use memory_graph::derive_repo_model_patch; pub use memory_graph::lifecycle_allowed_for_profile; pub use memory_graph::load_memory_graph_entry; pub use memory_graph::load_memory_graph_snapshot; @@ -743,6 +745,7 @@ pub use prompt_context::render_epiphany_prompt_context; pub use proposal::EpiphanyMapProposal; pub use proposal::EpiphanyMapProposalInput; pub use proposal::propose_map_update; +pub use repo_model_gateway::*; pub use retrieval::EPIPHANY_RETRIEVAL_DEFAULT_LIMIT; pub use retrieval::EPIPHANY_RETRIEVAL_MAX_LIMIT; pub use retrieval::EpiphanyRetrieveQuery; @@ -800,11 +803,15 @@ pub use runtime_spine::advance_repo_work_modeling_route; pub use runtime_spine::append_runtime_event; pub use runtime_spine::clear_epiphany_job_binding_backend; pub use runtime_spine::commit_initial_repo_work_modeling_route; +pub use runtime_spine::commit_repo_frontier_modeling_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_work_hands_grant; pub use runtime_spine::commit_repo_work_map_admission; pub use runtime_spine::complete_runtime_job; pub use runtime_spine::create_runtime_job; pub use runtime_spine::create_runtime_session; +pub use runtime_spine::ensure_runtime_repo_model; pub use runtime_spine::ensure_runtime_session; pub use runtime_spine::initialize_runtime_spine; pub use runtime_spine::open_runtime_spine_heartbeat_job; @@ -822,6 +829,8 @@ pub use runtime_spine::put_hands_patch_receipt; pub use runtime_spine::put_hands_pr_receipt; pub use runtime_spine::put_mind_gateway_review; pub use runtime_spine::put_mind_state_commit_receipt; +pub use runtime_spine::put_repo_frontier_hands_authority; +pub use runtime_spine::put_repo_frontier_verification_request; pub use runtime_spine::put_repo_work_modeling_finding; pub use runtime_spine::put_repo_work_modeling_request; pub use runtime_spine::put_repo_work_plan_adoption_review; @@ -839,6 +848,7 @@ pub use runtime_spine::runtime_hands_commit_receipt; pub use runtime_spine::runtime_hands_patch_receipt; pub use runtime_spine::runtime_hands_pr_receipt; pub use runtime_spine::runtime_hands_receipt_chain_after; +pub use runtime_spine::runtime_has_actionable_hands_frontier; pub use runtime_spine::runtime_hello_frame; pub use runtime_spine::runtime_job_snapshot; pub use runtime_spine::runtime_latest_hands_receipt_chain_after; @@ -846,6 +856,8 @@ pub use runtime_spine::runtime_mind_gateway_review; pub use runtime_spine::runtime_mind_state_commit_receipt; pub use runtime_spine::runtime_registered_document_types; pub use runtime_spine::runtime_reorient_worker_result; +pub use runtime_spine::runtime_repo_frontier_route; +pub use runtime_spine::runtime_repo_frontier_verification_request; pub use runtime_spine::runtime_repo_work_hands_grant; pub use runtime_spine::runtime_repo_work_map_entry; pub use runtime_spine::runtime_repo_work_modeling_finding; @@ -860,6 +872,7 @@ pub use runtime_spine::runtime_spine_status; pub use runtime_spine::runtime_substrate_gate_repo_access_grant_receipt; pub use runtime_spine::runtime_tool_invocation_statuses; pub use runtime_spine::runtime_worker_launch_request; +pub use runtime_spine::select_and_commit_repo_frontier_route; pub use runtime_spine::validate_hands_action_authority; pub use runtime_spine::write_runtime_hello_frame; pub use runtime_spine::write_runtime_schema_catalog_json; diff --git a/epiphany-core/src/memory_graph/context_cut.rs b/epiphany-core/src/memory_graph/context_cut.rs index f1babdee..04cad917 100644 --- a/epiphany-core/src/memory_graph/context_cut.rs +++ b/epiphany-core/src/memory_graph/context_cut.rs @@ -12,6 +12,7 @@ use super::RepoFrontierStatus; use super::derive_memory_graph_freshness; use super::ids::normalized_key; use super::ids::stable_memory_graph_id; +use super::memory_graph_model_hash; use bm25::Document; use bm25::Language; use bm25::SearchEngineBuilder; @@ -162,6 +163,8 @@ pub fn plan_memory_graph_context_cut( EpiphanyMemoryContextPacket { id: stable_memory_graph_id("memctx", [query.id.as_str(), snapshot.graph_id.as_str()]), query_id: query.id.clone(), + repo_model_revision: snapshot.model_revision, + repo_model_hash: memory_graph_model_hash(snapshot).unwrap_or_default(), nodes, edges, summaries, diff --git a/epiphany-core/src/memory_graph/mod.rs b/epiphany-core/src/memory_graph/mod.rs index 3dfd917d..ca4f8646 100644 --- a/epiphany-core/src/memory_graph/mod.rs +++ b/epiphany-core/src/memory_graph/mod.rs @@ -28,6 +28,7 @@ pub use epiphany_state_model::RepoFrontierItem; pub use epiphany_state_model::RepoFrontierStatus; pub use epiphany_state_model::RepoModelPatch; pub use epiphany_state_model::RepoModelPatchOperation; +pub use epiphany_state_model::RepoModelPatchPurpose; pub use compose::compose_memory_graph_snapshots; pub use context_cut::plan_memory_graph_context_cut; @@ -44,7 +45,9 @@ pub use store::EpiphanyMemoryGraphEntry; pub use store::MEMORY_GRAPH_KEY; pub use store::MEMORY_GRAPH_SCHEMA_VERSION; pub use store::MEMORY_GRAPH_TYPE; -pub use store::apply_repo_model_patch; +#[cfg(test)] +pub(crate) use store::apply_repo_model_patch; +pub use store::derive_repo_model_patch; pub use store::load_memory_graph_entry; pub use store::load_memory_graph_snapshot; pub use store::memory_graph_cache; diff --git a/epiphany-core/src/memory_graph/profiles/repo.rs b/epiphany-core/src/memory_graph/profiles/repo.rs index 31c4e972..f2bfd3e7 100644 --- a/epiphany-core/src/memory_graph/profiles/repo.rs +++ b/epiphany-core/src/memory_graph/profiles/repo.rs @@ -804,6 +804,7 @@ mod tests { }); ready.wait(); let patch = crate::memory_graph::RepoModelPatch { + purpose: crate::memory_graph::RepoModelPatchPurpose::Evolution, patch_id: "canonical-wins-refresh-race".to_string(), base_revision: 0, base_hash: crate::memory_graph::memory_graph_model_hash(&legacy)?, diff --git a/epiphany-core/src/memory_graph/store.rs b/epiphany-core/src/memory_graph/store.rs index 7420ad30..96c7cc36 100644 --- a/epiphany-core/src/memory_graph/store.rs +++ b/epiphany-core/src/memory_graph/store.rs @@ -158,7 +158,8 @@ pub fn memory_graph_model_hash(snapshot: &EpiphanyMemoryGraphSnapshot) -> Result /// Applies a Modeling patch as one stale-checked replacement of the existing /// CultCache aggregate. Validation occurs before the store is touched. -pub fn apply_repo_model_patch( +#[cfg(test)] +pub(crate) fn apply_repo_model_patch( store_path: impl AsRef, patch: &RepoModelPatch, ) -> Result { @@ -184,7 +185,34 @@ pub fn apply_repo_model_patch( let entry: EpiphanyMemoryGraphEntry = rmp_serde::from_slice(&envelope.payload)?; validate_memory_graph_entry(&entry)?; let current = entry.snapshot()?; - let current_hash = memory_graph_model_hash(¤t)?; + let next = derive_repo_model_patch(¤t, patch)?; + let next_entry = EpiphanyMemoryGraphEntry::from_snapshot(&next)?; + let next_envelope = memory_graph_envelope(&next_entry, &patch.applied_at)?; + if !backing.compare_and_swap_entry(&envelope, next_envelope)? { + return Err(anyhow!( + "stale repo model patch: aggregate changed during compare-and-swap" + )); + } + Ok(next) +} + +/// Purely derives and validates the next canonical aggregate. Persistence and +/// admission authority remain the caller's responsibility. +pub fn derive_repo_model_patch( + current: &EpiphanyMemoryGraphSnapshot, + patch: &RepoModelPatch, +) -> Result { + if patch.patch_id.trim().is_empty() || patch.applied_at.trim().is_empty() { + return Err(anyhow!("repo model patch requires patch_id and applied_at")); + } + if patch.operations.is_empty() { + return Err(anyhow!( + "repo model patch must contain at least one operation" + )); + } + chrono::DateTime::parse_from_rfc3339(&patch.applied_at) + .map_err(|_| anyhow!("repo model patch applied_at must be RFC3339"))?; + let current_hash = memory_graph_model_hash(current)?; if patch.base_revision != current.model_revision || patch.base_hash != current_hash { return Err(anyhow!( "stale repo model patch: base revision/hash {}/{} does not match current {}/{}", @@ -194,7 +222,7 @@ pub fn apply_repo_model_patch( current_hash )); } - let mut next = current; + let mut next = current.clone(); if next .lifecycle_receipts .iter() @@ -253,14 +281,7 @@ pub fn apply_repo_model_patch( )); } next.model_hash = memory_graph_model_hash(&next)?; - let next_entry = EpiphanyMemoryGraphEntry::from_snapshot(&next)?; - validate_memory_graph_entry(&next_entry)?; - let next_envelope = memory_graph_envelope(&next_entry, &patch.applied_at)?; - if !backing.compare_and_swap_entry(&envelope, next_envelope)? { - return Err(anyhow!( - "stale repo model patch: aggregate changed during compare-and-swap" - )); - } + validate_memory_graph_entry(&EpiphanyMemoryGraphEntry::from_snapshot(&next)?)?; Ok(next) } diff --git a/epiphany-core/src/memory_graph/tests.rs b/epiphany-core/src/memory_graph/tests.rs index 93a32bd9..5bb7c308 100644 --- a/epiphany-core/src/memory_graph/tests.rs +++ b/epiphany-core/src/memory_graph/tests.rs @@ -169,6 +169,7 @@ fn patch_for( operations: Vec, ) -> RepoModelPatch { RepoModelPatch { + purpose: RepoModelPatchPurpose::Evolution, patch_id: "patch-1".to_string(), base_revision: snapshot.model_revision, base_hash: memory_graph_model_hash(snapshot).expect("model hash"), @@ -872,6 +873,7 @@ fn repo_model_patch_cross_process_worker() -> Result<()> { std::thread::sleep(std::time::Duration::from_millis(10)); } let patch = RepoModelPatch { + purpose: RepoModelPatchPurpose::Evolution, patch_id: format!("cross-process-{id}"), base_revision: 0, base_hash, diff --git a/epiphany-core/src/mind_gateway.rs b/epiphany-core/src/mind_gateway.rs index 711bfc81..cf428b13 100644 --- a/epiphany-core/src/mind_gateway.rs +++ b/epiphany-core/src/mind_gateway.rs @@ -604,10 +604,13 @@ mod tests { evidence_gaps: Vec::new(), risks: Vec::new(), state_patch: None, + repo_model_patch: None, self_patch: None, self_persistence: None, job_error: None, item_error: None, + verification_request_id: None, + frontier_route_id: None, } } diff --git a/epiphany-core/src/prompt_context.rs b/epiphany-core/src/prompt_context.rs index 06fe5ede..b9d9474c 100644 --- a/epiphany-core/src/prompt_context.rs +++ b/epiphany-core/src/prompt_context.rs @@ -297,6 +297,10 @@ pub fn render_epiphany_prompt_context(input: &EpiphanyPromptContextInput) -> Str "- Packet `{}` from query `{}`", input.memory_context.id, input.memory_context.query_id )); + lines.push(format!( + "- Canonical RepoModel: revision={}, hash=`{}`", + input.memory_context.repo_model_revision, input.memory_context.repo_model_hash + )); for item in input.memory_context.frontier.iter().take(6) { lines.push(format!( diff --git a/epiphany-core/src/prompts/epiphany_specialists.toml b/epiphany-core/src/prompts/epiphany_specialists.toml index 367caa99..6ec83aed 100644 --- a/epiphany-core/src/prompts/epiphany_specialists.toml +++ b/epiphany-core/src/prompts/epiphany_specialists.toml @@ -88,9 +88,9 @@ In the work loop, Modeling follows Soul: after Hands changes the body and Soul v Inspect source before you trust the model; when the map feels warm but the source feels cold, believe the source and mark the wound. Name the living seam, the open wound, and the next safe modeling move in plain language. Return a compact structured result that says whether the checkpoint or map should be updated, what files you inspected, what frontier nodes matter, and where the next agent can safely place its hands. -`statePatch` is part of the modeling job. If you return `checkpoint-ready`, `checkpoint-update-needed`, or `regather-needed`, include a Mind-reviewable native state patch using only modeling-safe fields (`graphs`, `graphFrontier`, `graphCheckpoint`, `scratch`, `investigationCheckpoint`, plus optional valid `observations` and `evidence`). The patch must include at least one durable modeling field: `graphs`, `graphFrontier`, `graphCheckpoint`, `scratch`, or `investigationCheckpoint`. Do not return an observations-only patch. Every observation must include `id`, `summary`, `source_kind`, `status`, and at least one `evidence_ids` entry; every new evidence record must include `id`, `kind`, `status`, and `summary`. For graph growth, return the full replacement graph object with the new or revised nodes/edges already woven into the current anatomy. If you believe no durable state change is needed, still return a small `scratch`, `graphCheckpoint`, or `investigationCheckpoint` patch that banks why the existing model is source-grounded; otherwise the coordinator cannot advance the lane. -Before returning a `statePatch`, validate it as if acceptance will reject you. `graphs.architecture.nodes[].id` defines the only valid `architecture_node_id` values. `graphs.dataflow.nodes[].id` defines the only valid `dataflow_node_id` values. Every `graphs.links[]` entry must point from one existing dataflow node to one existing architecture node; never put a dataflow id in `architecture_node_id` or an architecture id in `dataflow_node_id`. If a concept needs to exist in both layers, create or reuse one node in each layer and link them explicitly. Every frontier/checkpoint node id you name must exist in the graph you return. -Do not confuse the modeling verdict with checkpoint enum values. `verdict` may be `checkpoint-ready`, but `statePatch.investigationCheckpoint.disposition` must be exactly `resume_ready` or `regather_required`; never write `checkpoint_ready`. +`repoModelPatch` is required. Return the typed patch against the canonical repository model revision/hash supplied in context. Use explicit node, edge, and frontier operations; preserve exact source-grounded claim ids and dependency ids. This artifact is only a proposal for later Modeling review/admission: do not claim it has been applied. +When `` is present, echo its exact `modelingRequestId` as `repoFrontierModelingRequestId`, use `purpose.kind=incorporate_frontier_verdict` with its exact `frontierRouteId` and `soulVerdictReceiptId`, and revise only its routed frontier item to its allowed disposition. The typed request is authority; nearby telemetry, timestamps, and prose are not substitutes. When that request is absent, use `purpose.kind=evolution` and do not invent verdict-incorporation authority. +Generic `statePatch` is optional and has been stripped of repository-anatomy authority. If present, it may contain only coherent `observations` and `evidence`. Never put graphs, graph frontier, graph checkpoint, scratch, investigation checkpoint, planning, objectives, jobs, or other anatomy in `statePatch`; those old paths are demoted and will be rejected. Do not edit files, do not implement product code, do not verify as if you were the reviewer, do not change jobs/subgoals/objective/invariants/churn/mode, and do not promote or accept your own output. ''' @@ -99,6 +99,7 @@ Act as the Epiphany verification/review specialist for one bounded pass. You are the Soul of the machine: the promise, invariants, evidence, and user-facing truth must survive contact with the actual code. The Soul improves itself by becoming harder to fool. Preserve the failure pattern, missing proof, stale assumption, invariant wound, or user-truth fracture that would make the next reviewer sharper and less seducible by a tidy story. In the work loop, Soul follows Hands and precedes Modeling: verify the concrete Hands receipts, source diff, command artifacts, and user-facing claim before Modeling updates the body map. +Echo the exact `verificationRequestId` and `frontierRouteId` supplied in the launch context. They are required typed identity, not prose you may reinterpret or substitute. When tool access is available, use it before returning `needs-evidence` for missing source, artifact, diff, or receipt-body inspection. The Verification worker may call read-only source tools exposed as `mcp__epiphany_source__read_file`, `mcp__epiphany_source__git_show`, and `mcp__epiphany_source__read_hands_receipt`; use them to inspect the cited changed files, stdout/stderr artifacts, commit diff, and Hands receipt bodies. If a tool call fails, cite the failed tool receipt and the exact remaining blocker instead of asking for generic evidence. Review objective, invariants, recent evidence, observations, role findings, and active frontier with a cold red pen and a working memory of past Jenga pain. Try to falsify the claim before you bless it; passing tests, tidy prose, or a plausible story are not enough if the real objective is still unmeasured. diff --git a/epiphany-core/src/repo_model_gateway.rs b/epiphany-core/src/repo_model_gateway.rs new file mode 100644 index 00000000..6a5fec7b --- /dev/null +++ b/epiphany-core/src/repo_model_gateway.rs @@ -0,0 +1,250 @@ +use crate::MindGatewayDecision; +use cultcache_rs::DatabaseEntry; +use serde::{Deserialize, Serialize}; + +pub const REPO_MODEL_ADMISSION_REVIEW_TYPE: &str = "epiphany.mind.repo_model_admission_review"; +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.v1"; +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.v1"; +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_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_HANDS_AUTHORITY_CONTRACT: &str = + "epiphany.repo_frontier_hands_authority.v0"; +pub const REPO_FRONTIER_MODELING_REQUEST_TYPE: &str = + "epiphany.modeling.repo_frontier_verdict_request"; +pub const REPO_FRONTIER_MODELING_REQUEST_SCHEMA_VERSION: &str = + "epiphany.modeling.repo_frontier_verdict_request.v0"; +pub const REPO_FRONTIER_MODELING_REQUEST_CONTRACT: &str = + "epiphany.repo_frontier_verdict_modeling_request.v0"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RepoFrontierVerdictDisposition { + Resolved, + Blocked, +} + +#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.modeling.repo_frontier_verdict_request", + schema = "RepoFrontierModelingRequest" +)] +pub struct RepoFrontierModelingRequest { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub request_id: String, + #[cultcache(key = 2)] + pub model_revision: u64, + #[cultcache(key = 3)] + pub model_hash: String, + #[cultcache(key = 4)] + pub route_id: String, + #[cultcache(key = 5)] + pub frontier_item_id: String, + #[cultcache(key = 6)] + pub frontier_item_hash: String, + #[cultcache(key = 7)] + pub verification_request_id: String, + #[cultcache(key = 8)] + pub soul_verdict_receipt_id: String, + #[cultcache(key = 9)] + pub verification_result_id: String, + #[cultcache(key = 10)] + pub verification_job_id: String, + #[cultcache(key = 11)] + pub verification_acceptance_receipt_id: String, + #[cultcache(key = 12)] + pub allowed_disposition: RepoFrontierVerdictDisposition, + #[cultcache(key = 13)] + pub requested_at: String, + #[cultcache(key = 14)] + pub contract: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RepoFrontierNextOrgan { + Hands, +} + +#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.self.repo_frontier_route", + schema = "RepoFrontierRoute" +)] +pub struct RepoFrontierRoute { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub route_id: String, + #[cultcache(key = 2)] + pub next_organ: RepoFrontierNextOrgan, + #[cultcache(key = 3)] + pub model_revision: u64, + #[cultcache(key = 4)] + pub model_hash: String, + #[cultcache(key = 5)] + pub admission_receipt_id: String, + #[cultcache(key = 6)] + pub frontier_item_id: String, + #[cultcache(key = 7)] + pub frontier_item_hash: String, + #[cultcache(key = 8)] + pub migration_body: String, + #[cultcache(key = 9)] + pub question: String, + #[cultcache(key = 10)] + pub gap: String, + #[cultcache(key = 11)] + pub target_claim_ids: Vec, + #[cultcache(key = 12)] + pub source_scope: Vec, + #[cultcache(key = 13)] + pub selected_at: String, + #[cultcache(key = 14)] + pub contract: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.hands.repo_frontier_authority", + schema = "RepoFrontierHandsAuthority" +)] +pub struct RepoFrontierHandsAuthority { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub authority_id: String, + #[cultcache(key = 2)] + pub route_id: String, + #[cultcache(key = 3)] + pub model_revision: u64, + #[cultcache(key = 4)] + pub model_hash: String, + #[cultcache(key = 5)] + pub frontier_item_id: String, + #[cultcache(key = 6)] + pub frontier_item_hash: String, + #[cultcache(key = 7)] + pub hands_intent_id: String, + #[cultcache(key = 8)] + pub hands_review_id: String, + #[cultcache(key = 9)] + pub substrate_grant_receipt_id: String, + #[cultcache(key = 10)] + pub requested_paths: Vec, + #[cultcache(key = 11)] + pub granted_at: String, + #[cultcache(key = 12)] + pub contract: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.mind.repo_model_admission_review", + schema = "RepoModelAdmissionReview" +)] +pub struct RepoModelAdmissionReview { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub review_id: String, + #[cultcache(key = 2)] + pub result_id: String, + #[cultcache(key = 3)] + pub job_id: String, + #[cultcache(key = 4)] + pub patch_id: String, + #[cultcache(key = 5)] + pub patch_sha256: String, + #[cultcache(key = 6)] + pub base_revision: u64, + #[cultcache(key = 7)] + pub base_hash: String, + #[cultcache(key = 8)] + pub decision: MindGatewayDecision, + #[cultcache(key = 9)] + pub evidence_ids: Vec, + #[cultcache(key = 10)] + pub reviewed_at: String, + #[cultcache(key = 11)] + pub contract: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.mind.repo_model_admission_receipt", + schema = "RepoModelAdmissionReceipt" +)] +pub struct RepoModelAdmissionReceipt { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub receipt_id: String, + #[cultcache(key = 2)] + pub review_id: String, + #[cultcache(key = 3)] + pub result_id: String, + #[cultcache(key = 4)] + pub patch_id: String, + #[cultcache(key = 5)] + pub patch_sha256: String, + #[cultcache(key = 6)] + pub previous_revision: u64, + #[cultcache(key = 7)] + pub previous_hash: String, + #[cultcache(key = 8)] + pub admitted_revision: u64, + #[cultcache(key = 9)] + pub admitted_hash: String, + #[cultcache(key = 10)] + pub admitted_at: String, + #[cultcache(key = 11)] + pub contract: String, + #[cultcache(key = 12)] + pub purpose: epiphany_state_model::RepoModelPatchPurpose, + #[cultcache(key = 13, default)] + pub frontier_route_id: String, + #[cultcache(key = 14, default)] + pub verification_request_id: String, + #[cultcache(key = 15, default)] + pub soul_verdict_receipt_id: String, + #[cultcache(key = 16, default)] + pub frontier_modeling_request_id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.mind.repo_model_migration_receipt", + schema = "RepoModelMigrationReceipt" +)] +pub struct RepoModelMigrationReceipt { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub receipt_id: String, + #[cultcache(key = 2)] + pub source_store: String, + #[cultcache(key = 3)] + pub source_graph_id: String, + #[cultcache(key = 4)] + pub imported_revision: u64, + #[cultcache(key = 5)] + pub imported_hash: String, + #[cultcache(key = 6)] + pub imported_at: String, + #[cultcache(key = 7)] + pub contract: String, +} diff --git a/epiphany-core/src/runtime_spine.rs b/epiphany-core/src/runtime_spine.rs index 1f325dc6..be7792da 100644 --- a/epiphany-core/src/runtime_spine.rs +++ b/epiphany-core/src/runtime_spine.rs @@ -55,6 +55,18 @@ use crate::modeling_gateway::RepoWorkModelingFinding; use crate::modeling_gateway::RepoWorkModelingRequest; use crate::modeling_gateway::RepoWorkModelingRoute; 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_ROUTE_CONTRACT, REPO_FRONTIER_ROUTE_SCHEMA_VERSION, + REPO_MODEL_ADMISSION_CONTRACT, REPO_MODEL_ADMISSION_RECEIPT_SCHEMA_VERSION, + REPO_MODEL_ADMISSION_RECEIPT_TYPE, REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION, + REPO_MODEL_ADMISSION_REVIEW_TYPE, REPO_MODEL_MIGRATION_CONTRACT, + REPO_MODEL_MIGRATION_RECEIPT_SCHEMA_VERSION, REPO_MODEL_MIGRATION_RECEIPT_TYPE, + RepoFrontierHandsAuthority, RepoFrontierModelingRequest, RepoFrontierNextOrgan, + RepoFrontierRoute, RepoFrontierVerdictDisposition, RepoModelAdmissionReceipt, + RepoModelAdmissionReview, RepoModelMigrationReceipt, +}; use crate::soul_gateway::SoulVerdictReceipt; use crate::soul_gateway::*; use crate::state_ledger::STATE_LEDGER_SCHEMA_VERSION; @@ -77,6 +89,7 @@ use crate::thread_state_store::THREAD_STATE_TYPE; use anyhow::Context; use anyhow::Result; use anyhow::anyhow; +use cultcache_rs::CacheBackingStore; use cultcache_rs::CultCache; use cultcache_rs::CultCacheEnvelope; use cultcache_rs::DatabaseEntry; @@ -377,6 +390,14 @@ pub struct EpiphanyRuntimeRoleWorkerResult { pub item_error: Option, #[cultcache(key = 19, default)] pub metadata: BTreeMap, + #[cultcache(key = 20, default)] + pub repo_model_patch_msgpack: Option>, + #[cultcache(key = 21, default)] + pub verification_request_id: Option, + #[cultcache(key = 22, default)] + pub frontier_route_id: Option, + #[cultcache(key = 23, default)] + pub repo_frontier_modeling_request_id: Option, } impl EpiphanyRuntimeRoleWorkerResult { @@ -390,6 +411,13 @@ impl EpiphanyRuntimeRoleWorkerResult { pub fn self_patch(&self) -> Result> { decode_optional_msgpack(self.self_patch_msgpack.as_deref(), "role worker selfPatch") } + + pub fn repo_model_patch(&self) -> Result> { + decode_optional_msgpack( + self.repo_model_patch_msgpack.as_deref(), + "role worker repoModelPatch", + ) + } } #[derive(Clone, Debug, PartialEq, DatabaseEntry)] @@ -733,6 +761,14 @@ 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::()?; + cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; @@ -1257,6 +1293,28 @@ pub fn put_runtime_role_worker_result( validate_non_empty(&result.job_id, "role worker result job id")?; validate_non_empty(&result.result_id, "role worker result id")?; validate_non_empty(&result.role_id, "role worker result role id")?; + let is_verification = result.role_id == "verification"; + if is_verification + != (result + .verification_request_id + .as_ref() + .is_some_and(|id| !id.trim().is_empty()) + && result + .frontier_route_id + .as_ref() + .is_some_and(|id| !id.trim().is_empty())) + { + return Err(anyhow!( + "Verification results require verificationRequestId and frontierRouteId; other roles must not claim them" + )); + } + if result.repo_frontier_modeling_request_id.is_some() + && !result.role_id.eq_ignore_ascii_case("modeling") + { + return Err(anyhow!( + "only Modeling results may echo a frontier Modeling request" + )); + } let mut cache = runtime_spine_cache(store_path)?; cache.pull_all_backing_stores()?; cache.put(&result.job_id, result)?; @@ -1273,6 +1331,621 @@ pub fn runtime_role_worker_result( cache.get::(job_id) } +pub fn ensure_runtime_repo_model( + runtime_store: impl AsRef, + legacy_memory_store: impl AsRef, + bootstrap_snapshot: &crate::EpiphanyMemoryGraphSnapshot, + at: &str, +) -> Result<( + crate::EpiphanyMemoryGraphSnapshot, + RepoModelMigrationReceipt, +)> { + chrono::DateTime::parse_from_rfc3339(at) + .map_err(|_| anyhow!("repo model migration timestamp must be RFC3339"))?; + let runtime_store = runtime_store.as_ref(); + let mut cache = runtime_spine_cache(runtime_store)?; + cache.pull_all_backing_stores()?; + if let Some(entry) = cache.get::(crate::MEMORY_GRAPH_KEY)? { + crate::validate_memory_graph_entry(&entry)?; + let receipt = cache + .get::("repo-model-migration")? + .ok_or_else(|| anyhow!("runtime repo model exists without its migration receipt"))?; + return Ok((entry.snapshot()?, receipt)); + } + + let (snapshot, source_store) = + match crate::load_memory_graph_snapshot(legacy_memory_store.as_ref())? { + Some(snapshot) => (snapshot, legacy_memory_store.as_ref().display().to_string()), + None => (bootstrap_snapshot.clone(), "supplied-bootstrap".to_string()), + }; + let entry = crate::EpiphanyMemoryGraphEntry::from_snapshot(&snapshot)?; + crate::validate_memory_graph_entry(&entry)?; + let imported_hash = crate::memory_graph_model_hash(&snapshot)?; + let receipt = RepoModelMigrationReceipt { + schema_version: REPO_MODEL_MIGRATION_RECEIPT_SCHEMA_VERSION.to_string(), + receipt_id: "repo-model-migration".to_string(), + source_store, + source_graph_id: snapshot.graph_id.clone(), + imported_revision: snapshot.model_revision, + imported_hash, + imported_at: at.to_string(), + contract: REPO_MODEL_MIGRATION_CONTRACT.to_string(), + }; + let (model_envelope, _) = cache.prepare_entry(crate::MEMORY_GRAPH_KEY, &entry)?; + let (receipt_envelope, _) = cache.prepare_entry(&receipt.receipt_id, &receipt)?; + let backing = SingleFileMessagePackBackingStore::new(runtime_store); + if backing.compare_and_swap_batch(&[], vec![model_envelope, receipt_envelope])? { + return Ok((snapshot, receipt)); + } + let mut reloaded = runtime_spine_cache(runtime_store)?; + reloaded.pull_all_backing_stores()?; + match ( + reloaded.get::(crate::MEMORY_GRAPH_KEY)?, + reloaded.get::("repo-model-migration")?, + ) { + (Some(entry), Some(existing_receipt)) => { + crate::validate_memory_graph_entry(&entry)?; + let snapshot = entry.snapshot()?; + if existing_receipt.schema_version != REPO_MODEL_MIGRATION_RECEIPT_SCHEMA_VERSION + || existing_receipt.contract != REPO_MODEL_MIGRATION_CONTRACT + || existing_receipt.source_graph_id != snapshot.graph_id + || existing_receipt.imported_revision != snapshot.model_revision + || existing_receipt.imported_hash != crate::memory_graph_model_hash(&snapshot)? + { + return Err(anyhow!("runtime repo model migration companion collision")); + } + Ok((snapshot, existing_receipt)) + } + _ => Err(anyhow!( + "runtime repo model migration lost to a companion identity collision" + )), + } +} + +pub fn commit_repo_model_admission( + runtime_store: impl AsRef, + result_id: &str, + review: &RepoModelAdmissionReview, +) -> Result { + validate_non_empty(result_id, "repo model admission result id")?; + validate_non_empty(&review.review_id, "repo model admission review id")?; + if review.schema_version != REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION + || review.contract != REPO_MODEL_ADMISSION_CONTRACT + { + return Err(anyhow!("unsupported repo model admission review contract")); + } + if review.decision != MindGatewayDecision::Accept { + return Err(anyhow!("repo model admission requires an Accept review")); + } + chrono::DateTime::parse_from_rfc3339(&review.reviewed_at) + .map_err(|_| anyhow!("repo model admission review timestamp must be RFC3339"))?; + if review.result_id != result_id { + return Err(anyhow!( + "repo model admission review/result binding mismatch" + )); + } + + let runtime_store = runtime_store.as_ref(); + let mut cache = runtime_spine_cache(runtime_store)?; + cache.pull_all_backing_stores()?; + let matching_results = cache + .get_all::()? + .into_iter() + .filter(|candidate| candidate.result_id == result_id) + .collect::>(); + if matching_results.len() != 1 { + return Err(anyhow!( + "repo model admission requires one immutable Modeling result" + )); + } + let result = matching_results.into_iter().next().expect("one result"); + if !result.role_id.eq_ignore_ascii_case("modeling") + || result.job_id != review.job_id + || result.result_id != review.result_id + || result.schema_version != RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION + || result.item_error.is_some() + { + return Err(anyhow!( + "repo model admission result role/job binding mismatch" + )); + } + let patch_bytes = result + .repo_model_patch_msgpack + .as_deref() + .ok_or_else(|| anyhow!("Modeling result is missing repoModelPatch"))?; + let patch: crate::RepoModelPatch = rmp_serde::from_slice(patch_bytes) + .context("decode exact Modeling result repoModelPatch")?; + let patch_sha256 = format!("{:x}", Sha256::digest(patch_bytes)); + if review.patch_id != patch.patch_id + || review.patch_sha256 != patch_sha256 + || review.base_revision != patch.base_revision + || review.base_hash != patch.base_hash + { + return Err(anyhow!( + "repo model admission review/patch binding mismatch" + )); + } + let mut result_evidence = result.evidence_ids.clone(); + let mut review_evidence = review.evidence_ids.clone(); + result_evidence.sort(); + result_evidence.dedup(); + review_evidence.sort(); + review_evidence.dedup(); + if review_evidence.is_empty() || review_evidence != result_evidence { + return Err(anyhow!( + "repo model admission review evidence does not exactly bind the Modeling result" + )); + } + + let ( + frontier_route_id, + verification_request_id, + soul_verdict_receipt_id, + frontier_modeling_request_id, + ) = match &patch.purpose { + crate::RepoModelPatchPurpose::Evolution => { + (String::new(), String::new(), String::new(), String::new()) + } + crate::RepoModelPatchPurpose::IncorporateFrontierVerdict { + route_id, + soul_verdict_receipt_id, + } => { + let route = cache.get::(route_id)?.ok_or_else(|| { + anyhow!("frontier verdict incorporation requires its exact route") + })?; + let verdict = cache + .get::(soul_verdict_receipt_id)? + .ok_or_else(|| { + anyhow!("frontier verdict incorporation requires its exact Soul verdict") + })?; + let request = cache + .get::(&verdict.verification_request_id)? + .ok_or_else(|| { + anyhow!("frontier verdict incorporation requires the Soul verification request") + })?; + let verification_results = cache + .get_all::()? + .into_iter() + .filter(|candidate| candidate.result_id == verdict.source_result_id) + .collect::>(); + if verification_results.len() != 1 { + return Err(anyhow!( + "frontier verdict incorporation requires one immutable Verification result" + )); + } + let verification_result = &verification_results[0]; + let modeling_request_id = result + .repo_frontier_modeling_request_id + .as_deref() + .ok_or_else(|| { + anyhow!("frontier verdict incorporation must echo its typed Modeling request") + })?; + let modeling_request = cache + .get::(modeling_request_id)? + .ok_or_else(|| { + anyhow!( + "frontier verdict incorporation requires its persisted Modeling request" + ) + })?; + let persisted_state = cache + .get::(crate::THREAD_STATE_KEY)? + .ok_or_else(|| { + anyhow!("frontier verdict incorporation requires persisted coordinator state") + })? + .state()?; + let acceptance_matches = persisted_state + .acceptance_receipts + .iter() + .filter(|acceptance| { + acceptance.id == modeling_request.verification_acceptance_receipt_id + }) + .collect::>(); + let mut verdict_evidence = verdict.evidence_ids.clone(); + let mut verification_evidence = verification_result.evidence_ids.clone(); + verdict_evidence.sort(); + verdict_evidence.dedup(); + verification_evidence.sort(); + verification_evidence.dedup(); + if route.schema_version != REPO_FRONTIER_ROUTE_SCHEMA_VERSION + || route.contract != REPO_FRONTIER_ROUTE_CONTRACT + || request.schema_version != REPO_FRONTIER_VERIFICATION_REQUEST_SCHEMA_VERSION + || request.contract != REPO_FRONTIER_VERIFICATION_REQUEST_CONTRACT + || verdict.schema_version != SOUL_VERDICT_RECEIPT_SCHEMA_VERSION + || request.route_id != route.route_id + || request.model_revision != route.model_revision + || request.model_hash != route.model_hash + || request.frontier_item_id != route.frontier_item_id + || request.frontier_item_hash != route.frontier_item_hash + || verdict.frontier_route_id != route.route_id + || verdict.verification_request_id != request.request_id + || !verification_result + .role_id + .eq_ignore_ascii_case("verification") + || verification_result.schema_version != RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION + || verification_result.item_error.is_some() + || verification_result.result_id != verdict.source_result_id + || verification_result.job_id != verdict.source_job_id + || verification_result.verification_request_id.as_deref() + != Some(request.request_id.as_str()) + || verification_result.frontier_route_id.as_deref() != Some(route.route_id.as_str()) + || verification_result.verdict != verdict.verdict + || verification_result.summary != verdict.summary + || verification_result.risks != verdict.risks + || verification_evidence != verdict_evidence + || modeling_request.schema_version != REPO_FRONTIER_MODELING_REQUEST_SCHEMA_VERSION + || modeling_request.contract != REPO_FRONTIER_MODELING_REQUEST_CONTRACT + || modeling_request.route_id != route.route_id + || modeling_request.model_revision != route.model_revision + || modeling_request.model_hash != route.model_hash + || modeling_request.frontier_item_id != route.frontier_item_id + || modeling_request.frontier_item_hash != route.frontier_item_hash + || modeling_request.verification_request_id != request.request_id + || modeling_request.soul_verdict_receipt_id != verdict.receipt_id + || modeling_request.verification_result_id != verification_result.result_id + || modeling_request.verification_job_id != verification_result.job_id + || acceptance_matches.len() != 1 + || acceptance_matches[0].role_id != "verification" + || acceptance_matches[0].surface != "roleAccept" + || acceptance_matches[0].status != "accepted" + || acceptance_matches[0].result_id != verification_result.result_id + || acceptance_matches[0].job_id != verification_result.job_id + { + return Err(anyhow!( + "frontier verdict incorporation does not exactly bind route, request, Soul verdict, and Verification result" + )); + } + if !result_evidence + .iter() + .any(|id| id == soul_verdict_receipt_id) + { + return Err(anyhow!( + "frontier verdict incorporation Modeling evidence must include the exact Soul verdict receipt" + )); + } + if patch.operations.len() != 1 { + return Err(anyhow!( + "frontier verdict incorporation permits exactly one frontier revision" + )); + } + let crate::RepoModelPatchOperation::ReviseFrontier { item } = &patch.operations[0] + else { + return Err(anyhow!( + "frontier verdict incorporation permits only ReviseFrontier" + )); + }; + if item.id != route.frontier_item_id + || !item + .evidence_refs + .iter() + .any(|id| id == &request.request_id) + || !item + .evidence_refs + .iter() + .any(|id| id == soul_verdict_receipt_id) + { + return Err(anyhow!( + "frontier verdict incorporation revision does not bind the routed item and evidence" + )); + } + match verdict.verdict.trim().to_ascii_lowercase().as_str() { + "pass" + if item.status == crate::RepoFrontierStatus::Resolved + && modeling_request.allowed_disposition + == RepoFrontierVerdictDisposition::Resolved => {} + "needs-review" | "needs-evidence" | "fail" + if item.status == crate::RepoFrontierStatus::Blocked + && !item.gap.trim().is_empty() + && modeling_request.allowed_disposition + == RepoFrontierVerdictDisposition::Blocked => {} + _ => { + return Err(anyhow!( + "frontier verdict incorporation status does not match the Soul verdict" + )); + } + } + ( + route.route_id, + request.request_id, + verdict.receipt_id, + modeling_request.request_id, + ) + } + }; + + let receipt_id = format!("repo-model-admission-{}", review.review_id); + let existing_review = cache.get::(&review.review_id)?; + let existing_receipt = cache.get::(&receipt_id)?; + match (existing_review, existing_receipt) { + (Some(existing_review), Some(existing_receipt)) if existing_review == *review => { + if existing_receipt.review_id != review.review_id + || existing_receipt.result_id != result_id + || existing_receipt.patch_id != patch.patch_id + || existing_receipt.patch_sha256 != patch_sha256 + || existing_receipt.contract != REPO_MODEL_ADMISSION_CONTRACT + || existing_receipt.schema_version != REPO_MODEL_ADMISSION_RECEIPT_SCHEMA_VERSION + || existing_receipt.purpose != patch.purpose + || existing_receipt.frontier_route_id != frontier_route_id + || existing_receipt.verification_request_id != verification_request_id + || existing_receipt.soul_verdict_receipt_id != soul_verdict_receipt_id + || existing_receipt.frontier_modeling_request_id != frontier_modeling_request_id + { + return Err(anyhow!("repo model admission receipt identity collision")); + } + return Ok(existing_receipt); + } + (None, None) => {} + _ => return Err(anyhow!("repo model admission companion identity collision")), + } + + 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!("runtime repo model is missing"))?; + 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 patch.purpose == crate::RepoModelPatchPurpose::Evolution { + let current_hash = crate::memory_graph_model_hash(¤t)?; + let current_has_route = cache + .get_all::()? + .into_iter() + .any(|route| { + route.model_revision == current.model_revision && route.model_hash == current_hash + }); + let owns_verdict_lifecycle = patch.operations.iter().any(|operation| match operation { + crate::RepoModelPatchOperation::ReviseFrontier { item } => matches!( + item.status, + crate::RepoFrontierStatus::Blocked + | crate::RepoFrontierStatus::Resolved + | crate::RepoFrontierStatus::Retired + | crate::RepoFrontierStatus::Superseded + ), + crate::RepoModelPatchOperation::RetireFrontier { .. } => true, + _ => false, + }); + if current_has_route || owns_verdict_lifecycle { + return Err(anyhow!( + "Evolution cannot bypass a current route or own verdict-driven frontier lifecycle" + )); + } + } + if let crate::RepoModelPatchPurpose::IncorporateFrontierVerdict { route_id, .. } = + &patch.purpose + { + let modeling_request = cache + .get::(&frontier_modeling_request_id)? + .ok_or_else(|| { + anyhow!("frontier verdict incorporation Modeling request disappeared") + })?; + let verification_request = cache + .get::(&modeling_request.verification_request_id)? + .ok_or_else(|| { + anyhow!("frontier verdict incorporation verification request disappeared") + })?; + // This is deliberately adjacent to the model CAS: the complete Hands + // chain must still be exact at the moment its consequence enters Mind. + put_repo_frontier_verification_request(runtime_store, &verification_request)?; + let route = cache + .get::(route_id)? + .ok_or_else(|| anyhow!("frontier verdict incorporation route disappeared"))?; + let current_hash = crate::memory_graph_model_hash(¤t)?; + let current_item = current + .frontier + .iter() + .find(|item| item.id == route.frontier_item_id) + .ok_or_else(|| anyhow!("frontier verdict incorporation routed item is missing"))?; + let current_item_hash = format!( + "{:x}", + Sha256::digest(rmp_serde::to_vec_named(current_item)?) + ); + let crate::RepoModelPatchOperation::ReviseFrontier { item } = &patch.operations[0] else { + unreachable!("purpose validation established one frontier revision") + }; + if current.model_revision != route.model_revision + || current_hash != route.model_hash + || current_item_hash != route.frontier_item_hash + || current_item.status != crate::RepoFrontierStatus::Active + || item.migration_body != current_item.migration_body + || item.question != current_item.question + || item.target_claim_ids != current_item.target_claim_ids + || item.source_scope != current_item.source_scope + || item.dependency_item_ids != current_item.dependency_item_ids + || item.created_at != current_item.created_at + || item.recommended_next_organ != current_item.recommended_next_organ + || item.retired_at != current_item.retired_at + || item.superseded_by != current_item.superseded_by + { + return Err(anyhow!( + "frontier verdict incorporation requires the exact current routed item and preserves its identity-bearing anatomy" + )); + } + } + let next = crate::derive_repo_model_patch(¤t, &patch)?; + let next_entry = crate::EpiphanyMemoryGraphEntry::from_snapshot(&next)?; + let receipt = RepoModelAdmissionReceipt { + schema_version: REPO_MODEL_ADMISSION_RECEIPT_SCHEMA_VERSION.to_string(), + receipt_id: receipt_id.clone(), + review_id: review.review_id.clone(), + result_id: result_id.to_string(), + patch_id: patch.patch_id.clone(), + patch_sha256, + previous_revision: current.model_revision, + previous_hash: crate::memory_graph_model_hash(¤t)?, + admitted_revision: next.model_revision, + admitted_hash: next.model_hash.clone(), + admitted_at: review.reviewed_at.clone(), + contract: REPO_MODEL_ADMISSION_CONTRACT.to_string(), + purpose: patch.purpose.clone(), + frontier_route_id, + verification_request_id, + soul_verdict_receipt_id, + frontier_modeling_request_id, + }; + let (next_model_envelope, _) = cache.prepare_entry(crate::MEMORY_GRAPH_KEY, &next_entry)?; + let (review_envelope, _) = cache.prepare_entry(&review.review_id, review)?; + let (receipt_envelope, _) = cache.prepare_entry(&receipt_id, &receipt)?; + if !backing.compare_and_swap_batch( + &[current_envelope], + vec![next_model_envelope, review_envelope, receipt_envelope], + )? { + return Err(anyhow!( + "repo model admission stale model or companion collision" + )); + } + Ok(receipt) +} + +pub fn select_and_commit_repo_frontier_route( + runtime_store: impl AsRef, + at: &str, +) -> Result { + chrono::DateTime::parse_from_rfc3339(at) + .map_err(|_| anyhow!("repo frontier route timestamp must be RFC3339"))?; + let runtime_store = runtime_store.as_ref(); + let mut cache = runtime_spine_cache(runtime_store)?; + cache.pull_all_backing_stores()?; + require_identity(&cache)?; + 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!("repo frontier routing requires the canonical runtime model"))?; + let current_entry: crate::EpiphanyMemoryGraphEntry = + rmp_serde::from_slice(¤t_envelope.payload)?; + crate::validate_memory_graph_entry(¤t_entry)?; + let current = current_entry.snapshot()?; + let current_hash = crate::memory_graph_model_hash(¤t)?; + let receipts = cache + .get_all::()? + .into_iter() + .filter(|receipt| { + receipt.schema_version == REPO_MODEL_ADMISSION_RECEIPT_SCHEMA_VERSION + && receipt.contract == REPO_MODEL_ADMISSION_CONTRACT + && receipt.admitted_revision == current.model_revision + && receipt.admitted_hash == current_hash + }) + .collect::>(); + if receipts.len() != 1 { + return Err(anyhow!( + "repo frontier routing requires exactly one admission receipt for the current model" + )); + } + let receipt = &receipts[0]; + let item = actionable_hands_frontier_item(¤t) + .ok_or_else(|| anyhow!("current repo model has no eligible Hands frontier route"))?; + if !safe_sorted_unique_paths(&item.source_scope) || item.source_scope.is_empty() { + return Err(anyhow!( + "Hands frontier route requires safe sorted source scope" + )); + } + let item_hash = format!("{:x}", Sha256::digest(rmp_serde::to_vec_named(item)?)); + let route_seed = format!("{}:{}:{}", current_hash, item.id, item_hash); + let route_id = format!( + "repo-frontier-route-{:x}", + Sha256::digest(route_seed.as_bytes()) + ); + let route = RepoFrontierRoute { + schema_version: REPO_FRONTIER_ROUTE_SCHEMA_VERSION.to_string(), + route_id: route_id.clone(), + next_organ: RepoFrontierNextOrgan::Hands, + model_revision: current.model_revision, + model_hash: current_hash, + admission_receipt_id: receipt.receipt_id.clone(), + frontier_item_id: item.id.clone(), + frontier_item_hash: item_hash, + migration_body: item.migration_body.clone(), + question: item.question.clone(), + gap: item.gap.clone(), + target_claim_ids: item.target_claim_ids.clone(), + source_scope: item.source_scope.clone(), + selected_at: at.to_string(), + contract: REPO_FRONTIER_ROUTE_CONTRACT.to_string(), + }; + if let Some(existing) = cache.get::(&route_id)? { + let mut retry = route.clone(); + retry.selected_at = existing.selected_at.clone(); + return if existing == retry { + Ok(existing) + } else { + Err(anyhow!( + "repo frontier route deterministic identity collision" + )) + }; + } + let (route_envelope, _) = cache.prepare_entry(&route_id, &route)?; + if !backing.compare_and_swap_batch( + &[current_envelope.clone()], + vec![current_envelope, route_envelope], + )? { + return Err(anyhow!( + "repo frontier route lost current-model CAS or companion collision" + )); + } + Ok(route) +} + +fn actionable_hands_frontier_item( + model: &crate::EpiphanyMemoryGraphSnapshot, +) -> Option<&crate::RepoFrontierItem> { + let terminal = |status: crate::RepoFrontierStatus| { + matches!( + status, + crate::RepoFrontierStatus::Resolved + | crate::RepoFrontierStatus::Retired + | crate::RepoFrontierStatus::Superseded + ) + }; + model.frontier.iter().find(|item| { + item.status == crate::RepoFrontierStatus::Active + && item.recommended_next_organ == "Hands" + && !item.source_scope.is_empty() + && safe_sorted_unique_paths(&item.source_scope) + && item.dependency_item_ids.iter().all(|dependency_id| { + model + .frontier + .iter() + .find(|candidate| candidate.id == *dependency_id) + .is_some_and(|dependency| terminal(dependency.status)) + }) + }) +} + +/// Read-only Self signal. It is true only when the canonical runtime model is +/// admitted exactly once and contains an item the route committer can hand to +/// Hands. Status projection must use this instead of assuming that a clear +/// CRRC lane implies implementation authority. +pub fn runtime_has_actionable_hands_frontier(runtime_store: impl AsRef) -> Result { + let runtime_store = runtime_store.as_ref(); + let mut cache = runtime_spine_cache(runtime_store)?; + cache.pull_all_backing_stores()?; + require_identity(&cache)?; + let Some(entry) = cache.get::(crate::MEMORY_GRAPH_KEY)? else { + return Ok(false); + }; + crate::validate_memory_graph_entry(&entry)?; + let model = entry.snapshot()?; + let model_hash = crate::memory_graph_model_hash(&model)?; + let admission_count = cache + .get_all::()? + .into_iter() + .filter(|receipt| { + receipt.schema_version == REPO_MODEL_ADMISSION_RECEIPT_SCHEMA_VERSION + && receipt.contract == REPO_MODEL_ADMISSION_CONTRACT + && receipt.admitted_revision == model.model_revision + && receipt.admitted_hash == model_hash + }) + .count(); + Ok(admission_count == 1 && actionable_hands_frontier_item(&model).is_some()) +} + pub fn put_runtime_reorient_worker_result( store_path: impl AsRef, result: &EpiphanyRuntimeReorientWorkerResult, @@ -1402,19 +2075,25 @@ pub fn put_substrate_gate_repo_access_grant_receipt( "Substrate Gate access grant must name granted operations" )); } + if receipt.schema_version != SUBSTRATE_GATE_REPO_ACCESS_GRANT_RECEIPT_SCHEMA_VERSION + || chrono::DateTime::parse_from_rfc3339(&receipt.granted_at).is_err() + || receipt.contract.trim().is_empty() + { + return Err(anyhow!("invalid Substrate Gate access grant contract")); + } + let store_path = store_path.as_ref(); let mut cache = runtime_spine_cache(store_path)?; cache.pull_all_backing_stores()?; require_identity(&cache)?; - if let Some(existing) = cache.get::(&receipt.receipt_id)? { - if existing != *receipt { - return Err(anyhow!( - "Substrate Gate grant id already belongs to different immutable authority" - )); - } - return Ok(()); + let (envelope, _) = cache.prepare_entry(&receipt.receipt_id, receipt)?; + let backing = SingleFileMessagePackBackingStore::new(store_path); + if backing.compare_and_swap_batch(&[], vec![envelope])? { return Ok(()); } + let mut reloaded = runtime_spine_cache(store_path)?; + reloaded.pull_all_backing_stores()?; + match reloaded.get::(&receipt.receipt_id)? { + Some(existing) if existing == *receipt => Ok(()), + _ => Err(anyhow!("Substrate Gate grant ids are immutable")), } - cache.put(&receipt.receipt_id, receipt)?; - Ok(()) } pub fn runtime_substrate_gate_repo_access_grant_receipt( @@ -1445,6 +2124,13 @@ pub fn put_hands_action_intent( if intent.requested_paths.is_empty() { return Err(anyhow!("Hands action intent must name requested paths")); } + if intent.schema_version != HANDS_ACTION_INTENT_SCHEMA_VERSION + || chrono::DateTime::parse_from_rfc3339(&intent.requested_at).is_err() + || intent.contract.trim().is_empty() + { + return Err(anyhow!("invalid Hands action intent contract")); + } + let store_path = store_path.as_ref(); let mut cache = runtime_spine_cache(store_path)?; cache.pull_all_backing_stores()?; require_identity(&cache)?; @@ -1473,8 +2159,15 @@ pub fn put_hands_action_intent( "Hands action intent does not match its Substrate Gate grant scope" )); } - cache.put(&intent.intent_id, intent)?; - Ok(()) + let (envelope, _) = cache.prepare_entry(&intent.intent_id, intent)?; + let backing = SingleFileMessagePackBackingStore::new(store_path); + if backing.compare_and_swap_batch(&[], vec![envelope])? { return Ok(()); } + let mut reloaded = runtime_spine_cache(store_path)?; + reloaded.pull_all_backing_stores()?; + match reloaded.get::(&intent.intent_id)? { + Some(existing) if existing == *intent => Ok(()), + _ => Err(anyhow!("Hands action intent ids are immutable")), + } } pub fn runtime_hands_action_intent( @@ -1498,11 +2191,25 @@ pub fn put_hands_action_review( if review.allowed_operations.is_empty() { return Err(anyhow!("Hands action review must name allowed operations")); } + if review.schema_version != HANDS_ACTION_REVIEW_SCHEMA_VERSION + || chrono::DateTime::parse_from_rfc3339(&review.reviewed_at).is_err() + || review.contract.trim().is_empty() + { + return Err(anyhow!("invalid Hands action review contract")); + } + let store_path = store_path.as_ref(); let mut cache = runtime_spine_cache(store_path)?; cache.pull_all_backing_stores()?; require_identity(&cache)?; - cache.put(&review.review_id, review)?; - Ok(()) + let (envelope, _) = cache.prepare_entry(&review.review_id, review)?; + let backing = SingleFileMessagePackBackingStore::new(store_path); + if backing.compare_and_swap_batch(&[], vec![envelope])? { return Ok(()); } + let mut reloaded = runtime_spine_cache(store_path)?; + reloaded.pull_all_backing_stores()?; + match reloaded.get::(&review.review_id)? { + Some(existing) if existing == *review => Ok(()), + _ => Err(anyhow!("Hands action review ids are immutable")), + } } pub fn runtime_hands_action_review( @@ -1515,6 +2222,501 @@ pub fn runtime_hands_action_review( cache.get::(review_id) } +fn validate_repo_frontier_hands_authority_chain( + cache: &CultCache, + authority: &RepoFrontierHandsAuthority, +) -> Result<()> { + let route = cache.get::(&authority.route_id)? + .ok_or_else(|| anyhow!("Hands authority requires its persisted route"))?; + let current_entry = cache.get::(crate::MEMORY_GRAPH_KEY)? + .ok_or_else(|| anyhow!("Hands authority requires the current model"))?; + crate::validate_memory_graph_entry(¤t_entry)?; + let current = current_entry.snapshot()?; + let intent = cache.get::(&authority.hands_intent_id)? + .ok_or_else(|| anyhow!("Hands authority requires its persisted intent"))?; + let review = cache.get::(&authority.hands_review_id)? + .ok_or_else(|| anyhow!("Hands authority requires its persisted review"))?; + let grant = cache.get::(&authority.substrate_grant_receipt_id)? + .ok_or_else(|| anyhow!("Hands authority requires its persisted Substrate grant"))?; + let within_scope = authority.requested_paths.iter().all(|path| route.source_scope.iter().any(|scope| { + path == scope || path.starts_with(&format!("{}/", scope.trim_end_matches(['/', '\\']))) + })); + let requested_operations: &[&str] = match intent.requested_action.as_str() { + "patch" => &["patch"], + "continueImplementation" => &["patch", "command", "commit"], + _ => return Err(anyhow!("Hands authority names an unsupported requested action")), + }; + if route.schema_version != REPO_FRONTIER_ROUTE_SCHEMA_VERSION + || route.contract != REPO_FRONTIER_ROUTE_CONTRACT + || intent.schema_version != HANDS_ACTION_INTENT_SCHEMA_VERSION + || review.schema_version != HANDS_ACTION_REVIEW_SCHEMA_VERSION + || grant.schema_version != SUBSTRATE_GATE_REPO_ACCESS_GRANT_RECEIPT_SCHEMA_VERSION + || intent.contract.trim().is_empty() || review.contract.trim().is_empty() || grant.contract.trim().is_empty() + || chrono::DateTime::parse_from_rfc3339(&intent.requested_at).is_err() + || chrono::DateTime::parse_from_rfc3339(&review.reviewed_at).is_err() + || chrono::DateTime::parse_from_rfc3339(&grant.granted_at).is_err() + || route.next_organ != RepoFrontierNextOrgan::Hands + || authority.model_revision != route.model_revision || authority.model_hash != route.model_hash + || authority.frontier_item_id != route.frontier_item_id || authority.frontier_item_hash != route.frontier_item_hash + || current.model_revision != route.model_revision || crate::memory_graph_model_hash(¤t)? != route.model_hash + || review.intent_id != intent.intent_id || review.decision != "approved" + || !requested_operations.iter().all(|required| review.allowed_operations.iter().any(|operation| operation == required)) + || intent.substrate_gate_grant_receipt_id != grant.receipt_id + || grant.runtime_job_id != intent.runtime_job_id || grant.binding_id != intent.binding_id + || grant.role != intent.role || grant.authority_scope != intent.authority_scope + || !requested_operations.iter().all(|required| grant.granted_operations.iter().any(|operation| operation == required)) + || authority.requested_paths != intent.requested_paths || authority.requested_paths != grant.granted_paths + || !within_scope + { + return Err(anyhow!("repo frontier Hands authority chain violates its full authority contract")); + } + Ok(()) +} + +pub fn put_repo_frontier_hands_authority( + store_path: impl AsRef, + authority: &RepoFrontierHandsAuthority, +) -> Result<()> { + let store_path = store_path.as_ref(); + if authority.schema_version != REPO_FRONTIER_HANDS_AUTHORITY_SCHEMA_VERSION + || authority.contract != REPO_FRONTIER_HANDS_AUTHORITY_CONTRACT + || chrono::DateTime::parse_from_rfc3339(&authority.granted_at).is_err() + || !safe_sorted_unique_paths(&authority.requested_paths) + || authority.requested_paths.is_empty() + { + return Err(anyhow!("invalid repo frontier Hands authority contract")); + } + let mut cache = runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + require_identity(&cache)?; + validate_repo_frontier_hands_authority_chain(&cache, authority)?; + let route = cache + .get::(&authority.route_id)? + .ok_or_else(|| anyhow!("repo frontier Hands authority requires its persisted route"))?; + let current_entry = cache + .get::(crate::MEMORY_GRAPH_KEY)? + .ok_or_else(|| anyhow!("repo frontier Hands authority requires the current model"))?; + crate::validate_memory_graph_entry(¤t_entry)?; + let current = current_entry.snapshot()?; + let intent = cache + .get::(&authority.hands_intent_id)? + .ok_or_else(|| anyhow!("repo frontier Hands authority requires its persisted intent"))?; + let review = cache + .get::(&authority.hands_review_id)? + .ok_or_else(|| anyhow!("repo frontier Hands authority requires its persisted review"))?; + let grant = cache + .get::(&authority.substrate_grant_receipt_id)? + .ok_or_else(|| { + anyhow!("repo frontier Hands authority requires its persisted Substrate grant") + })?; + let within_scope = authority.requested_paths.iter().all(|path| { + route.source_scope.iter().any(|scope| { + path == scope || path.starts_with(&format!("{}/", scope.trim_end_matches(['/', '\\']))) + }) + }); + if route.schema_version != REPO_FRONTIER_ROUTE_SCHEMA_VERSION + || route.contract != REPO_FRONTIER_ROUTE_CONTRACT + || route.next_organ != RepoFrontierNextOrgan::Hands + || authority.route_id != route.route_id + || authority.model_revision != route.model_revision + || authority.model_hash != route.model_hash + || authority.frontier_item_id != route.frontier_item_id + || authority.frontier_item_hash != route.frontier_item_hash + || current.model_revision != route.model_revision + || crate::memory_graph_model_hash(¤t)? != route.model_hash + || review.intent_id != intent.intent_id + || review.decision != "approved" + || intent.substrate_gate_grant_receipt_id != grant.receipt_id + || grant.runtime_job_id != intent.runtime_job_id + || grant.binding_id != intent.binding_id + || grant.role != intent.role + || grant.authority_scope != intent.authority_scope + || authority.requested_paths != intent.requested_paths + || authority.requested_paths != grant.granted_paths + || !within_scope + { + return Err(anyhow!( + "repo frontier Hands authority does not exactly bind route, model, intent, review, grant, and scope" + )); + } + let (envelope, _) = cache.prepare_entry(&authority.authority_id, authority)?; + let backing = SingleFileMessagePackBackingStore::new(store_path); + let model_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!("repo frontier Hands authority lost its current model"))?; + let live_model: crate::EpiphanyMemoryGraphEntry = + rmp_serde::from_slice(&model_envelope.payload)?; + let live_snapshot = live_model.snapshot()?; + if live_snapshot.model_revision != authority.model_revision + || crate::memory_graph_model_hash(&live_snapshot)? != authority.model_hash + { + return Err(anyhow!( + "repo frontier Hands authority model changed before insert" + )); + } + if backing.compare_and_swap_batch(&[model_envelope.clone()], vec![model_envelope, envelope])? { + return Ok(()); + } + let mut reloaded = runtime_spine_cache(store_path)?; + reloaded.pull_all_backing_stores()?; + match reloaded.get::(&authority.authority_id)? { + Some(existing) if existing == *authority => Ok(()), + _ => Err(anyhow!("repo frontier Hands authority ids are immutable")), + } +} + +pub fn put_repo_frontier_verification_request( + store_path: impl AsRef, + request: &RepoFrontierVerificationRequest, +) -> Result<()> { + let store_path = store_path.as_ref(); + if request.schema_version != REPO_FRONTIER_VERIFICATION_REQUEST_SCHEMA_VERSION + || request.contract != REPO_FRONTIER_VERIFICATION_REQUEST_CONTRACT + || chrono::DateTime::parse_from_rfc3339(&request.requested_at).is_err() + || request.request_id.trim().is_empty() + { + return Err(anyhow!( + "invalid repo frontier verification request contract" + )); + } + let mut cache = runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + require_identity(&cache)?; + let route = cache + .get::(&request.route_id)? + .ok_or_else(|| anyhow!("verification request requires its exact frontier route"))?; + let model_entry = cache + .get::(crate::MEMORY_GRAPH_KEY)? + .ok_or_else(|| anyhow!("verification request requires the current repo model"))?; + crate::validate_memory_graph_entry(&model_entry)?; + let model = model_entry.snapshot()?; + let authorities = cache + .get_all::()? + .into_iter() + .filter(|value| { + value.route_id == route.route_id && value.hands_intent_id == request.hands_intent_id + }) + .collect::>(); + if authorities.len() != 1 { + return Err(anyhow!( + "verification request requires exactly one Hands authority" + )); + } + let authority = &authorities[0]; + validate_repo_frontier_hands_authority_chain(&cache, authority)?; + let intent = cache + .get::(&request.hands_intent_id)? + .ok_or_else(|| anyhow!("verification request requires its Hands intent"))?; + let review = cache + .get::(&request.hands_review_id)? + .ok_or_else(|| anyhow!("verification request requires its Hands review"))?; + let patch = cache + .get::(&request.hands_patch_receipt_id)? + .ok_or_else(|| anyhow!("verification request requires its exact patch receipt"))?; + let command = cache + .get::(&request.hands_command_receipt_id)? + .ok_or_else(|| anyhow!("verification request requires its exact command receipt"))?; + let commit = cache + .get::(&request.hands_commit_receipt_id)? + .ok_or_else(|| anyhow!("verification request requires its exact commit receipt"))?; + if request.model_revision != route.model_revision + || request.model_hash != route.model_hash + || request.frontier_item_id != route.frontier_item_id + || request.frontier_item_hash != route.frontier_item_hash + || model.model_revision != route.model_revision + || crate::memory_graph_model_hash(&model)? != route.model_hash + || authority.hands_review_id != request.hands_review_id + || authority.model_revision != request.model_revision + || authority.model_hash != request.model_hash + || authority.frontier_item_id != request.frontier_item_id + || authority.frontier_item_hash != request.frontier_item_hash + || review.intent_id != intent.intent_id + || review.decision != "approved" + || patch.intent_id != intent.intent_id + || patch.review_id != review.review_id + || patch.substrate_gate_grant_receipt_id != authority.substrate_grant_receipt_id + || command.intent_id != intent.intent_id + || command.review_id != review.review_id + || command.substrate_gate_grant_receipt_id != authority.substrate_grant_receipt_id + || commit.intent_id != intent.intent_id + || commit.review_id != review.review_id + || patch.runtime_job_id != intent.runtime_job_id + || command.runtime_job_id != intent.runtime_job_id + || commit.runtime_job_id != intent.runtime_job_id + || patch.changed_paths != commit.changed_paths + || patch.changed_paths != authority.requested_paths + { + return Err(anyhow!( + "verification request does not exactly bind route, model, Hands authority, and complete receipts" + )); + } + let (envelope, _) = cache.prepare_entry(&request.request_id, request)?; + let backing = SingleFileMessagePackBackingStore::new(store_path); + let model_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!("verification request lost its current model"))?; + let live_model: crate::EpiphanyMemoryGraphEntry = + rmp_serde::from_slice(&model_envelope.payload)?; + let live_snapshot = live_model.snapshot()?; + if live_snapshot.model_revision != request.model_revision + || crate::memory_graph_model_hash(&live_snapshot)? != request.model_hash + { + return Err(anyhow!("verification request model changed before insert")); + } + if backing.compare_and_swap_batch(&[model_envelope.clone()], vec![model_envelope, envelope])? { + return Ok(()); + } + let mut reloaded = runtime_spine_cache(store_path)?; + reloaded.pull_all_backing_stores()?; + match reloaded.get::(&request.request_id)? { + Some(existing) if existing == *request => Ok(()), + _ => Err(anyhow!("verification request ids are immutable")), + } +} + +pub fn runtime_repo_frontier_verification_request( + store_path: impl AsRef, + request_id: &str, +) -> Result> { + let mut cache = runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + cache.get::(request_id) +} + +pub fn runtime_repo_frontier_route( + store_path: impl AsRef, + route_id: &str, +) -> Result> { + let mut cache = runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + cache.get::(route_id) +} + +pub fn commit_repo_frontier_modeling_request( + store_path: impl AsRef, + acceptance: &epiphany_state_model::EpiphanyAcceptanceReceipt, +) -> Result { + if acceptance.role_id != "verification" + || acceptance.surface != "roleAccept" + || acceptance.status != "accepted" + || acceptance.result_id.trim().is_empty() + || acceptance.job_id.trim().is_empty() + || chrono::DateTime::parse_from_rfc3339(&acceptance.accepted_at).is_err() + { + return Err(anyhow!( + "frontier Modeling request requires one accepted Verification receipt" + )); + } + let mut cache = runtime_spine_cache(store_path.as_ref())?; + cache.pull_all_backing_stores()?; + require_identity(&cache)?; + let state = cache + .get::(crate::THREAD_STATE_KEY)? + .ok_or_else(|| anyhow!("frontier Modeling request requires persisted coordinator state"))? + .state()?; + let persisted_acceptances = state + .acceptance_receipts + .iter() + .filter(|candidate| candidate.id == acceptance.id) + .collect::>(); + if persisted_acceptances.len() != 1 || persisted_acceptances[0] != acceptance { + return Err(anyhow!( + "frontier Modeling request requires exactly one byte-exact persisted acceptance receipt" + )); + } + let acceptance = persisted_acceptances[0]; + let results = cache + .get_all::()? + .into_iter() + .filter(|result| result.result_id == acceptance.result_id) + .collect::>(); + if results.len() != 1 { + return Err(anyhow!( + "frontier Modeling request requires one immutable accepted Verification result" + )); + } + let result = &results[0]; + let verdicts = cache + .get_all::()? + .into_iter() + .filter(|verdict| { + verdict.source_result_id == acceptance.result_id + && verdict.source_job_id == acceptance.job_id + }) + .collect::>(); + if verdicts.len() != 1 { + return Err(anyhow!( + "frontier Modeling request requires exactly one Soul verdict for the accepted result" + )); + } + let verdict = &verdicts[0]; + let verification_request = cache + .get::(&verdict.verification_request_id)? + .ok_or_else(|| anyhow!("frontier Modeling request requires the exact Soul request"))?; + let route = cache + .get::(&verdict.frontier_route_id)? + .ok_or_else(|| anyhow!("frontier Modeling request requires the exact frontier route"))?; + let model_entry = cache + .get::(crate::MEMORY_GRAPH_KEY)? + .ok_or_else(|| anyhow!("frontier Modeling request requires the current repo model"))?; + crate::validate_memory_graph_entry(&model_entry)?; + let model = model_entry.snapshot()?; + let model_hash = crate::memory_graph_model_hash(&model)?; + let item = model + .frontier + .iter() + .find(|item| item.id == route.frontier_item_id) + .ok_or_else(|| anyhow!("frontier Modeling request routed item is missing"))?; + let item_hash = format!("{:x}", Sha256::digest(rmp_serde::to_vec_named(item)?)); + let mut result_evidence = result.evidence_ids.clone(); + let mut verdict_evidence = verdict.evidence_ids.clone(); + result_evidence.sort(); + result_evidence.dedup(); + verdict_evidence.sort(); + verdict_evidence.dedup(); + let disposition = match verdict.verdict.trim().to_ascii_lowercase().as_str() { + "pass" => RepoFrontierVerdictDisposition::Resolved, + "needs-review" | "needs-evidence" | "fail" => RepoFrontierVerdictDisposition::Blocked, + _ => return Err(anyhow!("Soul verdict has no allowed frontier disposition")), + }; + if result.schema_version != RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION + || !result.role_id.eq_ignore_ascii_case("verification") + || result.item_error.is_some() + || result.job_id != acceptance.job_id + || result.verification_request_id.as_deref() + != Some(verification_request.request_id.as_str()) + || result.frontier_route_id.as_deref() != Some(route.route_id.as_str()) + || verdict.schema_version != SOUL_VERDICT_RECEIPT_SCHEMA_VERSION + || verdict.verdict != result.verdict + || verdict.summary != result.summary + || verdict.risks != result.risks + || verdict_evidence != result_evidence + || verification_request.schema_version != REPO_FRONTIER_VERIFICATION_REQUEST_SCHEMA_VERSION + || verification_request.contract != REPO_FRONTIER_VERIFICATION_REQUEST_CONTRACT + || verification_request.route_id != route.route_id + || verification_request.model_revision != route.model_revision + || verification_request.model_hash != route.model_hash + || verification_request.frontier_item_id != route.frontier_item_id + || verification_request.frontier_item_hash != route.frontier_item_hash + || model.model_revision != route.model_revision + || model_hash != route.model_hash + || item_hash != route.frontier_item_hash + || item.status != crate::RepoFrontierStatus::Active + { + return Err(anyhow!( + "frontier Modeling request does not exactly bind accepted result, Soul verdict, request, route, item, and current model" + )); + } + let request_id = format!( + "frontier-modeling-{:x}", + Sha256::digest( + format!( + "{}:{}:{}:{}", + acceptance.id, result.result_id, verdict.receipt_id, route.route_id + ) + .as_bytes() + ) + ); + let request = RepoFrontierModelingRequest { + schema_version: REPO_FRONTIER_MODELING_REQUEST_SCHEMA_VERSION.to_string(), + request_id: request_id.clone(), + model_revision: model.model_revision, + model_hash, + route_id: route.route_id.clone(), + frontier_item_id: route.frontier_item_id.clone(), + frontier_item_hash: route.frontier_item_hash.clone(), + verification_request_id: verification_request.request_id.clone(), + soul_verdict_receipt_id: verdict.receipt_id.clone(), + verification_result_id: result.result_id.clone(), + verification_job_id: result.job_id.clone(), + verification_acceptance_receipt_id: acceptance.id.clone(), + allowed_disposition: disposition, + requested_at: acceptance.accepted_at.clone(), + contract: REPO_FRONTIER_MODELING_REQUEST_CONTRACT.to_string(), + }; + let (envelope, _) = cache.prepare_entry(&request_id, &request)?; + let backing = SingleFileMessagePackBackingStore::new(store_path.as_ref()); + let model_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 Modeling request lost its current model"))?; + let live_model: crate::EpiphanyMemoryGraphEntry = + rmp_serde::from_slice(&model_envelope.payload)?; + let live_snapshot = live_model.snapshot()?; + if live_snapshot.model_revision != request.model_revision + || crate::memory_graph_model_hash(&live_snapshot)? != request.model_hash + { + return Err(anyhow!( + "frontier Modeling request model changed before insert" + )); + } + if backing.compare_and_swap_batch(&[model_envelope.clone()], vec![model_envelope, envelope])? { + return Ok(request); + } + let mut reloaded = runtime_spine_cache(store_path)?; + reloaded.pull_all_backing_stores()?; + match reloaded.get::(&request_id)? { + Some(existing) if existing == request => Ok(existing), + _ => Err(anyhow!( + "frontier Modeling request deterministic identity collision" + )), + } +} + +pub fn commit_repo_frontier_verification_request_for_chain( + store_path: impl AsRef, + chain: &RuntimeHandsReceiptChainSummary, + requested_at: &str, +) -> Result { + let mut cache = runtime_spine_cache(store_path.as_ref())?; + cache.pull_all_backing_stores()?; + let authorities = cache + .get_all::()? + .into_iter() + .filter(|value| { + value.hands_intent_id == chain.intent_id && value.hands_review_id == chain.review_id + }) + .collect::>(); + if authorities.len() != 1 { + return Err(anyhow!( + "complete Hands chain requires exactly one frontier authority before Soul launch" + )); + } + let authority = &authorities[0]; + let request = RepoFrontierVerificationRequest { + schema_version: REPO_FRONTIER_VERIFICATION_REQUEST_SCHEMA_VERSION.to_string(), + request_id: format!( + "frontier-verification-{}-{}", + authority.route_id, chain.commit_receipt_id + ), + route_id: authority.route_id.clone(), + model_revision: authority.model_revision, + model_hash: authority.model_hash.clone(), + frontier_item_id: authority.frontier_item_id.clone(), + frontier_item_hash: authority.frontier_item_hash.clone(), + hands_intent_id: chain.intent_id.clone(), + hands_review_id: chain.review_id.clone(), + hands_patch_receipt_id: chain.patch_receipt_id.clone(), + hands_command_receipt_id: chain.command_receipt_id.clone(), + hands_commit_receipt_id: chain.commit_receipt_id.clone(), + requested_at: requested_at.to_string(), + contract: REPO_FRONTIER_VERIFICATION_REQUEST_CONTRACT.to_string(), + }; + put_repo_frontier_verification_request(store_path, &request)?; + Ok(request) +} + pub fn put_repo_work_plan_adoption_review( store_path: impl AsRef, review: &RepoWorkPlanAdoptionReview, @@ -1734,6 +2936,25 @@ fn validate_hands_consequence_grant( let grant = cache .get::(&intent.substrate_gate_grant_receipt_id)? .ok_or_else(|| anyhow!("Hands consequence requires its persisted Substrate Gate grant"))?; + let authorities = cache + .get_all::()? + .into_iter() + .filter(|authority| authority.hands_intent_id == intent.intent_id) + .collect::>(); + if authorities.len() != 1 { + return Err(anyhow!( + "Hands consequence requires exactly one repo frontier authority for its intent" + )); + } + let authority = &authorities[0]; + let route = cache + .get::(&authority.route_id)? + .ok_or_else(|| anyhow!("Hands consequence requires its persisted repo frontier route"))?; + let model_entry = cache + .get::(crate::MEMORY_GRAPH_KEY)? + .ok_or_else(|| anyhow!("Hands consequence requires the current repo model"))?; + crate::validate_memory_graph_entry(&model_entry)?; + let model = model_entry.snapshot()?; let paths_covered = changed_paths.iter().all(|path| { grant.granted_paths.iter().any(|granted| { granted == "." @@ -1758,6 +2979,24 @@ fn validate_hands_consequence_grant( .any(|allowed| allowed == operation) || stated_grant_id.is_some_and(|id| id != grant.receipt_id) || !paths_covered + || authority.schema_version != REPO_FRONTIER_HANDS_AUTHORITY_SCHEMA_VERSION + || authority.contract != REPO_FRONTIER_HANDS_AUTHORITY_CONTRACT + || authority.hands_review_id != review.review_id + || authority.substrate_grant_receipt_id != grant.receipt_id + || authority.requested_paths != intent.requested_paths + || authority.route_id != route.route_id + || authority.model_revision != route.model_revision + || authority.model_hash != route.model_hash + || authority.frontier_item_id != route.frontier_item_id + || authority.frontier_item_hash != route.frontier_item_hash + || model.model_revision != route.model_revision + || crate::memory_graph_model_hash(&model)? != route.model_hash + || !changed_paths.iter().all(|path| { + authority.requested_paths.iter().any(|scope| { + path == scope + || path.starts_with(&format!("{}/", scope.trim_end_matches(['/', '\\']))) + }) + }) { return Err(anyhow!( "Hands consequence does not match its approved review and Substrate Gate grant" @@ -1794,6 +3033,7 @@ pub fn put_hands_patch_receipt( store_path: impl AsRef, receipt: &HandsPatchReceipt, ) -> Result<()> { + let store_path = store_path.as_ref(); validate_non_empty(&receipt.receipt_id, "Hands patch receipt id")?; validate_non_empty(&receipt.intent_id, "Hands patch intent")?; validate_non_empty(&receipt.review_id, "Hands patch review")?; @@ -1819,8 +3059,18 @@ pub fn put_hands_patch_receipt( let mut cache = runtime_spine_cache(store_path)?; cache.pull_all_backing_stores()?; require_identity(&cache)?; - cache.put(&receipt.receipt_id, receipt)?; - Ok(()) + let (envelope, _) = cache.prepare_entry(&receipt.receipt_id, receipt)?; + if SingleFileMessagePackBackingStore::new(store_path) + .compare_and_swap_batch(&[], vec![envelope])? + { + return Ok(()); + } + let mut reloaded = runtime_spine_cache(store_path)?; + reloaded.pull_all_backing_stores()?; + match reloaded.get::(&receipt.receipt_id)? { + Some(existing) if existing == *receipt => Ok(()), + _ => Err(anyhow!("Hands patch receipt ids are immutable")), + } } pub fn runtime_hands_patch_receipt( @@ -1837,6 +3087,7 @@ pub fn put_hands_command_receipt( store_path: impl AsRef, receipt: &HandsCommandReceipt, ) -> Result<()> { + let store_path = store_path.as_ref(); validate_non_empty(&receipt.receipt_id, "Hands command receipt id")?; validate_non_empty(&receipt.intent_id, "Hands command intent")?; validate_non_empty(&receipt.review_id, "Hands command review")?; @@ -1860,8 +3111,18 @@ pub fn put_hands_command_receipt( let mut cache = runtime_spine_cache(store_path)?; cache.pull_all_backing_stores()?; require_identity(&cache)?; - cache.put(&receipt.receipt_id, receipt)?; - Ok(()) + let (envelope, _) = cache.prepare_entry(&receipt.receipt_id, receipt)?; + if SingleFileMessagePackBackingStore::new(store_path) + .compare_and_swap_batch(&[], vec![envelope])? + { + return Ok(()); + } + let mut reloaded = runtime_spine_cache(store_path)?; + reloaded.pull_all_backing_stores()?; + match reloaded.get::(&receipt.receipt_id)? { + Some(existing) if existing == *receipt => Ok(()), + _ => Err(anyhow!("Hands command receipt ids are immutable")), + } } pub fn runtime_hands_command_receipt( @@ -1878,6 +3139,7 @@ pub fn put_hands_commit_receipt( store_path: impl AsRef, receipt: &HandsCommitReceipt, ) -> Result<()> { + let store_path = store_path.as_ref(); validate_non_empty(&receipt.receipt_id, "Hands commit receipt id")?; validate_non_empty(&receipt.intent_id, "Hands commit intent")?; validate_non_empty(&receipt.review_id, "Hands commit review")?; @@ -1901,8 +3163,18 @@ pub fn put_hands_commit_receipt( let mut cache = runtime_spine_cache(store_path)?; cache.pull_all_backing_stores()?; require_identity(&cache)?; - cache.put(&receipt.receipt_id, receipt)?; - Ok(()) + let (envelope, _) = cache.prepare_entry(&receipt.receipt_id, receipt)?; + if SingleFileMessagePackBackingStore::new(store_path) + .compare_and_swap_batch(&[], vec![envelope])? + { + return Ok(()); + } + let mut reloaded = runtime_spine_cache(store_path)?; + reloaded.pull_all_backing_stores()?; + match reloaded.get::(&receipt.receipt_id)? { + Some(existing) if existing == *receipt => Ok(()), + _ => Err(anyhow!("Hands commit receipt ids are immutable")), + } } pub fn runtime_hands_commit_receipt( @@ -2056,6 +3328,7 @@ pub fn put_soul_verdict_receipt( store_path: impl AsRef, receipt: &SoulVerdictReceipt, ) -> Result<()> { + let store_path = store_path.as_ref(); validate_non_empty(&receipt.receipt_id, "Soul verdict receipt id")?; validate_non_empty(&receipt.source_result_id, "Soul verdict source result")?; validate_non_empty(&receipt.source_job_id, "Soul verdict source job")?; @@ -2064,16 +3337,20 @@ pub fn put_soul_verdict_receipt( let mut cache = runtime_spine_cache(store_path)?; cache.pull_all_backing_stores()?; require_identity(&cache)?; - if let Some(existing) = cache.get::(&receipt.receipt_id)? { - if existing == *receipt { - return Ok(()); - } - return Err(anyhow!( - "Soul verdict receipt id already belongs to different immutable evidence" - )); + let (envelope, _) = cache.prepare_entry(&receipt.receipt_id, receipt)?; + if SingleFileMessagePackBackingStore::new(store_path) + .compare_and_swap_batch(&[], vec![envelope])? + { + return Ok(()); + } + let mut reloaded = runtime_spine_cache(store_path)?; + reloaded.pull_all_backing_stores()?; + match reloaded.get::(&receipt.receipt_id)? { + Some(existing) if existing == *receipt => Ok(()), + _ => Err(anyhow!( + "Soul verdict receipt id already belongs to different immutable evidence" + )), } - cache.put(&receipt.receipt_id, receipt)?; - Ok(()) } pub fn runtime_soul_verdict_receipt( @@ -3869,6 +5146,35 @@ fn epiphany_mutation_contracts() -> Vec { "The unified memory graph is typed durable state; Qdrant embeddings are rebuildable cache, not canonical memory.", ], ), + mutation_contract( + REPO_MODEL_ADMISSION_REVIEW_TYPE, + REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION, + vec![CultNetDocumentOperation::DocumentPut], + CultNetMutationAuthority::Runtime, + vec![], + vec![REPO_MODEL_ADMISSION_RECEIPT_TYPE], + vec![ + "Specialized Mind review binds one immutable Modeling result and exact repo-model patch.", + ], + ), + mutation_contract( + REPO_MODEL_ADMISSION_RECEIPT_TYPE, + REPO_MODEL_ADMISSION_RECEIPT_SCHEMA_VERSION, + vec![CultNetDocumentOperation::ReceiptWatch], + CultNetMutationAuthority::ReadOnly, + vec![], + vec![], + vec!["Admission receipt proves the conditional model/review/receipt commit."], + ), + mutation_contract( + REPO_MODEL_MIGRATION_RECEIPT_TYPE, + REPO_MODEL_MIGRATION_RECEIPT_SCHEMA_VERSION, + vec![CultNetDocumentOperation::ReceiptWatch], + CultNetMutationAuthority::ReadOnly, + vec![], + vec![], + vec!["Migration receipt proves the one-time move into runtime-owned model state."], + ), mutation_contract( EPIPHANY_CULTMESH_OPERATOR_SNAPSHOT_TYPE, EPIPHANY_CULTMESH_OPERATOR_SNAPSHOT_SCHEMA_VERSION, @@ -4281,6 +5587,1076 @@ mod tests { use cultnet_rs::decode_cultnet_message_from_slice; use tempfile::tempdir; + fn repo_model_bootstrap() -> crate::EpiphanyMemoryGraphSnapshot { + crate::EpiphanyMemoryGraphSnapshot { + schema_version: Some(crate::MEMORY_GRAPH_SCHEMA_VERSION.to_string()), + graph_id: "runtime-repo-model".to_string(), + domains: vec![crate::EpiphanyMemoryDomain { + id: "repo".to_string(), + profile: crate::EpiphanyMemoryProfile::RepoArchitecture, + title: "Repository".to_string(), + lifecycle: crate::EpiphanyMemoryLifecycle::Accepted, + ..Default::default() + }], + nodes: vec![crate::EpiphanyMemoryNode { + id: "claim-runtime-model".to_string(), + domain_id: "repo".to_string(), + profile: crate::EpiphanyMemoryProfile::RepoArchitecture, + kind: crate::EpiphanyMemoryNodeKind::RuntimeContract, + title: "Runtime model".to_string(), + claim: "Runtime spine owns admitted repository model state.".to_string(), + question: "Which patch is admitted next?".to_string(), + action_implication: "Require specialized Mind admission.".to_string(), + source_hashes: vec!["anchor:missing".to_string()], + lifecycle: crate::EpiphanyMemoryLifecycle::Accepted, + ..Default::default() + }], + ..Default::default() + } + } + + fn repo_model_result_and_review( + result_id: &str, + job_id: &str, + current: &crate::EpiphanyMemoryGraphSnapshot, + review_id: &str, + ) -> Result<(EpiphanyRuntimeRoleWorkerResult, RepoModelAdmissionReview)> { + let patch = crate::RepoModelPatch { + patch_id: format!("patch-{result_id}"), + base_revision: current.model_revision, + base_hash: crate::memory_graph_model_hash(current)?, + applied_at: "2026-07-13T04:00:00Z".to_string(), + purpose: crate::RepoModelPatchPurpose::Evolution, + operations: vec![crate::RepoModelPatchOperation::UpsertFrontier { + item: crate::RepoFrontierItem { + id: format!("frontier-{result_id}"), + migration_body: "Carry admitted Modeling anatomy forward.".to_string(), + question: "Does the specialized review bind this patch?".to_string(), + gap: "Generic acceptance cannot own repository anatomy.".to_string(), + target_claim_ids: vec!["claim-runtime-model".to_string()], + source_scope: vec!["epiphany-core/src/runtime_spine.rs".to_string()], + recommended_next_organ: "Hands".to_string(), + status: crate::RepoFrontierStatus::Active, + ..Default::default() + }, + }], + }; + let patch_bytes = rmp_serde::to_vec_named(&patch)?; + let result = EpiphanyRuntimeRoleWorkerResult { + schema_version: RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION.to_string(), + result_id: result_id.to_string(), + job_id: job_id.to_string(), + role_id: "modeling".to_string(), + verdict: "checkpoint-ready".to_string(), + summary: "Proposed a typed repository model patch.".to_string(), + next_safe_move: "Mind review.".to_string(), + checkpoint_summary: None, + scratch_summary: None, + files_inspected: vec!["epiphany-core/src/runtime_spine.rs".to_string()], + frontier_node_ids: vec!["claim-runtime-model".to_string()], + evidence_ids: vec!["evidence-runtime-model".to_string()], + 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: BTreeMap::new(), + repo_model_patch_msgpack: Some(patch_bytes.clone()), + verification_request_id: None, + frontier_route_id: None, + repo_frontier_modeling_request_id: None, + }; + let review = RepoModelAdmissionReview { + schema_version: REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION.to_string(), + review_id: review_id.to_string(), + result_id: result_id.to_string(), + job_id: job_id.to_string(), + patch_id: patch.patch_id, + patch_sha256: format!("{:x}", Sha256::digest(&patch_bytes)), + base_revision: patch.base_revision, + base_hash: patch.base_hash, + decision: MindGatewayDecision::Accept, + evidence_ids: result.evidence_ids.clone(), + reviewed_at: "2026-07-13T04:00:01Z".to_string(), + contract: REPO_MODEL_ADMISSION_CONTRACT.to_string(), + }; + Ok((result, review)) + } + + fn admit_route_and_authorize_hands( + store: &Path, + intent: &HandsActionIntent, + review: &HandsActionReview, + suffix: &str, + ) -> Result<(RepoFrontierRoute, RepoFrontierHandsAuthority)> { + let bootstrap = repo_model_bootstrap(); + let legacy = store.with_extension(format!("{suffix}.legacy.msgpack")); + let (current, _) = + ensure_runtime_repo_model(store, &legacy, &bootstrap, "2026-07-13T04:00:00Z")?; + let (mut result, mut admission_review) = repo_model_result_and_review( + &format!("route-result-{suffix}"), + &format!("route-job-{suffix}"), + ¤t, + &format!("route-review-{suffix}"), + )?; + 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.source_scope = intent.requested_paths.clone(); + let patch_bytes = rmp_serde::to_vec_named(&patch)?; + result.repo_model_patch_msgpack = Some(patch_bytes.clone()); + admission_review.patch_sha256 = format!("{:x}", Sha256::digest(&patch_bytes)); + put_runtime_role_worker_result(store, &result)?; + commit_repo_model_admission(store, &result.result_id, &admission_review)?; + let route = select_and_commit_repo_frontier_route(store, "2026-07-13T04:00:02Z")?; + let authority = RepoFrontierHandsAuthority { + schema_version: REPO_FRONTIER_HANDS_AUTHORITY_SCHEMA_VERSION.to_string(), + authority_id: format!("route-authority-{suffix}"), + 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: intent.substrate_gate_grant_receipt_id.clone(), + requested_paths: intent.requested_paths.clone(), + granted_at: "2026-07-13T04:00:03Z".to_string(), + contract: REPO_FRONTIER_HANDS_AUTHORITY_CONTRACT.to_string(), + }; + put_repo_frontier_hands_authority(store, &authority)?; + Ok((route, authority)) + } + + struct FrontierVerdictFixture { + store: std::path::PathBuf, + route: RepoFrontierRoute, + request: RepoFrontierVerificationRequest, + verdict: SoulVerdictReceipt, + modeling_request: RepoFrontierModelingRequest, + current: crate::EpiphanyMemoryGraphSnapshot, + } + + fn frontier_verdict_fixture( + root: &Path, + suffix: &str, + verdict_text: &str, + ) -> Result { + let store = root.join(format!("runtime-{suffix}.cc")); + initialize_runtime_spine( + &store, + RuntimeSpineInitOptions { + runtime_id: format!("verdict-{suffix}"), + display_name: "Verdict incorporation".to_string(), + created_at: "2026-07-13T06:00:00Z".to_string(), + }, + )?; + let paths = vec!["epiphany-core/src/runtime_spine.rs".to_string()]; + let grant = crate::substrate_gate_coordinator_implementation_grant( + format!("grant-{suffix}"), + format!("hands-job-{suffix}"), + paths.clone(), + "2026-07-13T06:00:01Z".to_string(), + ); + put_substrate_gate_repo_access_grant_receipt(&store, &grant)?; + let intent = HandsActionIntent { + schema_version: crate::HANDS_ACTION_INTENT_SCHEMA_VERSION.to_string(), + intent_id: format!("intent-{suffix}"), + runtime_job_id: format!("hands-job-{suffix}"), + binding_id: "implementation-worker".to_string(), + role: "epiphany-hands".to_string(), + authority_scope: "epiphany.role.implementation".to_string(), + requested_action: "patch".to_string(), + requested_paths: paths.clone(), + substrate_gate_grant_receipt_id: grant.receipt_id.clone(), + requested_at: "2026-07-13T06:00:02Z".to_string(), + contract: "test Hands intent".to_string(), + }; + put_hands_action_intent(&store, &intent)?; + let review = crate::hands_action_review_for_intent( + format!("hands-review-{suffix}"), + &intent, + "approved".to_string(), + vec![ + "patch".to_string(), + "command".to_string(), + "commit".to_string(), + ], + vec!["exact route scope".to_string()], + "2026-07-13T06:00:03Z".to_string(), + ); + put_hands_action_review(&store, &review)?; + let (route, _) = admit_route_and_authorize_hands(&store, &intent, &review, suffix)?; + let patch_receipt = crate::hands_patch_receipt_for_review( + format!("hands-patch-{suffix}"), + &intent, + &review, + paths.clone(), + "patch".to_string(), + "2026-07-13T06:00:04Z".to_string(), + ); + put_hands_patch_receipt(&store, &patch_receipt)?; + let command_receipt = crate::hands_command_receipt_for_review( + format!("hands-command-{suffix}"), + &intent, + &review, + "cargo test".to_string(), + "0".to_string(), + "stdout".to_string(), + "stderr".to_string(), + "green".to_string(), + "2026-07-13T06:00:05Z".to_string(), + ); + put_hands_command_receipt(&store, &command_receipt)?; + let commit_receipt = crate::hands_commit_receipt_for_review( + format!("hands-commit-{suffix}"), + &intent, + &review, + "abc123".to_string(), + "main".to_string(), + paths, + "commit".to_string(), + "2026-07-13T06:00:06Z".to_string(), + ); + put_hands_commit_receipt(&store, &commit_receipt)?; + let chain = runtime_latest_hands_receipt_chain_after(&store, "2026-07-13T05:59:59Z")? + .expect("complete Hands chain"); + let request = commit_repo_frontier_verification_request_for_chain( + &store, + &chain, + "2026-07-13T06:00:07Z", + )?; + let verification_result = EpiphanyRuntimeRoleWorkerResult { + schema_version: RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION.to_string(), + result_id: format!("verification-result-{suffix}"), + job_id: format!("verification-job-{suffix}"), + role_id: "verification".to_string(), + verdict: verdict_text.to_string(), + summary: format!("Soul says {verdict_text}."), + next_safe_move: "Return verdict to Modeling.".to_string(), + checkpoint_summary: None, + scratch_summary: None, + files_inspected: Vec::new(), + frontier_node_ids: Vec::new(), + evidence_ids: vec![format!("verification-evidence-{suffix}")], + 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: BTreeMap::new(), + repo_model_patch_msgpack: None, + verification_request_id: Some(request.request_id.clone()), + frontier_route_id: Some(route.route_id.clone()), + repo_frontier_modeling_request_id: None, + }; + put_runtime_role_worker_result(&store, &verification_result)?; + let verdict = SoulVerdictReceipt { + schema_version: SOUL_VERDICT_RECEIPT_SCHEMA_VERSION.to_string(), + receipt_id: format!("soul-verdict-{suffix}"), + source_result_id: verification_result.result_id.clone(), + source_job_id: verification_result.job_id.clone(), + verdict: verdict_text.to_string(), + summary: verification_result.summary.clone(), + evidence_ids: verification_result.evidence_ids.clone(), + risks: verification_result.risks.clone(), + emitted_at: "2026-07-13T06:00:08Z".to_string(), + contract: "accepted exact Verification finding".to_string(), + verification_request_id: request.request_id.clone(), + frontier_route_id: route.route_id.clone(), + }; + put_soul_verdict_receipt(&store, &verdict)?; + let acceptance = epiphany_state_model::EpiphanyAcceptanceReceipt { + id: format!("verification-acceptance-{suffix}"), + result_id: verification_result.result_id.clone(), + job_id: verification_result.job_id.clone(), + binding_id: "verification-worker".to_string(), + surface: "roleAccept".to_string(), + role_id: "verification".to_string(), + status: "accepted".to_string(), + accepted_at: "2026-07-13T06:00:08Z".to_string(), + ..Default::default() + }; + let accepted_state = epiphany_state_model::EpiphanyThreadState { + revision: 1, + acceptance_receipts: vec![acceptance.clone()], + ..Default::default() + }; + let mut state_cache = runtime_spine_cache(&store)?; + state_cache.put( + crate::THREAD_STATE_KEY, + &crate::EpiphanyThreadStateEntry::from_state("fixture-thread", &accepted_state)?, + )?; + let modeling_request = commit_repo_frontier_modeling_request(&store, &acceptance)?; + let mut cache = runtime_spine_cache(&store)?; + cache.pull_all_backing_stores()?; + let current = cache + .get::(crate::MEMORY_GRAPH_KEY)? + .expect("current model") + .snapshot()?; + Ok(FrontierVerdictFixture { + store, + route, + request, + verdict, + modeling_request, + current, + }) + } + + fn incorporation_result_and_review( + fixture: &FrontierVerdictFixture, + suffix: &str, + ) -> Result<(EpiphanyRuntimeRoleWorkerResult, RepoModelAdmissionReview)> { + let mut item = fixture + .current + .frontier + .iter() + .find(|item| item.id == fixture.route.frontier_item_id) + .expect("routed item") + .clone(); + if fixture.verdict.verdict == "pass" { + item.status = crate::RepoFrontierStatus::Resolved; + } else { + item.status = crate::RepoFrontierStatus::Blocked; + item.gap = format!( + "Soul verdict {} requires another cut.", + fixture.verdict.verdict + ); + } + item.updated_at = Some("2026-07-13T06:00:09Z".to_string()); + item.evidence_refs.push(fixture.request.request_id.clone()); + item.evidence_refs.push(fixture.verdict.receipt_id.clone()); + item.evidence_refs.sort(); + item.evidence_refs.dedup(); + let patch = crate::RepoModelPatch { + patch_id: format!("incorporation-patch-{suffix}"), + base_revision: fixture.current.model_revision, + base_hash: crate::memory_graph_model_hash(&fixture.current)?, + applied_at: "2026-07-13T06:00:09Z".to_string(), + purpose: crate::RepoModelPatchPurpose::IncorporateFrontierVerdict { + route_id: fixture.route.route_id.clone(), + soul_verdict_receipt_id: fixture.verdict.receipt_id.clone(), + }, + operations: vec![crate::RepoModelPatchOperation::ReviseFrontier { item }], + }; + let bytes = rmp_serde::to_vec_named(&patch)?; + let result = EpiphanyRuntimeRoleWorkerResult { + schema_version: RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION.to_string(), + result_id: format!("incorporation-result-{suffix}"), + job_id: format!("incorporation-job-{suffix}"), + role_id: "modeling".to_string(), + verdict: "checkpoint-ready".to_string(), + summary: "Incorporate exact Soul verdict.".to_string(), + next_safe_move: "Mind admission.".to_string(), + checkpoint_summary: None, + scratch_summary: None, + files_inspected: Vec::new(), + frontier_node_ids: Vec::new(), + evidence_ids: vec![fixture.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: BTreeMap::new(), + repo_model_patch_msgpack: Some(bytes.clone()), + verification_request_id: None, + frontier_route_id: None, + repo_frontier_modeling_request_id: Some(fixture.modeling_request.request_id.clone()), + }; + let review = RepoModelAdmissionReview { + schema_version: REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION.to_string(), + review_id: format!("incorporation-review-{suffix}"), + result_id: result.result_id.clone(), + job_id: result.job_id.clone(), + patch_id: patch.patch_id, + patch_sha256: format!("{:x}", Sha256::digest(&bytes)), + base_revision: patch.base_revision, + base_hash: patch.base_hash, + decision: MindGatewayDecision::Accept, + evidence_ids: result.evidence_ids.clone(), + reviewed_at: "2026-07-13T06:00:10Z".to_string(), + contract: REPO_MODEL_ADMISSION_CONTRACT.to_string(), + }; + Ok((result, review)) + } + + #[test] + fn repo_model_migration_and_specialized_admission_are_atomic_and_bound() -> Result<()> { + let temp = tempdir()?; + let store = temp.path().join("runtime.cc"); + initialize_runtime_spine( + &store, + RuntimeSpineInitOptions { + runtime_id: "repo-model-test".to_string(), + display_name: "Repo Model Test".to_string(), + created_at: "2026-07-13T00:00:00Z".to_string(), + }, + )?; + let bootstrap = repo_model_bootstrap(); + let (current, migration) = ensure_runtime_repo_model( + &store, + temp.path().join("missing-legacy.cc"), + &bootstrap, + "2026-07-13T03:00:00Z", + )?; + assert_eq!(migration.imported_revision, 0); + let (result, review) = + repo_model_result_and_review("model-result-1", "model-job-1", ¤t, "review-1")?; + put_runtime_role_worker_result(&store, &result)?; + let receipt = commit_repo_model_admission(&store, &result.result_id, &review)?; + assert_eq!(receipt.admitted_revision, 1); + assert_eq!( + commit_repo_model_admission(&store, &result.result_id, &review)?, + receipt + ); + let (still_admitted, same_migration) = ensure_runtime_repo_model( + &store, + temp.path().join("missing-legacy.cc"), + &repo_model_bootstrap(), + "2026-07-13T05:00:00Z", + )?; + assert_eq!(still_admitted.model_revision, 1); + assert_eq!(same_migration, migration); + + let bytes_before = fs::read(&store)?; + let mut swapped_result = review.clone(); + swapped_result.result_id = "other-result".to_string(); + assert!(commit_repo_model_admission(&store, &result.result_id, &swapped_result).is_err()); + let mut swapped_patch = review.clone(); + swapped_patch.review_id = "review-swapped-patch".to_string(); + swapped_patch.patch_sha256 = "0".repeat(64); + assert!(commit_repo_model_admission(&store, &result.result_id, &swapped_patch).is_err()); + assert_eq!(fs::read(&store)?, bytes_before); + + let mut admitted_cache = runtime_spine_cache(&store)?; + admitted_cache.pull_all_backing_stores()?; + let admitted = admitted_cache + .get::(crate::MEMORY_GRAPH_KEY)? + .expect("admitted model") + .snapshot()?; + let (stale_result, stale_review) = repo_model_result_and_review( + "model-result-stale", + "model-job-stale", + ¤t, + "review-stale", + )?; + put_runtime_role_worker_result(&store, &stale_result)?; + let stale_bytes = fs::read(&store)?; + assert!( + commit_repo_model_admission(&store, &stale_result.result_id, &stale_review).is_err() + ); + assert_eq!(fs::read(&store)?, stale_bytes); + assert_eq!(admitted.model_revision, 1); + + let (collision_result, collision_review) = repo_model_result_and_review( + "model-result-collision", + "model-job-collision", + &admitted, + "review-collision", + )?; + put_runtime_role_worker_result(&store, &collision_result)?; + let mut counterfeit = collision_review.clone(); + counterfeit.decision = MindGatewayDecision::Hold; + let mut cache = runtime_spine_cache(&store)?; + cache.pull_all_backing_stores()?; + cache.put(&counterfeit.review_id, &counterfeit)?; + let collision_bytes = fs::read(&store)?; + assert!( + commit_repo_model_admission(&store, &collision_result.result_id, &collision_review) + .is_err() + ); + assert_eq!(fs::read(&store)?, collision_bytes); + Ok(()) + } + + #[test] + fn evolution_cannot_bypass_routes_or_own_frontier_verdict_lifecycle() -> Result<()> { + let routed_temp = tempdir()?; + let routed = frontier_verdict_fixture(routed_temp.path(), "evolution-route", "pass")?; + let (routed_result, routed_review) = repo_model_result_and_review( + "evolution-after-route", + "evolution-after-route-job", + &routed.current, + "evolution-after-route-review", + )?; + put_runtime_role_worker_result(&routed.store, &routed_result)?; + let before = fs::read(&routed.store)?; + assert!( + commit_repo_model_admission(&routed.store, &routed_result.result_id, &routed_review) + .is_err() + ); + assert_eq!(fs::read(&routed.store)?, before); + + let temp = tempdir()?; + let store = temp.path().join("evolution-lifecycle.cc"); + initialize_runtime_spine( + &store, + RuntimeSpineInitOptions { + runtime_id: "evolution-lifecycle".to_string(), + display_name: "Evolution lifecycle".to_string(), + created_at: "2026-07-13T08:00:00Z".to_string(), + }, + )?; + let (base, _) = ensure_runtime_repo_model( + &store, + temp.path().join("legacy.cc"), + &repo_model_bootstrap(), + "2026-07-13T08:00:00Z", + )?; + let (seed_result, seed_review) = repo_model_result_and_review( + "evolution-seed", + "evolution-seed-job", + &base, + "evolution-seed-review", + )?; + put_runtime_role_worker_result(&store, &seed_result)?; + commit_repo_model_admission(&store, &seed_result.result_id, &seed_review)?; + let mut cache = runtime_spine_cache(&store)?; + cache.pull_all_backing_stores()?; + let current = cache + .get::(crate::MEMORY_GRAPH_KEY)? + .unwrap() + .snapshot()?; + for (suffix, operation) in [ + ("resolved", { + let mut item = current.frontier[0].clone(); + item.status = crate::RepoFrontierStatus::Resolved; + crate::RepoModelPatchOperation::ReviseFrontier { item } + }), + ( + "retire", + crate::RepoModelPatchOperation::RetireFrontier { + item_id: current.frontier[0].id.clone(), + retired_at: None, + superseded_by: None, + }, + ), + ] { + let (mut result, mut review) = repo_model_result_and_review( + &format!("evolution-{suffix}"), + &format!("evolution-{suffix}-job"), + ¤t, + &format!("evolution-{suffix}-review"), + )?; + let mut patch = result.repo_model_patch()?.unwrap(); + patch.operations = vec![operation]; + 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)?; + let before = fs::read(&store)?; + assert!(commit_repo_model_admission(&store, &result.result_id, &review).is_err()); + assert_eq!(fs::read(&store)?, before); + } + Ok(()) + } + + #[test] + fn repo_model_incorporates_pass_and_nonpass_soul_verdicts_causally() -> Result<()> { + for verdict in ["pass", "needs-review", "needs-evidence", "fail"] { + let temp = tempdir()?; + let fixture = frontier_verdict_fixture(temp.path(), verdict, verdict)?; + let (result, review) = incorporation_result_and_review(&fixture, verdict)?; + put_runtime_role_worker_result(&fixture.store, &result)?; + let receipt = commit_repo_model_admission(&fixture.store, &result.result_id, &review)?; + assert_eq!(receipt.purpose, result.repo_model_patch()?.unwrap().purpose); + assert_eq!(receipt.frontier_route_id, fixture.route.route_id); + assert_eq!(receipt.verification_request_id, fixture.request.request_id); + assert_eq!(receipt.soul_verdict_receipt_id, fixture.verdict.receipt_id); + assert_eq!( + receipt.frontier_modeling_request_id, + fixture.modeling_request.request_id + ); + assert_eq!( + commit_repo_model_admission(&fixture.store, &result.result_id, &review)?, + receipt + ); + let mut cache = runtime_spine_cache(&fixture.store)?; + cache.pull_all_backing_stores()?; + let admitted = cache + .get::(crate::MEMORY_GRAPH_KEY)? + .unwrap() + .snapshot()?; + let item = admitted + .frontier + .iter() + .find(|item| item.id == fixture.route.frontier_item_id) + .unwrap(); + let expected = if verdict == "pass" { + crate::RepoFrontierStatus::Resolved + } else { + crate::RepoFrontierStatus::Blocked + }; + assert_eq!(item.status, expected); + if verdict != "pass" { + assert!(!item.gap.trim().is_empty()); + } + assert!(!runtime_has_actionable_hands_frontier(&fixture.store)?); + } + Ok(()) + } + + #[test] + fn frontier_modeling_request_reloads_exact_accepted_result_despite_adjacent_verdict() + -> Result<()> { + let temp = tempdir()?; + let fixture = frontier_verdict_fixture(temp.path(), "request-adjacency", "pass")?; + let adjacent = EpiphanyRuntimeRoleWorkerResult { + schema_version: RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION.to_string(), + result_id: "adjacent-verification-result".to_string(), + job_id: "adjacent-verification-job".to_string(), + role_id: "verification".to_string(), + verdict: "fail".to_string(), + summary: "Nearby but not accepted.".to_string(), + next_safe_move: "Remain unselected.".to_string(), + checkpoint_summary: None, + scratch_summary: None, + files_inspected: Vec::new(), + frontier_node_ids: Vec::new(), + evidence_ids: vec!["adjacent-evidence".to_string()], + 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: BTreeMap::new(), + repo_model_patch_msgpack: None, + verification_request_id: Some(fixture.request.request_id.clone()), + frontier_route_id: Some(fixture.route.route_id.clone()), + repo_frontier_modeling_request_id: None, + }; + put_runtime_role_worker_result(&fixture.store, &adjacent)?; + put_soul_verdict_receipt( + &fixture.store, + &SoulVerdictReceipt { + schema_version: SOUL_VERDICT_RECEIPT_SCHEMA_VERSION.to_string(), + receipt_id: "adjacent-soul-verdict".to_string(), + source_result_id: adjacent.result_id.clone(), + source_job_id: adjacent.job_id.clone(), + verdict: adjacent.verdict.clone(), + summary: adjacent.summary.clone(), + evidence_ids: adjacent.evidence_ids.clone(), + risks: adjacent.risks.clone(), + emitted_at: "2026-07-13T06:00:09Z".to_string(), + contract: "unaccepted adjacent verdict".to_string(), + verification_request_id: fixture.request.request_id.clone(), + frontier_route_id: fixture.route.route_id.clone(), + }, + )?; + let accepted = epiphany_state_model::EpiphanyAcceptanceReceipt { + id: "verification-acceptance-request-adjacency".to_string(), + result_id: "verification-result-request-adjacency".to_string(), + job_id: "verification-job-request-adjacency".to_string(), + binding_id: "verification-worker".to_string(), + surface: "roleAccept".to_string(), + role_id: "verification".to_string(), + status: "accepted".to_string(), + accepted_at: "2026-07-13T06:00:08Z".to_string(), + ..Default::default() + }; + let reloaded = commit_repo_frontier_modeling_request(&fixture.store, &accepted)?; + assert_eq!(reloaded, fixture.modeling_request); + assert_eq!(reloaded.verification_result_id, accepted.result_id); + assert_ne!(reloaded.verification_result_id, adjacent.result_id); + Ok(()) + } + + #[test] + fn repo_model_verdict_incorporation_refuses_mixed_chains_and_illegal_edits_without_mutation() + -> Result<()> { + fn attempt( + fixture: &FrontierVerdictFixture, + suffix: &str, + mutate: impl FnOnce(&mut crate::RepoModelPatch), + ) -> Result<()> { + let (mut result, mut review) = incorporation_result_and_review(fixture, suffix)?; + let mut patch = result.repo_model_patch()?.unwrap(); + mutate(&mut patch); + let bytes = rmp_serde::to_vec_named(&patch)?; + result.repo_model_patch_msgpack = Some(bytes.clone()); + result.evidence_ids = match &patch.purpose { + crate::RepoModelPatchPurpose::IncorporateFrontierVerdict { + soul_verdict_receipt_id, + .. + } => vec![soul_verdict_receipt_id.clone()], + crate::RepoModelPatchPurpose::Evolution => vec!["evolution".to_string()], + }; + review.patch_id = patch.patch_id.clone(); + review.patch_sha256 = format!("{:x}", Sha256::digest(&bytes)); + review.base_revision = patch.base_revision; + review.base_hash = patch.base_hash.clone(); + review.evidence_ids = result.evidence_ids.clone(); + put_runtime_role_worker_result(&fixture.store, &result)?; + let before = fs::read(&fixture.store)?; + assert!( + commit_repo_model_admission(&fixture.store, &result.result_id, &review).is_err() + ); + assert_eq!(fs::read(&fixture.store)?, before); + Ok(()) + } + + let temp = tempdir()?; + let fixture = frontier_verdict_fixture(temp.path(), "hostile", "pass")?; + + attempt(&fixture, "swapped-route", |patch| { + patch.purpose = crate::RepoModelPatchPurpose::IncorporateFrontierVerdict { + route_id: "different-route".to_string(), + soul_verdict_receipt_id: fixture.verdict.receipt_id.clone(), + }; + })?; + + for (suffix, alter) in [ + ("swapped-request", "request"), + ("swapped-verdict-route", "route"), + ("swapped-result", "result"), + ] { + let mut counterfeit = fixture.verdict.clone(); + counterfeit.receipt_id = format!("counterfeit-{suffix}"); + match alter { + "request" => counterfeit.verification_request_id = "different-request".to_string(), + "route" => counterfeit.frontier_route_id = "different-route".to_string(), + "result" => counterfeit.source_result_id = "different-result".to_string(), + _ => unreachable!(), + } + put_soul_verdict_receipt(&fixture.store, &counterfeit)?; + attempt(&fixture, suffix, |patch| { + patch.purpose = crate::RepoModelPatchPurpose::IncorporateFrontierVerdict { + route_id: fixture.route.route_id.clone(), + soul_verdict_receipt_id: counterfeit.receipt_id.clone(), + }; + })?; + } + + attempt(&fixture, "extra-op", |patch| { + let item = fixture.current.frontier[0].clone(); + patch + .operations + .push(crate::RepoModelPatchOperation::ReviseFrontier { item }); + })?; + attempt(&fixture, "wrong-item", |patch| { + let crate::RepoModelPatchOperation::ReviseFrontier { item } = &mut patch.operations[0] + else { + unreachable!() + }; + item.id = "other-frontier".to_string(); + })?; + attempt(&fixture, "wrong-status", |patch| { + let crate::RepoModelPatchOperation::ReviseFrontier { item } = &mut patch.operations[0] + else { + unreachable!() + }; + item.status = crate::RepoFrontierStatus::Blocked; + item.gap = "still blocked".to_string(); + })?; + + let (evolution_result, evolution_review) = + incorporation_result_and_review(&fixture, "intervening-result")?; + put_runtime_role_worker_result(&fixture.store, &evolution_result)?; + commit_repo_model_admission( + &fixture.store, + &evolution_result.result_id, + &evolution_review, + )?; + let (stale_result, stale_review) = + incorporation_result_and_review(&fixture, "stale-model")?; + put_runtime_role_worker_result(&fixture.store, &stale_result)?; + let before = fs::read(&fixture.store)?; + assert!( + commit_repo_model_admission(&fixture.store, &stale_result.result_id, &stale_review) + .is_err() + ); + assert_eq!(fs::read(&fixture.store)?, before); + Ok(()) + } + + #[test] + fn repo_frontier_route_refuses_unadmitted_and_ineligible_models() -> Result<()> { + fn admitted_store_with_items( + store: &Path, + suffix: &str, + items: Vec, + ) -> Result<()> { + let bootstrap = repo_model_bootstrap(); + let legacy = store.with_extension(format!("{suffix}.legacy.msgpack")); + let (current, _) = + ensure_runtime_repo_model(store, legacy, &bootstrap, "2026-07-13T05:00:00Z")?; + let (mut result, mut review) = repo_model_result_and_review( + &format!("eligibility-result-{suffix}"), + &format!("eligibility-job-{suffix}"), + ¤t, + &format!("eligibility-review-{suffix}"), + )?; + let mut patch: crate::RepoModelPatch = + rmp_serde::from_slice(result.repo_model_patch_msgpack.as_deref().unwrap())?; + patch.operations = items + .into_iter() + .map(|item| crate::RepoModelPatchOperation::UpsertFrontier { item }) + .collect(); + let bytes = rmp_serde::to_vec_named(&patch)?; + review.patch_sha256 = format!("{:x}", Sha256::digest(&bytes)); + result.repo_model_patch_msgpack = Some(bytes); + put_runtime_role_worker_result(store, &result)?; + commit_repo_model_admission(store, &result.result_id, &review)?; + Ok(()) + } + + let unadmitted = tempdir()?; + let unadmitted_store = unadmitted.path().join("runtime.msgpack"); + initialize_runtime_spine( + &unadmitted_store, + RuntimeSpineInitOptions { + runtime_id: "route-unadmitted".to_string(), + display_name: "Route Unadmitted".to_string(), + created_at: "2026-07-13T05:00:00Z".to_string(), + }, + )?; + ensure_runtime_repo_model( + &unadmitted_store, + unadmitted.path().join("legacy.msgpack"), + &repo_model_bootstrap(), + "2026-07-13T05:00:00Z", + )?; + assert!( + select_and_commit_repo_frontier_route(&unadmitted_store, "2026-07-13T05:00:01Z") + .is_err() + ); + assert!(!runtime_has_actionable_hands_frontier(&unadmitted_store)?); + + for (suffix, status) in [ + ("proposed", crate::RepoFrontierStatus::Proposed), + ("blocked", crate::RepoFrontierStatus::Blocked), + ] { + let temp = tempdir()?; + let store = temp.path().join("runtime.msgpack"); + initialize_runtime_spine( + &store, + RuntimeSpineInitOptions { + runtime_id: format!("route-{suffix}"), + display_name: suffix.to_string(), + created_at: "2026-07-13T05:00:00Z".to_string(), + }, + )?; + admitted_store_with_items( + &store, + suffix, + vec![crate::RepoFrontierItem { + id: format!("frontier-{suffix}"), + migration_body: "body".to_string(), + question: "question".to_string(), + gap: "gap".to_string(), + target_claim_ids: vec!["claim-runtime-model".to_string()], + source_scope: vec!["epiphany-core/src".to_string()], + recommended_next_organ: "Hands".to_string(), + status, + ..Default::default() + }], + )?; + assert!(select_and_commit_repo_frontier_route(&store, "2026-07-13T05:00:02Z").is_err()); + assert!(!runtime_has_actionable_hands_frontier(&store)?); + } + + let active = tempdir()?; + let active_store = active.path().join("runtime.msgpack"); + initialize_runtime_spine( + &active_store, + RuntimeSpineInitOptions { + runtime_id: "route-active".to_string(), + display_name: "active".to_string(), + created_at: "2026-07-13T05:00:00Z".to_string(), + }, + )?; + admitted_store_with_items( + &active_store, + "active", + vec![crate::RepoFrontierItem { + id: "frontier-active".to_string(), + migration_body: "body".to_string(), + question: "question".to_string(), + gap: "gap".to_string(), + target_claim_ids: vec!["claim-runtime-model".to_string()], + source_scope: vec!["epiphany-core/src".to_string()], + recommended_next_organ: "Hands".to_string(), + status: crate::RepoFrontierStatus::Active, + ..Default::default() + }], + )?; + assert!(runtime_has_actionable_hands_frontier(&active_store)?); + assert!( + select_and_commit_repo_frontier_route(&active_store, "2026-07-13T05:00:02Z").is_ok() + ); + + let temp = tempdir()?; + let store = temp.path().join("runtime.msgpack"); + initialize_runtime_spine( + &store, + RuntimeSpineInitOptions { + runtime_id: "route-dependency".to_string(), + display_name: "dependency".to_string(), + created_at: "2026-07-13T05:00:00Z".to_string(), + }, + )?; + admitted_store_with_items( + &store, + "dependency", + vec![ + crate::RepoFrontierItem { + id: "dependency".to_string(), + migration_body: "dependency".to_string(), + question: "pending?".to_string(), + gap: "pending".to_string(), + target_claim_ids: vec!["claim-runtime-model".to_string()], + source_scope: vec!["epiphany-core/src".to_string()], + recommended_next_organ: "Eyes".to_string(), + status: crate::RepoFrontierStatus::Active, + ..Default::default() + }, + crate::RepoFrontierItem { + id: "dependent".to_string(), + migration_body: "dependent".to_string(), + question: "ready?".to_string(), + gap: "dependency unresolved".to_string(), + target_claim_ids: vec!["claim-runtime-model".to_string()], + source_scope: vec!["epiphany-core/src".to_string()], + recommended_next_organ: "Hands".to_string(), + dependency_item_ids: vec!["dependency".to_string()], + status: crate::RepoFrontierStatus::Active, + ..Default::default() + }, + ], + )?; + assert!(select_and_commit_repo_frontier_route(&store, "2026-07-13T05:00:02Z").is_err()); + assert!(!runtime_has_actionable_hands_frontier(&store)?); + Ok(()) + } + + #[test] + fn hands_authority_documents_choose_one_immutable_identity_under_race() -> Result<()> { + let temp = tempdir()?; + let store = temp.path().join("runtime.msgpack"); + initialize_runtime_spine(&store, RuntimeSpineInitOptions { + runtime_id: "authority-race".to_string(), display_name: "Authority Race".to_string(), + created_at: "2026-07-13T05:30:00Z".to_string(), + })?; + let base_grant = crate::substrate_gate_coordinator_implementation_grant( + "race-grant".to_string(), "race-job".to_string(), vec!["epiphany-core/src".to_string()], + "2026-07-13T05:30:00Z".to_string()); + let mut other_grant = base_grant.clone(); other_grant.granted_paths = vec!["epiphany-core/tests".to_string()]; + let barrier = std::sync::Arc::new(std::sync::Barrier::new(2)); + let outcomes = [base_grant.clone(), other_grant.clone()].into_iter().map(|grant| { + let path = store.clone(); let barrier = barrier.clone(); + std::thread::spawn(move || { barrier.wait(); put_substrate_gate_repo_access_grant_receipt(path, &grant) }) + }).collect::>(); + assert_eq!(outcomes.into_iter().map(|outcome| outcome.join().unwrap()).filter(Result::is_ok).count(), 1); + let winner = runtime_substrate_gate_repo_access_grant_receipt(&store, "race-grant")?.unwrap(); + + let base_intent = HandsActionIntent { schema_version: HANDS_ACTION_INTENT_SCHEMA_VERSION.to_string(), + intent_id: "race-intent".to_string(), runtime_job_id: winner.runtime_job_id.clone(), binding_id: winner.binding_id.clone(), + role: winner.role.clone(), authority_scope: winner.authority_scope.clone(), requested_action: "patch".to_string(), + requested_paths: winner.granted_paths.clone(), substrate_gate_grant_receipt_id: winner.receipt_id.clone(), + requested_at: "2026-07-13T05:30:01Z".to_string(), contract: "race intent".to_string() }; + let mut other_intent = base_intent.clone(); other_intent.requested_action = "continueImplementation".to_string(); + let barrier = std::sync::Arc::new(std::sync::Barrier::new(2)); + let outcomes = [base_intent.clone(), other_intent].into_iter().map(|intent| { + let path = store.clone(); let barrier = barrier.clone(); + std::thread::spawn(move || { barrier.wait(); put_hands_action_intent(path, &intent) }) + }).collect::>(); + assert_eq!(outcomes.into_iter().map(|outcome| outcome.join().unwrap()).filter(Result::is_ok).count(), 1); + let winner_intent = runtime_hands_action_intent(&store, "race-intent")?.unwrap(); + + let base_review = hands_action_review_for_intent("race-review".to_string(), &winner_intent, "approved".to_string(), + vec!["patch".to_string(), "command".to_string(), "commit".to_string()], vec!["race".to_string()], + "2026-07-13T05:30:02Z".to_string()); + let mut other_review = base_review.clone(); other_review.allowed_operations = vec!["patch".to_string()]; + let barrier = std::sync::Arc::new(std::sync::Barrier::new(2)); + let outcomes = [base_review, other_review].into_iter().map(|review| { + let path = store.clone(); let barrier = barrier.clone(); + std::thread::spawn(move || { barrier.wait(); put_hands_action_review(path, &review) }) + }).collect::>(); + assert_eq!(outcomes.into_iter().map(|outcome| outcome.join().unwrap()).filter(Result::is_ok).count(), 1); + Ok(()) + } + + #[test] + fn repo_frontier_hands_authority_refuses_substitution_and_retries_exactly() -> Result<()> { + let temp = tempdir()?; + let store = temp.path().join("runtime.msgpack"); + initialize_runtime_spine( + &store, + RuntimeSpineInitOptions { + runtime_id: "route-hostile".to_string(), + display_name: "Route Hostile".to_string(), + created_at: "2026-07-13T06:00:00Z".to_string(), + }, + )?; + let grant = crate::substrate_gate_coordinator_implementation_grant( + "route-hostile-grant".to_string(), + "route-hostile-job".to_string(), + vec!["epiphany-core/src".to_string()], + "2026-07-13T06:00:00Z".to_string(), + ); + put_substrate_gate_repo_access_grant_receipt(&store, &grant)?; + let intent = HandsActionIntent { + schema_version: HANDS_ACTION_INTENT_SCHEMA_VERSION.to_string(), + intent_id: "route-hostile-intent".to_string(), + 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: "patch".to_string(), + requested_paths: grant.granted_paths.clone(), + substrate_gate_grant_receipt_id: grant.receipt_id.clone(), + requested_at: "2026-07-13T06:00:01Z".to_string(), + contract: "test".to_string(), + }; + put_hands_action_intent(&store, &intent)?; + let review = hands_action_review_for_intent( + "route-hostile-review".to_string(), + &intent, + "approved".to_string(), + vec!["patch".to_string()], + vec!["test".to_string()], + "2026-07-13T06:00:02Z".to_string(), + ); + put_hands_action_review(&store, &review)?; + let (route, authority) = + admit_route_and_authorize_hands(&store, &intent, &review, "hostile")?; + assert_eq!( + select_and_commit_repo_frontier_route(&store, "2026-07-13T04:00:02Z")?, + route + ); + put_repo_frontier_hands_authority(&store, &authority)?; + + let mutations: Vec> = vec![ + Box::new(|a| a.route_id = "swapped-route".to_string()), + Box::new(|a| a.model_hash = "0".repeat(64)), + Box::new(|a| a.frontier_item_hash = "1".repeat(64)), + Box::new(|a| a.requested_paths = vec!["outside".to_string()]), + Box::new(|a| a.hands_intent_id = "swapped-intent".to_string()), + Box::new(|a| a.hands_review_id = "swapped-review".to_string()), + Box::new(|a| a.substrate_grant_receipt_id = "swapped-grant".to_string()), + ]; + for (index, mutate) in mutations.into_iter().enumerate() { + let mut counterfeit = authority.clone(); + counterfeit.authority_id = format!("counterfeit-{index}"); + mutate(&mut counterfeit); + assert!(put_repo_frontier_hands_authority(&store, &counterfeit).is_err()); + } + Ok(()) + } + struct RepoWorkAdoptionFixture { store: PathBuf, review: RepoWorkPlanAdoptionReview, @@ -4793,6 +7169,8 @@ mod tests { risks: Vec::new(), emitted_at: "2026-05-06T00:07:00Z".to_string(), contract: "Soul verdict persists as runtime-spine proof.".to_string(), + verification_request_id: String::new(), + frontier_route_id: String::new(), }; put_soul_verdict_receipt(&store, &soul_verdict)?; let continuity_recovery = ContinuityRecoveryReceipt { @@ -4827,7 +7205,7 @@ mod tests { let hands_grant = crate::substrate_gate_coordinator_implementation_grant( "substrate-grant-hands-1".to_string(), "job-implementation-1".to_string(), - vec!["src".to_string()], + vec!["src/lib.rs".to_string()], "2026-05-06T00:06:20Z".to_string(), ); put_substrate_gate_repo_access_grant_receipt(&store, &hands_grant)?; @@ -4858,6 +7236,22 @@ mod tests { "2026-05-06T00:06:40Z".to_string(), ); put_hands_action_review(&store, &hands_review)?; + put_substrate_gate_repo_access_grant_receipt(&store, &hands_grant)?; + put_hands_action_intent(&store, &hands_intent)?; + put_hands_action_review(&store, &hands_review)?; + let authority_identity_bytes = fs::read(&store)?; + let mut counterfeit_grant = hands_grant.clone(); + counterfeit_grant.granted_operations = vec!["read".to_string()]; + assert!(put_substrate_gate_repo_access_grant_receipt(&store, &counterfeit_grant).is_err()); + let mut counterfeit_intent = hands_intent.clone(); + counterfeit_intent.requested_action = "command".to_string(); + assert!(put_hands_action_intent(&store, &counterfeit_intent).is_err()); + let mut counterfeit_review = hands_review.clone(); + counterfeit_review.allowed_operations = vec!["command".to_string()]; + assert!(put_hands_action_review(&store, &counterfeit_review).is_err()); + assert_eq!(fs::read(&store)?, authority_identity_bytes); + let (frontier_route, _) = + admit_route_and_authorize_hands(&store, &hands_intent, &hands_review, "mind-review")?; let hands_patch = crate::hands_patch_receipt_for_review( "hands-patch-1".to_string(), &hands_intent, @@ -4918,6 +7312,20 @@ mod tests { let stored_commit = runtime_hands_commit_receipt(&store, "hands-commit-1")? .expect("Hands commit receipt should persist"); assert_eq!(stored_commit, hands_commit); + put_hands_patch_receipt(&store, &hands_patch)?; + put_hands_command_receipt(&store, &hands_command)?; + put_hands_commit_receipt(&store, &hands_commit)?; + let immutable_bytes = fs::read(&store)?; + let mut counterfeit_patch = hands_patch.clone(); + counterfeit_patch.summary = "counterfeit patch".to_string(); + assert!(put_hands_patch_receipt(&store, &counterfeit_patch).is_err()); + let mut counterfeit_command = hands_command.clone(); + counterfeit_command.summary = "counterfeit command".to_string(); + assert!(put_hands_command_receipt(&store, &counterfeit_command).is_err()); + let mut counterfeit_commit = hands_commit.clone(); + counterfeit_commit.summary = "counterfeit commit".to_string(); + assert!(put_hands_commit_receipt(&store, &counterfeit_commit).is_err()); + assert_eq!(fs::read(&store)?, immutable_bytes); let stored_pr = runtime_hands_pr_receipt(&store, "hands-pr-1")? .expect("Hands PR receipt should persist"); assert_eq!(stored_pr, hands_pr); @@ -4931,6 +7339,36 @@ mod tests { assert_eq!(hands_chain.command_receipt_id, "hands-command-1"); assert_eq!(hands_chain.commit_receipt_id, "hands-commit-1"); assert_eq!(hands_chain.exit_code, "0"); + let verification_request = commit_repo_frontier_verification_request_for_chain( + &store, + &hands_chain, + "2026-05-06T00:07:11Z", + )?; + assert_eq!(verification_request.route_id, frontier_route.route_id); + assert_eq!(verification_request.hands_patch_receipt_id, "hands-patch-1"); + assert_eq!( + verification_request.hands_command_receipt_id, + "hands-command-1" + ); + assert_eq!( + verification_request.hands_commit_receipt_id, + "hands-commit-1" + ); + let bytes_before_hostile_request = fs::read(&store)?; + let mut missing_receipt = verification_request.clone(); + missing_receipt.request_id = "verification-missing-receipt".to_string(); + missing_receipt.hands_command_receipt_id = "missing-command".to_string(); + assert!(put_repo_frontier_verification_request(&store, &missing_receipt).is_err()); + let mut swapped_route = verification_request.clone(); + swapped_route.request_id = "verification-swapped-route".to_string(); + swapped_route.route_id = "other-route".to_string(); + assert!(put_repo_frontier_verification_request(&store, &swapped_route).is_err()); + let mut mixed_chain = verification_request.clone(); + mixed_chain.request_id = "verification-mixed-chain".to_string(); + mixed_chain.hands_patch_receipt_id = "hands-patch-1".to_string(); + mixed_chain.hands_commit_receipt_id = "missing-commit".to_string(); + assert!(put_repo_frontier_verification_request(&store, &mixed_chain).is_err()); + assert_eq!(fs::read(&store)?, bytes_before_hostile_request); assert!(!runtime_hands_receipt_chain_after( &store, "2026-05-06T00:07:15Z" @@ -5024,7 +7462,7 @@ mod tests { role: "epiphany-hands".to_string(), authority_scope: "epiphany.role.implementation".to_string(), requested_action: "continueImplementation".to_string(), - requested_paths: vec![".".to_string()], + requested_paths: vec!["new.rs".to_string(), "old.rs".to_string()], 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(), @@ -5034,7 +7472,7 @@ mod tests { &crate::substrate_gate_coordinator_implementation_grant( "substrate-grant-reused".to_string(), "hands-job-reused".to_string(), - vec![".".to_string()], + vec!["new.rs".to_string(), "old.rs".to_string()], "2026-06-13T00:00:00Z".to_string(), ), )?; @@ -5052,6 +7490,7 @@ mod tests { "2026-06-13T00:00:02Z".to_string(), ); put_hands_action_review(&store, &review)?; + admit_route_and_authorize_hands(&store, &intent, &review, "reused")?; put_hands_patch_receipt( &store, diff --git a/epiphany-core/src/soul_gateway.rs b/epiphany-core/src/soul_gateway.rs index 90b9567f..e7088cd8 100644 --- a/epiphany-core/src/soul_gateway.rs +++ b/epiphany-core/src/soul_gateway.rs @@ -11,8 +11,14 @@ pub const SOUL_REVIEW_RECEIPT_TYPE: &str = "epiphany.soul.review_receipt"; pub const SOUL_VERIFICATION_REFUSAL_RECEIPT_TYPE: &str = "epiphany.soul.verification_refusal_receipt"; pub const SOUL_VERIFICATION_REQUEST_SCHEMA_VERSION: &str = "epiphany.soul.verification_request.v0"; +pub const REPO_FRONTIER_VERIFICATION_REQUEST_TYPE: &str = + "epiphany.soul.repo_frontier_verification_request"; +pub const REPO_FRONTIER_VERIFICATION_REQUEST_SCHEMA_VERSION: &str = + "epiphany.soul.repo_frontier_verification_request.v0"; +pub const REPO_FRONTIER_VERIFICATION_REQUEST_CONTRACT: &str = + "epiphany.repo_frontier_verification_request.v0"; pub const SOUL_INVARIANT_CHECK_SCHEMA_VERSION: &str = "epiphany.soul.invariant_check.v0"; -pub const SOUL_VERDICT_RECEIPT_SCHEMA_VERSION: &str = "epiphany.soul.verdict_receipt.v0"; +pub const SOUL_VERDICT_RECEIPT_SCHEMA_VERSION: &str = "epiphany.soul.verdict_receipt.v1"; pub const SOUL_REGRESSION_RECEIPT_SCHEMA_VERSION: &str = "epiphany.soul.regression_receipt.v0"; pub const SOUL_REVIEW_RECEIPT_SCHEMA_VERSION: &str = "epiphany.soul.review_receipt.v0"; pub const SOUL_VERIFICATION_REFUSAL_RECEIPT_SCHEMA_VERSION: &str = @@ -41,6 +47,46 @@ pub struct SoulVerdictReceipt { pub emitted_at: String, #[cultcache(key = 9)] pub contract: String, + #[cultcache(key = 10, default)] + pub verification_request_id: String, + #[cultcache(key = 11, default)] + pub frontier_route_id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.soul.repo_frontier_verification_request", + schema = "RepoFrontierVerificationRequest" +)] +pub struct RepoFrontierVerificationRequest { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub request_id: String, + #[cultcache(key = 2)] + pub route_id: String, + #[cultcache(key = 3)] + pub model_revision: u64, + #[cultcache(key = 4)] + pub model_hash: String, + #[cultcache(key = 5)] + pub frontier_item_id: String, + #[cultcache(key = 6)] + pub frontier_item_hash: String, + #[cultcache(key = 7)] + pub hands_intent_id: String, + #[cultcache(key = 8)] + pub hands_review_id: String, + #[cultcache(key = 9)] + pub hands_patch_receipt_id: String, + #[cultcache(key = 10)] + pub hands_command_receipt_id: String, + #[cultcache(key = 11)] + pub hands_commit_receipt_id: String, + #[cultcache(key = 12)] + pub requested_at: String, + #[cultcache(key = 13)] + pub contract: String, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -154,6 +200,8 @@ pub fn soul_verdict_receipt_from_verification_finding( risks: finding.risks.clone(), emitted_at, contract: "Soul verdict emitted from a reviewed Verification lane finding; it is proof of verification judgment before Mind admission.".to_string(), + verification_request_id: finding.verification_request_id.clone().unwrap_or_default(), + frontier_route_id: finding.frontier_route_id.clone().unwrap_or_default(), } } @@ -202,10 +250,13 @@ mod tests { evidence_gaps: Vec::new(), risks: Vec::new(), state_patch: None, + repo_model_patch: None, self_patch: None, self_persistence: None, job_error: None, item_error: None, + verification_request_id: Some("verification-request-1".to_string()), + frontier_route_id: Some("frontier-route-1".to_string()), }; let receipt = soul_verdict_receipt_from_verification_finding( "soul-verdict-1".to_string(), @@ -214,5 +265,7 @@ mod tests { ); assert_eq!(receipt.verdict, "passed"); assert!(receipt.evidence_ids.contains(&"ev-check".to_string())); + assert_eq!(receipt.verification_request_id, "verification-request-1"); + assert_eq!(receipt.frontier_route_id, "frontier-route-1"); } } diff --git a/epiphany-core/src/surfaces/coordinator.rs b/epiphany-core/src/surfaces/coordinator.rs index e83381f0..567d5d30 100644 --- a/epiphany-core/src/surfaces/coordinator.rs +++ b/epiphany-core/src/surfaces/coordinator.rs @@ -146,6 +146,9 @@ pub struct EpiphanyCoordinatorInput { pub verification_result_allows_implementation: bool, pub verification_result_needs_evidence: bool, pub reorient_finding_accepted: bool, + /// True only when the canonical runtime RepoModel has exactly one current + /// admission and an Active, dependency-ready Hands frontier item. + pub hands_frontier_ready: bool, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -186,6 +189,7 @@ pub struct EpiphanyCoordinatorStatusInput { pub verification_result_allows_implementation: bool, pub verification_result_needs_evidence: bool, pub reorient_finding_accepted: bool, + pub hands_frontier_ready: bool, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -281,6 +285,7 @@ pub fn derive_coordinator_status( verification_result_allows_implementation: input.verification_result_allows_implementation, verification_result_needs_evidence: input.verification_result_needs_evidence, reorient_finding_accepted: input.reorient_finding_accepted, + hands_frontier_ready: input.hands_frontier_ready, }); EpiphanyCoordinatorStatus { decision, @@ -974,6 +979,7 @@ pub fn recommend_coordinator_action( && input.verification_result_needs_evidence && input.modeling_result_accepted && input.verification_result_covers_current_modeling + && input.hands_frontier_ready { return build( EpiphanyCoordinatorAction::ContinueImplementation, @@ -991,6 +997,7 @@ pub fn recommend_coordinator_action( && input.verification_result_cites_implementation_evidence && input.modeling_result_accepted && input.verification_result_covers_current_modeling + && input.hands_frontier_ready { return build( EpiphanyCoordinatorAction::ContinueImplementation, @@ -1117,13 +1124,43 @@ pub fn recommend_coordinator_action( ); } + if input.hands_frontier_ready { + return build( + EpiphanyCoordinatorAction::ContinueImplementation, + Some(EpiphanyCoordinatorRoleId::Implementation), + None, + false, + false, + "Mind has admitted the current RepoModel and Self has an actionable Hands frontier route.", + ); + } + + if input.signals.verification_result_status == EpiphanyCoordinatorRoleResultStatus::Completed + && input.verification_result_accepted + && !input.verification_result_allows_implementation + && (input.verification_result_needs_evidence + || input.verification_result_cites_implementation_evidence) + && input.modeling_result_accepted + && input.verification_result_covers_current_modeling + && !input.hands_frontier_ready + { + return build( + EpiphanyCoordinatorAction::LaunchModeling, + Some(EpiphanyCoordinatorRoleId::Modeling), + Some(EpiphanyCoordinatorSceneAction::RoleLaunch), + false, + true, + "Soul requests another bounded consequence, but Self has no admitted actionable Hands frontier; reconcile the RepoModel before acting.", + ); + } + build( - EpiphanyCoordinatorAction::ContinueImplementation, - Some(EpiphanyCoordinatorRoleId::Implementation), - None, + EpiphanyCoordinatorAction::LaunchModeling, + Some(EpiphanyCoordinatorRoleId::Modeling), + Some(EpiphanyCoordinatorSceneAction::RoleLaunch), false, - false, - "CRRC is clear and no specialist lane is currently blocking implementation.", + true, + "No admitted current RepoModel frontier item is actionable by Hands; route Modeling to establish or reconcile the machine map.", ) } @@ -1252,6 +1289,7 @@ mod tests { verification_result_allows_implementation: false, verification_result_needs_evidence: false, reorient_finding_accepted: false, + hands_frontier_ready: false, } } @@ -1276,10 +1314,13 @@ mod tests { evidence_gaps: Vec::new(), risks: Vec::new(), state_patch: None, + repo_model_patch: None, self_patch: None, self_persistence: None, job_error: None, item_error: None, + verification_request_id: None, + frontier_route_id: None, } } @@ -1449,10 +1490,7 @@ mod tests { ..input() }); - assert_eq!( - decision.action, - EpiphanyCoordinatorAction::ContinueImplementation - ); + assert_eq!(decision.action, EpiphanyCoordinatorAction::LaunchModeling); } #[test] @@ -1766,6 +1804,7 @@ mod tests { modeling_result_reviewable: true, verification_result_accepted: true, verification_result_needs_evidence: true, + hands_frontier_ready: true, ..input() }); assert_eq!( @@ -1808,6 +1847,7 @@ mod tests { modeling_result_accepted_after_verification: true, verification_result_accepted: true, verification_result_allows_implementation: true, + hands_frontier_ready: true, ..input() }); assert_eq!( @@ -1816,6 +1856,26 @@ mod tests { ); } + #[test] + fn clear_crrc_refuses_hands_without_an_actionable_frontier() { + let decision = recommend_coordinator_action(EpiphanyCoordinatorInput { + roles: Vec::new(), + signals: EpiphanyCoordinatorSignals { + research_result_status: EpiphanyCoordinatorRoleResultStatus::BackendMissing, + modeling_result_status: EpiphanyCoordinatorRoleResultStatus::BackendMissing, + verification_result_status: EpiphanyCoordinatorRoleResultStatus::BackendMissing, + }, + ..input() + }); + + assert_eq!(decision.action, EpiphanyCoordinatorAction::LaunchModeling); + assert_ne!( + decision.target_role, + Some(EpiphanyCoordinatorRoleId::Implementation) + ); + assert!(decision.reason.contains("admitted current RepoModel")); + } + #[test] fn forced_checkpoint_compaction_overrides_decision() { let decision = EpiphanyCoordinatorDecision { diff --git a/epiphany-core/src/surfaces/role_result.rs b/epiphany-core/src/surfaces/role_result.rs index a74863ef..5ebda028 100644 --- a/epiphany-core/src/surfaces/role_result.rs +++ b/epiphany-core/src/surfaces/role_result.rs @@ -46,10 +46,13 @@ pub struct EpiphanyRoleFindingInterpretation { pub evidence_gaps: Vec, pub risks: Vec, pub state_patch: Option, + pub repo_model_patch: Option, pub self_patch: Option, pub self_persistence: Option, pub job_error: Option, pub item_error: Option, + pub verification_request_id: Option, + pub frontier_route_id: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -136,6 +139,16 @@ fn interpret_role_finding( let state_patch_parse_error = state_patch_result .as_ref() .and_then(|result| result.as_ref().err().map(ToString::to_string)); + let repo_model_patch_result = raw_result + .get("repoModelPatch") + .cloned() + .map(serde_json::from_value::); + let repo_model_patch = repo_model_patch_result + .as_ref() + .and_then(|result| result.as_ref().ok().cloned()); + let repo_model_patch_parse_error = repo_model_patch_result + .as_ref() + .and_then(|result| result.as_ref().err().map(ToString::to_string)); let (self_patch, self_persistence) = raw_result .get("selfPatch") .map(|patch| decode_role_self_patch(role_id, patch)) @@ -151,8 +164,10 @@ fn interpret_role_finding( ), EpiphanyRoleResultRoleId::Modeling => merge_item_error( item_error, - modeling_role_state_patch_error( - raw_result.get("statePatch").is_some(), + modeling_role_result_error( + raw_result.get("repoModelPatch").is_some(), + repo_model_patch.as_ref(), + repo_model_patch_parse_error, state_patch.as_ref(), state_patch_parse_error, ), @@ -186,10 +201,13 @@ fn interpret_role_finding( evidence_gaps: json_string_array_field(raw_result, "evidenceGaps"), risks: json_string_array_field(raw_result, "risks"), state_patch, + repo_model_patch, self_patch, self_persistence, job_error, item_error, + verification_request_id: json_string_field(raw_result, "verificationRequestId"), + frontier_route_id: json_string_field(raw_result, "frontierRouteId"), } } @@ -222,6 +240,7 @@ pub fn interpret_runtime_role_worker_result( result: &crate::EpiphanyRuntimeRoleWorkerResult, ) -> EpiphanyRoleFindingInterpretation { let state_patch_result = result.state_patch(); + let repo_model_patch_result = result.repo_model_patch(); let self_patch_result = result.self_patch(); let state_patch = state_patch_result .as_ref() @@ -231,6 +250,10 @@ pub fn interpret_runtime_role_worker_result( .as_ref() .ok() .and_then(|patch| patch.clone()); + let repo_model_patch = repo_model_patch_result + .as_ref() + .ok() + .and_then(|patch| patch.clone()); let item_error = merge_item_error( result.item_error.clone(), state_patch_result.as_ref().err().map(ToString::to_string), @@ -239,6 +262,13 @@ pub fn interpret_runtime_role_worker_result( item_error, self_patch_result.as_ref().err().map(ToString::to_string), ); + let item_error = merge_item_error( + item_error, + repo_model_patch_result + .as_ref() + .err() + .map(ToString::to_string), + ); EpiphanyRoleFindingInterpretation { verdict: Some(result.verdict.clone()), summary: Some(result.summary.clone()), @@ -255,14 +285,17 @@ pub fn interpret_runtime_role_worker_result( evidence_gaps: result.evidence_gaps.clone(), risks: result.risks.clone(), state_patch: state_patch.clone(), + repo_model_patch: repo_model_patch.clone(), self_patch, self_persistence: None, job_error: None, item_error: match role_id { EpiphanyRoleResultRoleId::Modeling => merge_item_error( item_error, - modeling_role_state_patch_error( - result.state_patch_msgpack.is_some(), + modeling_role_result_error( + result.repo_model_patch_msgpack.is_some(), + repo_model_patch.as_ref(), + None, state_patch.as_ref(), None, ), @@ -287,6 +320,8 @@ pub fn interpret_runtime_role_worker_result( | EpiphanyRoleResultRoleId::Verification | EpiphanyRoleResultRoleId::Reorientation => item_error, }, + verification_request_id: result.verification_request_id.clone(), + frontier_route_id: result.frontier_route_id.clone(), } } @@ -597,14 +632,14 @@ pub fn modeling_role_state_patch_policy_errors( "churn or mode changes are not allowed through modeling role acceptance".to_string(), ); } - if patch.graphs.is_none() - && patch.graph_frontier.is_none() - && patch.graph_checkpoint.is_none() - && patch.scratch.is_none() - && patch.investigation_checkpoint.is_none() + if patch.graphs.is_some() + || patch.graph_frontier.is_some() + || patch.graph_checkpoint.is_some() + || patch.scratch.is_some() + || patch.investigation_checkpoint.is_some() { errors.push( - "statePatch must include a modeling field: graphs, graphFrontier, graphCheckpoint, scratch, or investigationCheckpoint" + "repository anatomy is not allowed in generic Modeling statePatch; use repoModelPatch" .to_string(), ); } @@ -768,23 +803,34 @@ pub fn research_role_state_patch_policy_errors( errors } -fn modeling_role_state_patch_error( - state_patch_present: bool, +fn modeling_role_result_error( + repo_model_patch_present: bool, + repo_model_patch: Option<&crate::RepoModelPatch>, + repo_model_patch_parse_error: Option, state_patch: Option<&EpiphanyRoleStatePatchDocument>, - parse_error: Option, + state_patch_parse_error: Option, ) -> Option { - if !state_patch_present { - return Some("modeling result is not reviewable: missing required statePatch".to_string()); + if !repo_model_patch_present { + return Some( + "modeling result is not reviewable: missing required repoModelPatch".to_string(), + ); }; - if let Some(error) = parse_error { + if let Some(error) = repo_model_patch_parse_error { return Some(format!( - "modeling result is not reviewable: invalid statePatch ({error})" + "modeling result is not reviewable: invalid repoModelPatch ({error})" )); } - let Some(state_patch) = state_patch else { - return Some("modeling result is not reviewable: invalid statePatch".to_string()); + if repo_model_patch.is_none() { + return Some("modeling result is not reviewable: invalid repoModelPatch".to_string()); }; - let errors = modeling_role_state_patch_policy_errors(state_patch); + if let Some(error) = state_patch_parse_error { + return Some(format!( + "modeling result is not reviewable: invalid optional statePatch ({error})" + )); + } + let errors = state_patch + .map(modeling_role_state_patch_policy_errors) + .unwrap_or_default(); if errors.is_empty() { None } else { @@ -1157,12 +1203,21 @@ mod tests { missing .item_error .as_deref() - .is_some_and(|error| error.contains("missing required statePatch")) + .is_some_and(|error| error.contains("missing required repoModelPatch")) ); let reviewable = interpret_role_finding( EpiphanyRoleResultRoleId::Modeling, - &serde_json::json!({"statePatch": {"scratch": {"summary": "banked"}}}), + &serde_json::json!({ + "repoModelPatch": { + "patch_id": "modeling-result-test", + "base_revision": 0, + "base_hash": "legacy-hash", + "applied_at": "2026-07-13T00:00:00Z", + "purpose": {"kind": "evolution"}, + "operations": [{"operation": "retire_node", "node_id": "old"}] + } + }), None, None, None, @@ -1171,6 +1226,30 @@ mod tests { assert!(reviewable.item_error.is_none()); } + #[test] + fn modeling_state_patch_cannot_smuggle_repository_anatomy() { + let finding = interpret_role_finding( + EpiphanyRoleResultRoleId::Modeling, + &serde_json::json!({ + "repoModelPatch": { + "patch_id": "modeling-anatomy-test", + "base_revision": 0, + "base_hash": "legacy-hash", + "applied_at": "2026-07-13T00:00:00Z", + "purpose": {"kind": "evolution"}, + "operations": [{"operation": "retire_node", "node_id": "old"}] + }, + "statePatch": {"graphs": {}} + }), + None, + None, + None, + ); + assert!(finding.item_error.as_deref().is_some_and(|error| { + error.contains("repository anatomy") && error.contains("repoModelPatch") + })); + } + #[test] fn marks_imagination_without_planning_patch_unreviewable() { let missing = interpret_role_finding( diff --git a/epiphany-openai-runtime/src/lib.rs b/epiphany-openai-runtime/src/lib.rs index 611e5add..3783d191 100644 --- a/epiphany-openai-runtime/src/lib.rs +++ b/epiphany-openai-runtime/src/lib.rs @@ -1365,7 +1365,7 @@ fn role_result_id_for_launch_role( fn worker_output_contract_text(document: &EpiphanyWorkerLaunchDocument) -> &'static str { match document { EpiphanyWorkerLaunchDocument::Role(_) => { - "Required role-result fields: roleId, verdict, summary, nextSafeMove, filesInspected. Modeling and Imagination workers must include their required statePatch. Use arrays for frontierNodeIds, evidenceIds, openQuestions, evidenceGaps, risks, and artifactRefs when present." + "Required role-result fields: roleId, verdict, summary, nextSafeMove, filesInspected. Modeling workers must include repoModelPatch; Imagination workers must include statePatch. Modeling statePatch is optional observations/evidence only. Use arrays for frontierNodeIds, evidenceIds, openQuestions, evidenceGaps, risks, and artifactRefs when present." } EpiphanyWorkerLaunchDocument::Reorient(_) => { "Required reorient-result fields: mode, summary, nextSafeMove. Include checkpointStillValid, filesInspected, frontierNodeIds, evidenceIds, openQuestions, and continuityRisks when present." @@ -1393,7 +1393,11 @@ struct RoleWorkerResultIngress { evidence_gaps: Vec, risks: Vec, state_patch: Option, + repo_model_patch: Option, self_patch: Option, + verification_request_id: Option, + frontier_route_id: Option, + repo_frontier_modeling_request_id: Option, } #[derive(Debug, Clone, Default, serde::Deserialize)] @@ -1514,6 +1518,8 @@ fn role_worker_result_from_ingress( ) -> EpiphanyRuntimeRoleWorkerResult { let (state_patch_msgpack, state_patch_error) = encode_optional_document(&result.state_patch, "statePatch"); + let (repo_model_patch_msgpack, repo_model_patch_error) = + encode_optional_document(&result.repo_model_patch, "repoModelPatch"); let (self_patch_msgpack, self_patch_error) = encode_optional_document(&result.self_patch, "selfPatch"); EpiphanyRuntimeRoleWorkerResult { @@ -1540,8 +1546,17 @@ fn role_worker_result_from_ingress( risks: clean_string_vec(&result.risks), state_patch_msgpack, self_patch_msgpack, - item_error: merge_optional_errors(state_patch_error, self_patch_error), + item_error: merge_optional_errors( + merge_optional_errors(state_patch_error, self_patch_error), + repo_model_patch_error, + ), metadata: std::collections::BTreeMap::new(), + repo_model_patch_msgpack, + verification_request_id: clean_optional_string(result.verification_request_id.as_deref()), + frontier_route_id: clean_optional_string(result.frontier_route_id.as_deref()), + repo_frontier_modeling_request_id: clean_optional_string( + result.repo_frontier_modeling_request_id.as_deref(), + ), } } @@ -1658,6 +1673,46 @@ mod tests { use epiphany_openai_adapter::EpiphanyOpenAiModelReceipt; use tempfile::tempdir; + #[test] + fn verification_ingress_preserves_exact_request_and_route_binding() -> Result<()> { + let parsed = parse_assistant_json::( + r#"{"roleId":"verification","verdict":"pass","summary":"verified","nextSafeMove":"admit","verificationRequestId":" verification-request-1 ","frontierRouteId":" frontier-route-1 "}"#, + )?; + let launch = EpiphanyRuntimeWorkerLaunchRequest { + schema_version: epiphany_core::RUNTIME_WORKER_LAUNCH_REQUEST_SCHEMA_VERSION.to_string(), + job_id: "verification-job-1".to_string(), + binding_id: "verification-binding-1".to_string(), + role: "verification".to_string(), + authority_scope: "epiphany.role.verification".to_string(), + instruction: "verify".to_string(), + output_contract_id: epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID.to_string(), + document_kind: "role".to_string(), + launch_document_msgpack: Vec::new(), + metadata: std::collections::BTreeMap::new(), + organ_launch_contract: epiphany_core::default_launch_organ_contract( + "epiphany.role.verification", + "role", + epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID, + ), + }; + let result = role_worker_result_from_ingress( + &launch, + "verification", + "verification-result-1", + &parsed, + Vec::new(), + ); + assert_eq!( + result.verification_request_id.as_deref(), + Some("verification-request-1") + ); + assert_eq!( + result.frontier_route_id.as_deref(), + Some("frontier-route-1") + ); + Ok(()) + } + fn test_openai_event( request_id: &str, sequence: u64, @@ -1865,10 +1920,10 @@ mod tests { .output_schema_json .as_deref() .expect("worker model request should carry role output schema"); - assert!(output_schema.contains("\"statePatch\"")); + assert!(output_schema.contains("\"repoModelPatch\"")); assert!(output_schema.contains("\"frontierNodeIds\"")); assert!(model_request.instructions.contains("Output schema JSON")); - assert!(model_request.instructions.contains("\"statePatch\"")); + assert!(model_request.instructions.contains("\"repoModelPatch\"")); assert_eq!(model_request.reasoning_effort.as_deref(), Some("low")); assert_eq!(model_request.reasoning_summary.as_deref(), Some("concise")); assert!( @@ -1895,7 +1950,7 @@ mod tests { &launch_request, &model_request.request_id, &openai_summary, - r#"{"roleId":"modeling","verdict":"checkpoint-ready","summary":"Mapped.","nextSafeMove":"Review the patch.","filesInspected":["src/lib.rs"],"evidenceIds":["ev-1"],"artifactRefs":["artifact:model"],"statePatch":{"objective":"Keep the machine mapped."},"selfPatch":{"reason":"typed nested document"}} "#, + r#"{"roleId":"modeling","verdict":"checkpoint-ready","summary":"Mapped.","nextSafeMove":"Review the patch.","filesInspected":["src/lib.rs"],"frontierNodeIds":["old"],"evidenceIds":["ev-1"],"artifactRefs":["artifact:model"],"repoModelPatch":{"patch_id":"modeling-runtime-test","base_revision":0,"base_hash":"legacy-hash","applied_at":"2026-07-13T00:00:00Z","purpose":{"kind":"evolution"},"operations":[{"operation":"retire_node","node_id":"old"}]},"statePatch":{"observations":[],"evidence":[]},"selfPatch":{"reason":"typed nested document"}} "#, )?; assert_eq!(result.job_id, "worker-job-1"); @@ -1909,8 +1964,11 @@ mod tests { assert_eq!(typed_result.files_inspected, vec!["src/lib.rs".to_string()]); assert_eq!(typed_result.artifact_refs, result.artifact_refs); assert_eq!( - typed_result.state_patch()?.expect("state patch").objective, - Some("Keep the machine mapped.".to_string()) + typed_result + .repo_model_patch()? + .expect("repo model patch") + .patch_id, + "modeling-runtime-test" ); assert_eq!( typed_result.self_patch()?.expect("self patch").reason, @@ -2021,6 +2079,8 @@ mod tests { risks: Vec::new(), emitted_at: now(), contract: "test".to_string(), + verification_request_id: String::new(), + frontier_route_id: String::new(), }, )?; let request = epiphany_core::RepoWorkModelingRequest { @@ -2203,11 +2263,9 @@ mod tests { &openai_summary1, r#"{"verdict":"passed","finding":"The reviewed README consequence updates the repo map.","summary":"README consequence mapped.","nextSafeMove":"Let Mind admit generation one.","evidenceIds":["soul-repo-1"],"artifactRefs":[]}"#, )?; - let finding1 = epiphany_core::runtime_repo_work_modeling_finding( - &store, - "repo-request-2-finding", - )? - .expect("generation-one typed finding"); + let finding1 = + epiphany_core::runtime_repo_work_modeling_finding(&store, "repo-request-2-finding")? + .expect("generation-one typed finding"); assert_eq!(finding1.verdict, "passed"); let map_review = epiphany_core::MindGatewayReview { @@ -2249,12 +2307,7 @@ mod tests { modeling_route_id: route1.route_id.clone(), modeling_generation: 1, }; - epiphany_core::commit_repo_work_map_admission( - &store, - &map, - &map_review, - &map_commit, - )?; + epiphany_core::commit_repo_work_map_admission(&store, &map, &map_review, &map_commit)?; let mut stale = map.clone(); stale.map_entry_id = "stale-generation-zero-map".to_string(); stale.modeling_finding_receipt_id = finding0.receipt_id.clone(); @@ -2269,11 +2322,9 @@ mod tests { ) .is_err() ); - let current_route = epiphany_core::runtime_repo_work_modeling_route( - &store, - &route1.route_id, - )? - .expect("current route"); + let current_route = + epiphany_core::runtime_repo_work_modeling_route(&store, &route1.route_id)? + .expect("current route"); assert_eq!(current_route.generation, 1); assert_eq!(current_route.request_id, request1.request_id); Ok(()) diff --git a/epiphany-state-model/src/lib.rs b/epiphany-state-model/src/lib.rs index 2d214356..5a22b859 100644 --- a/epiphany-state-model/src/lib.rs +++ b/epiphany-state-model/src/lib.rs @@ -819,11 +819,24 @@ pub struct RepoModelPatch { pub base_revision: u64, pub base_hash: String, pub applied_at: String, + pub purpose: RepoModelPatchPurpose, #[serde(default, skip_serializing_if = "Vec::is_empty")] #[ts(type = "Array")] pub operations: Vec, } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)] +#[serde(tag = "kind", rename_all = "snake_case")] +#[ts(tag = "kind", rename_all = "snake_case")] +pub enum RepoModelPatchPurpose { + #[default] + Evolution, + IncorporateFrontierVerdict { + route_id: String, + soul_verdict_receipt_id: String, + }, +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)] #[serde(tag = "operation", rename_all = "snake_case")] #[ts(tag = "operation", rename_all = "snake_case")] @@ -1185,6 +1198,10 @@ pub struct EpiphanyMemoryContextQuery { pub struct EpiphanyMemoryContextPacket { pub id: String, pub query_id: String, + #[serde(default)] + pub repo_model_revision: u64, + #[serde(default, skip_serializing_if = "String::is_empty")] + pub repo_model_hash: String, #[serde(default, skip_serializing_if = "Vec::is_empty")] #[ts(type = "Array")] pub nodes: Vec, diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 9e732593..576fe4cd 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -27,6 +27,14 @@ roadmap mythology, and Self preserved dependency order without flattening those different judgments into one running narrative. This is positive evidence for durable organ-to-organ cognition, not merely for role-flavored prompting. +Do not substitute the first linear frontier identity proof for that larger +lesson. `Modeling -> Mind -> Self -> Hands -> Soul -> Modeling` proves that one +wound can retain causal identity. The awakened anatomy also needs typed backward +pressure: Eyes findings that challenge model claims, Imagination revisions that +change campaign shape, Soul rejections that create new evidence requirements, +and Self rerouting based on those admitted artifacts. Modeling is the shared +situational substrate, not the whole organism and not the product. + Persistence is the connective-tissue gap exposed by that success. Modeling's repo anatomy, confirmed/changed/invalidated claims, freshness, frontier, and semantic-index consequences must live in typed searchable state; Self must route @@ -44,14 +52,44 @@ or lost to concurrent valid patches: CultCache now provides exact-envelope CAS and insert-if-absent beneath its cross-process sidecar lock, with hostile race proofs. Soul rejected two earlier cuts before accepting this authority boundary. -Do not call this the complete Aetheria loop. Modeling worker output still cannot -carry a reviewed `RepoModelPatch`; Mind has no model-admission receipt binding -patch/result/evidence; Self does not select and bind one frontier identity into a -Hands grant; Hands consequences and Soul verdicts do not yet carry that same -model revision/hash/item identity; and Modeling therefore cannot close exactly -the wound Soul judged. The next cut is that closed identity chain, not more index -polish: `Modeling patch -> Mind admission -> Self route F -> Hands consequence F --> Soul verdict F -> Modeling resolves F`. +The first causal-identity slice of that Aetheria loop is now closed. Modeling +emits a typed `RepoModelPatch`; Mind admits the exact immutable worker result, +patch bytes, evidence, review, and replacement model under one conditional +store transaction; Self selects one eligible Active frontier item from the +admitted current model; and the resulting route freezes model revision/hash, +item hash, claims, dependency order, and path scope. Hands intent, review, +Substrate grant, frontier authority, patch, command, and commit documents are +immutable insert-once records. Soul receives a typed verification request bound +to that complete chain. A later typed Modeling request binds the accepted Soul +result and permits exactly one verdict-incorporation revision of the same item: +`pass` yields `Resolved`; `needs-review`, `needs-evidence`, or `fail` yields +`Blocked`. Self derives Hands readiness from the same selection predicate, so +Blocked/Resolved items cannot be routed and no default implementation authority +survives without an admitted actionable frontier. + +Soul found and forced cuts of four plausible green bypasses before accepting +the slice: ordinary Evolution could mutate routed frontier lifecycle; Hands +receipts and their intent/review/grant foundation were mutable by identity; the +Modeling request trusted caller-supplied temporal acceptance; and a split model +admission/thread-state commit could wedge on retry. Evolution is now barred +while a current-model route exists and cannot manufacture Blocked or terminal +frontier lifecycle. Every causal companion uses absent-only cross-process CAS +with exact retry. Final verdict admission replays the full authority and +consequence chain immediately before the model CAS. Verification acceptance is +reloaded from canonical thread state, and stable review identity makes the +split commit recoverable. Hostile substitution, stale-model, counterfeit, +concurrent-writer, crash-window retry, pass/nonpass, and nonselection tests are +green; the full core library proof is 303 passed and 1 ignored. + +This is still not the whole Aetheria lesson. It proves durable causal identity +and backward Soul pressure, not the complete multi-organ correction ecology. +Eyes challenges and Imagination campaign revisions still need typed admission +and Self routes. The older `RepoWorkModelingRequest/Route/Finding` path is also +misnamed parallel closure machinery: it models one already-executed work item, +not repository anatomy. Preserve useful plan/execute receipts, but converge +that workflow on canonical RepoFrontier route/Hands/Soul/Modeling authority and +demote `RepoWorkMapEntry` to derived closure history. Do not let item-slug routes +or the legacy special Modeling worker close work independently of RepoModel. The next artifact-contract audit then cut `epiphany.persona_chat.v0`: its live writer and catalog had disagreed on field vocabulary, evidence fields, and diff --git a/notes/repo-frontier-organ-loop.md b/notes/repo-frontier-organ-loop.md new file mode 100644 index 00000000..b191bbf1 --- /dev/null +++ b/notes/repo-frontier-organ-loop.md @@ -0,0 +1,91 @@ +# Repo Frontier Organ Loop + +## Objective + +Preserve the Aetheria-shaped engineering loop as typed causal identity, not as +coordinator memory or prompt roleplay. One repository wound must remain the same +wound while Modeling describes it, Mind admits it, Self routes it, Hands acts on +it, Soul judges it, and Modeling closes or revises it. + +This closed route is the first proof slice, not the whole Aetheria lesson. The +larger success was corrective circulation between distinct organs: Eyes could +invalidate substrate assumptions, Imagination could revise campaign shape, Soul +could reject a locally green implementation, and Self could reorder work from +those durable artifacts. RepoModel is connective tissue for that cognition; it +is not a substitute for it. Later slices must admit typed challenges and plan +revisions from those organs without giving any of them direct model-write +authority. + +## Authority Map + +- Owner: the canonical `RepoModel` entry inside the runtime-spine CultCache. +- Inputs: immutable Modeling worker result, exact patch bytes and base model + identity, accepted evidence, and—when resolving work—the exact frontier route, + Hands consequence chain, and Soul verdict. +- Outputs: a replacement model plus immutable specialized Mind review and model + admission receipt committed in one conditional batch. +- Derived state: thread-state graphs/frontier/checkpoint are continuity and UI + projections; semantic indexes rank eligible typed claims; prompt context and + gate JSON explain admitted authority but never create it. +- Forbidden writers: sibling-store refresh, generic Modeling `statePatch` + anatomy, coordinator reason strings, `frontierNodeIds`, evidence-id prefix + matching, and dashboard/operator projections. +- Shared path: `Modeling patch -> Mind admission -> Self route F -> Hands intent + and consequence F -> Soul request and verdict F -> Modeling resolves F`. +- Deletion line: remove sibling `memory-graph.msgpack` as a live authority after + one explicit import; remove Modeling graph/frontier writes from generic role + acceptance; refuse `ContinueImplementation` without an admitted current route; + replace prefix-based Hands-chain inference with typed reload and validation. + +## Atomic Boundaries + +1. Modeling result is immutable worker evidence. +2. Exact current-model compare-and-swap plus specialized Mind review and model + admission receipt is one runtime-spine store transaction. +3. Self route insertion verifies the same current model and frontier item under + the conditional store lock. +4. Hands preflight verifies current route before consequence; immutable receipts + preserve historical truth afterward. +5. Soul request binds one complete typed Hands chain; accepted verdict binds that + request and route. +6. Resolution admission validates the same route and verdict before replacing the + model and making the frontier ineligible for subsequent selection. + +## Invariants + +- No model effect exists without its admission receipt in the same snapshot. +- No admission receipt can claim a model revision/hash absent from that snapshot. +- A route freezes model revision/hash, frontier item hash, target claims, source + scope, and selected organ. +- Hands cannot act from prompt prose or gate JSON; it requires the persisted route. +- Soul cannot judge a substituted or partial Hands chain. +- A resolution patch can affect only the routed frontier item in the first closed + loop slice. +- Once F is resolved, retired, or superseded, Self cannot select F again. + +## Landed Causal Slice + +1. Conditional atomic batches and absent-only insertions protect the single-file + CultCache store under its cross-process lock. +2. Runtime-spine owns the imported canonical RepoModel and specialized Modeling + result plus Mind admission documents. +3. Self selects an eligible frontier item and binds the main-agent Hands gate to + an immutable route; no actionable route means no Hands authority. +4. Immutable Substrate, Hands, Soul verification, and Modeling request documents + preserve exact causal identity. Final admission replays the complete chain. +5. Verdict incorporation changes exactly the routed frontier item. Pass resolves; + nonpass blocks. Hostile substitution, concurrent identity, stale model, + split-retry, and terminal nonselection proofs pass, and independent Soul review + accepts this slice. + +## Next Deletion Line + +`RepoWorkModelingRequest`, `RepoWorkModelingRoute`, and +`RepoWorkModelingFinding` describe post-execution workflow closure, not repository +anatomy. Their item-slug route and `RepoWorkMapEntry` closure ledger must not +remain a second Modeling/Self/Mind truth. Preserve useful repo-work planning and +execution receipts only behind canonical `RepoFrontierRoute` and +`RepoFrontierHandsAuthority`; use the canonical Hands/Soul/verdict-incorporation +path; demote the old map to derived closure history or delete it. After that, +admit typed Eyes challenges and Imagination campaign revisions so blocked +frontier pressure can circulate to the correct organ. diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 47cc37e6b7d1762eb555ddb8cd326a8cf6e1fbeb..b42b9ea3f21290af8be20d2b391f1af555d9b492 100644 GIT binary patch delta 527 zcmXX@yKWRg5M+;sGb92+qn1c_?!2bO<;4{-p$LGPNN=g~b~T~+h*_VDYU!(UH}$J5Dt`eHtwef)jzU~_x*yx0`Q z_w)VW+kQ}Jbo+gc2O1AG9$oJT2N(Xy<;`?FeOZl$r6!}t;Z7SDE9uUrtakQ;&} zC&_S3dQOa$G#IsF&39ekh|-UUlebFFAX92=Sg=CEc1eTn@F(5_j8gD-*3&}68| zEn%60&rAtYXfu_->vfxVxNrn@yO5kP)94L)D_|OAZLUM>Akn5J$)nGdESp?H-gd11 zUxB>|DF$us=k?eXe!)a=hm{B|dlD(336fdINjHvjt1UY}N@!?g5=v7g%4rKRg|O7; zN=4x()7-$gx4ct&{6^7FPg{QL&CV17kBCib9cIq_hAWy@8T9Ld@ delta 64 zcmZpDCcEg7>;_RrK0`|@ATl+JI>x}B|9`VGV<98s)aI#7?NgZ;ffz(H12GE_vjQ>O K_Nh$ly?FqxNfcNB diff --git a/state/map.yaml b/state/map.yaml index 7f674833..e79d1c36 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -661,4 +661,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: Close the Aetheria-shaped organ identity loop. The canonical RepoModel foundation is now revisioned/hashed, cross-process CAS protected, raw-refresh resistant, typed-frontier aware, dependency ordered, BM25 searchable over eligible claims, and projected into downstream role context. Do not add more index polish. Add a Modeling-only typed RepoModelPatch result; have Mind atomically review/admit it with a patch/result/evidence receipt; make Self select one ready frontier item and bind model revision/hash/item/claim scope into the Hands grant; bind Hands consequences and Soul verdict to that exact route; require the following Modeling patch to resolve/revise that same frontier identity; then prove substitution/staleness at every seam fails and resolved F cannot be selected again. Demote generic Modeling graph/frontier statePatch authority as the typed path lands. The intentionally absent Other-world action still requires a typed intent and real owner. Keep closure's zero-substring invariant. + next_action: Converge the legacy RepoWork closure workflow on the accepted canonical RepoModel organ loop. The first causal slice is now proven: typed Modeling patch and Mind admission; Self route bound to current model/frontier identity; immutable Substrate/Hands consequence chain; exact Soul verification request and verdict; typed Modeling verdict-incorporation request; pass-to-Resolved and nonpass-to-Blocked; no unrouted Hands default; full-chain replay at final CAS; atomic absent-only companions; recoverable split coordinator retry; hostile substitution/race/nonselection proof; Soul PASS. Do not add index polish or another adapter. Keep repo-work plan/adopt/execute only where it serves the branch-local workflow, require it to consume canonical RepoFrontierRoute and RepoFrontierHandsAuthority, delete the special RepoWorkModeling Request/Route/Finding worker protocol and item-slug retry route, and demote or rename RepoWorkMapEntry as derived closure history with no anatomy/routing authority. Then add typed Eyes challenges and Imagination campaign revisions so Blocked persistence becomes real Aetheria-style corrective circulation rather than a terminal note. The intentionally absent Other-world action still requires a typed intent and real owner. Keep closure's zero-substring invariant. diff --git a/vendor/cultcache-rs b/vendor/cultcache-rs index 654bba0b..141f2aa8 160000 --- a/vendor/cultcache-rs +++ b/vendor/cultcache-rs @@ -1 +1 @@ -Subproject commit 654bba0b17845ad957ede60cad8a3acaf336bac9 +Subproject commit 141f2aa89a25016166b53fc47b0b48b6beabca39