From 79e1e26cf84f603b579d783f5f37f2e43d92c79d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 14 Jul 2026 01:03:54 +0100 Subject: [PATCH] Bind selected proposals to Modeling admission --- epiphany-core/src/agent_launch.rs | 11 + .../src/bin/epiphany-mvp-coordinator.rs | 15 +- epiphany-core/src/coordinator_acceptance.rs | 1 + epiphany-core/src/coordinator_launch.rs | 391 +++++- .../src/coordinator_launch_context.rs | 64 +- epiphany-core/src/coordinator_service.rs | 19 + epiphany-core/src/lib.rs | 8 + .../src/prompts/epiphany_specialists.toml | 1 + epiphany-core/src/repo_model_gateway.rs | 95 +- epiphany-core/src/runtime_spine.rs | 1146 ++++++++++++++++- epiphany-core/src/substrate_gate.rs | 2 + epiphany-core/src/surfaces/mod.rs | 3 + epiphany-core/src/surfaces/worker_launch.rs | 36 + epiphany-openai-runtime/src/lib.rs | 9 + notes/fresh-workspace-handoff.md | 38 +- notes/repo-frontier-organ-loop.md | 18 + state/ledgers.msgpack | Bin 489135 -> 489606 bytes state/map.yaml | 2 +- 18 files changed, 1802 insertions(+), 57 deletions(-) diff --git a/epiphany-core/src/agent_launch.rs b/epiphany-core/src/agent_launch.rs index b5778dba..b6859551 100644 --- a/epiphany-core/src/agent_launch.rs +++ b/epiphany-core/src/agent_launch.rs @@ -327,6 +327,14 @@ pub fn epiphany_role_launch_output_schema(role_id: EpiphanyRoleResultRoleId) -> "repoFrontierModelingRequestId".to_string(), serde_json::json!({"type": "string", "minLength": 1}), ); + map.insert( + "proposalModelingRequestId".to_string(), + serde_json::json!({ + "type": "string", + "minLength": 1, + "description": "Exact echo of an explicit typed repo-frontier proposal Modeling request, when supplied." + }), + ); map.insert( "statePatch".to_string(), serde_json::json!({ @@ -560,6 +568,7 @@ pub fn build_epiphany_role_launch_request_with_dynamic_context( state_revision: state.revision, objective: state.objective.clone(), dynamic_prompt_context, + proposal_modeling_context: None, active_subgoal_id: state.active_subgoal_id.clone(), active_subgoals: state .subgoals @@ -600,6 +609,7 @@ pub fn build_epiphany_role_launch_request_with_dynamic_context( output_contract_id, organ_launch_contract, max_runtime_seconds, + proposal_modeling_request_id: None, }) } @@ -702,6 +712,7 @@ pub fn build_epiphany_job_launch_request( output_contract_id, organ_launch_contract, max_runtime_seconds, + proposal_modeling_request_id: None, } } diff --git a/epiphany-core/src/bin/epiphany-mvp-coordinator.rs b/epiphany-core/src/bin/epiphany-mvp-coordinator.rs index fce34e6e..8d0b4c96 100644 --- a/epiphany-core/src/bin/epiphany-mvp-coordinator.rs +++ b/epiphany-core/src/bin/epiphany-mvp-coordinator.rs @@ -95,6 +95,7 @@ struct Args { simulate_continue_implementation: bool, simulate_source_drift: bool, dry_compact: bool, + proposal_modeling_request_id: Option, } impl Args { @@ -133,6 +134,7 @@ impl Args { simulate_continue_implementation: false, simulate_source_drift: false, dry_compact: false, + proposal_modeling_request_id: None, }; while let Some(arg) = args.next() { match arg.as_str() { @@ -152,6 +154,10 @@ impl Args { parsed.model_provider = take_string(&mut args, "--model-provider")?; } "--thread-id" => parsed.thread_id = Some(take_string(&mut args, "--thread-id")?), + "--proposal-modeling-request-id" => { + parsed.proposal_modeling_request_id = + Some(take_string(&mut args, "--proposal-modeling-request-id")?); + } "--cwd" => parsed.cwd = take_path(&mut args, "--cwd")?, "--codex-home" => parsed.codex_home = take_path(&mut args, "--codex-home")?, "--artifact-dir" => parsed.artifact_dir = take_path(&mut args, "--artifact-dir")?, @@ -488,6 +494,11 @@ fn run_coordinator(args: &Args) -> Result { role_id, revision, args.max_runtime_seconds, + if role_id == "modeling" { + args.proposal_modeling_request_id.as_deref() + } else { + None + }, )?; let worker_job_id = worker_job_id_from_launch(&launch)?; push_event( @@ -937,6 +948,7 @@ fn launch_role( role_id: &str, expected_revision: Option, max_runtime_seconds: u64, + proposal_modeling_request_id: Option<&str>, ) -> Result { let service = epiphany_core::EpiphanyCoordinatorService::new(runtime_store); let state = service @@ -965,7 +977,7 @@ fn launch_role( ) .map_err(anyhow::Error::msg)?; } - let request = epiphany_core::build_epiphany_role_launch_request_with_dynamic_context( + let mut request = epiphany_core::build_epiphany_role_launch_request_with_dynamic_context( thread_id, role, expected_revision, @@ -974,6 +986,7 @@ fn launch_role( Some(context), ) .map_err(anyhow::Error::msg)?; + request.proposal_modeling_request_id = proposal_modeling_request_id.map(str::to_string); let launched = service.launch_job( thread_id, &state, diff --git a/epiphany-core/src/coordinator_acceptance.rs b/epiphany-core/src/coordinator_acceptance.rs index e421fcae..ce12eb8e 100644 --- a/epiphany-core/src/coordinator_acceptance.rs +++ b/epiphany-core/src/coordinator_acceptance.rs @@ -1169,6 +1169,7 @@ mod tests { verification_request_id: String::new(), soul_verdict_receipt_id: String::new(), frontier_modeling_request_id: String::new(), + proposal_modeling_request_id: String::new(), }, )?; let mut fresh = existing.clone(); diff --git a/epiphany-core/src/coordinator_launch.rs b/epiphany-core/src/coordinator_launch.rs index 2a58762d..821a7538 100644 --- a/epiphany-core/src/coordinator_launch.rs +++ b/epiphany-core/src/coordinator_launch.rs @@ -2,6 +2,7 @@ use crate::*; use anyhow::{Result, anyhow}; use cultcache_rs::{CultCache, CultCacheEnvelope}; use epiphany_state_model::{EpiphanyRuntimeLink, EpiphanyThreadState}; +use sha2::{Digest, Sha256}; use std::path::Path; #[derive(Debug, Clone)] @@ -43,12 +44,45 @@ fn commit_coordinator_job_launch_in_cache( created_at: String, injected_envelopes: Vec, ) -> Result { + let runtime_identity = cache + .get::(RUNTIME_IDENTITY_KEY)? + .ok_or_else(|| anyhow!("coordinator launch requires runtime identity"))?; let next_state = apply_coordinator_state_update_to_state(current_state, plan.state_update.clone(), None)?; + let mut effective_launch_document = request.launch_document.clone(); + let proposal_launch = if let Some(request_id) = request.proposal_modeling_request_id.as_deref() + { + let (selection, proposal, identity) = + validate_proposal_modeling_launch(cache, current_state, request, request_id)?; + let model = cache + .get::(crate::MEMORY_GRAPH_KEY)? + .ok_or_else(|| anyhow!("proposal Modeling launch requires canonical RepoModel"))? + .snapshot()?; + let projection = build_proposal_modeling_context_projection(&selection, &proposal, &model)?; + match &mut effective_launch_document { + EpiphanyWorkerLaunchDocument::Role(document) => { + document.proposal_modeling_context = Some(projection); + } + EpiphanyWorkerLaunchDocument::Reorient(_) => { + return Err(anyhow!( + "reorient launch cannot carry proposal Modeling context" + )); + } + } + let bytes = rmp_serde::to_vec_named(&effective_launch_document)?; + Some(( + selection, + proposal, + identity, + format!("{:x}", Sha256::digest(bytes)), + )) + } else { + None + }; let prepared = prepare_runtime_spine_heartbeat_job( &cache, RuntimeSpineHeartbeatJobOptions { - runtime_id: "epiphany-local".to_string(), + runtime_id: runtime_identity.runtime_id, display_name: "Epiphany Local".to_string(), session_id: "epiphany-main".to_string(), objective: plan @@ -64,13 +98,46 @@ fn commit_coordinator_job_launch_in_cache( binding_id: request.binding_id.clone(), authority_scope: request.authority_scope.clone(), instruction: request.instruction.clone(), - launch_document: request.launch_document.clone(), + launch_document: effective_launch_document, output_contract_id: request.output_contract_id.clone(), organ_launch_contract: request.organ_launch_contract.clone(), + proposal_modeling_request_id: request.proposal_modeling_request_id.clone(), created_at: created_at.clone(), }, )?; let mut batch = prepared.envelopes; + if let Some((selection, proposal, identity, worker_launch_document_sha256)) = proposal_launch { + let launch_binding = RepoFrontierProposalModelingLaunchBinding { + schema_version: REPO_FRONTIER_PROPOSAL_MODELING_LAUNCH_BINDING_SCHEMA_VERSION.into(), + binding_record_id: format!( + "repo-frontier-proposal-modeling-launch-{}", + plan.backend_job_id + ), + proposal_modeling_request_id: selection.request_id, + proposal_id: proposal.proposal_id, + proposal_payload_sha256: proposal.payload_sha256, + job_id: plan.backend_job_id.clone(), + binding_id: request.binding_id.clone(), + runtime_id: identity.runtime_id, + thread_id: selection.thread_id.clone(), + launched_at: created_at.clone(), + worker_launch_document_sha256, + contract: REPO_FRONTIER_PROPOSAL_MODELING_LAUNCH_BINDING_CONTRACT.into(), + }; + if cache + .get::(&launch_binding.binding_record_id)? + .is_some() + { + return Err(anyhow!( + "proposal Modeling launch binding already exists for backend job" + )); + } + batch.push( + cache + .prepare_entry(&launch_binding.binding_record_id, &launch_binding)? + .0, + ); + } if request.binding_id == EPIPHANY_RESEARCH_ROLE_BINDING_ID { let grant = substrate_gate_repo_access_grant_for_launch( format!("substrate-grant-{}", plan.backend_job_id), @@ -102,6 +169,15 @@ pub fn plan_coordinator_job_launch( launcher_job_id: String, backend_job_id: String, ) -> Result { + let caller_projection = match &request.launch_document { + EpiphanyWorkerLaunchDocument::Role(document) => document.proposal_modeling_context.as_ref(), + EpiphanyWorkerLaunchDocument::Reorient(_) => None, + }; + if caller_projection.is_some() { + return Err(anyhow!( + "caller-prepopulated proposal Modeling context is forbidden; coordinator commit owns projection" + )); + } if let Some(expected) = request.expected_revision && expected != state.revision { @@ -110,6 +186,13 @@ pub fn plan_coordinator_job_launch( state.revision )); } + if let Some(request_id) = request.proposal_modeling_request_id.as_deref() { + let mut cache = runtime_spine_cache(runtime_store)?; + cache.pull_all_backing_stores()?; + validate_proposal_modeling_launch(&cache, state, request, request_id)?; + } else if request.owner_role == EPIPHANY_MODELING_OWNER_ROLE { + // Ordinary Modeling launches remain valid, but carry no proposal authority. + } let completed_prior_link = terminal_runtime_link_for_binding(state, &request.binding_id, runtime_store)?; let mut planning_state = state.clone(); @@ -164,6 +247,115 @@ pub fn plan_coordinator_job_launch( }) } +fn build_proposal_modeling_context_projection( + request: &RepoFrontierProposalModelingRequest, + proposal: &RepoFrontierWorkProposal, + model: &crate::EpiphanyMemoryGraphSnapshot, +) -> Result { + Ok(RepoFrontierProposalModelingContextProjection { + schema_version: REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_SCHEMA_VERSION.into(), + contract: REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_CONTRACT.into(), + request_id: request.request_id.clone(), + proposal_id: proposal.proposal_id.clone(), + proposal_payload_sha256: proposal.payload_sha256.clone(), + runtime_id: request.runtime_id.clone(), + thread_id: request.thread_id.clone(), + repository: request.repository.clone(), + workspace: request.workspace.clone(), + source_kind: proposal.source_kind, + source_actor: proposal.source_actor.clone(), + source_ref: proposal.source_ref.clone(), + title: proposal.title.clone(), + body: proposal.body.clone(), + desired_outcome: proposal.desired_outcome.clone(), + constraints: proposal.constraints.clone(), + scope_hints: proposal.scope_hints.clone(), + evidence_refs: proposal.evidence_refs.clone(), + private_state_included: proposal.private_state_included, + model_revision: model.model_revision, + model_hash: crate::memory_graph_model_hash(model)?, + }) +} + +fn validate_proposal_modeling_launch( + cache: &CultCache, + state: &EpiphanyThreadState, + launch: &EpiphanyJobLaunchRequest, + request_id: &str, +) -> Result<( + RepoFrontierProposalModelingRequest, + RepoFrontierWorkProposal, + EpiphanyRuntimeIdentity, +)> { + if match &launch.launch_document { + EpiphanyWorkerLaunchDocument::Role(document) => { + document.proposal_modeling_context.is_some() + } + EpiphanyWorkerLaunchDocument::Reorient(_) => false, + } { + return Err(anyhow!("caller cannot author proposal Modeling context")); + } + if launch.owner_role != EPIPHANY_MODELING_OWNER_ROLE + || launch.binding_id != EPIPHANY_MODELING_ROLE_BINDING_ID + { + return Err(anyhow!( + "proposal selection may only be carried by the Modeling role launch" + )); + } + let selection = cache + .get::(request_id)? + .ok_or_else(|| { + anyhow!("proposal Modeling selection request {request_id:?} does not exist") + })?; + crate::runtime_spine::validate_repo_frontier_proposal_modeling_request(&selection)?; + let proposal = cache + .get::(&selection.proposal_id)? + .ok_or_else(|| anyhow!("proposal Modeling selection references a missing proposal"))?; + crate::runtime_spine::validate_repo_frontier_work_proposal(&proposal)?; + if cache + .get_all::()? + .iter() + .any(|binding| binding.proposal_modeling_request_id == request_id) + { + return Err(anyhow!( + "proposal Modeling selection request is already bound to a launch" + )); + } + let identity = cache + .get::(RUNTIME_IDENTITY_KEY)? + .ok_or_else(|| anyhow!("proposal Modeling launch requires runtime identity"))?; + let persisted_state = cache + .get::(crate::THREAD_STATE_KEY)? + .ok_or_else(|| anyhow!("proposal Modeling launch requires authoritative thread state"))?; + let persisted_state_value = persisted_state.state()?; + let content = rmp_serde::to_vec_named(&( + &proposal.title, + &proposal.body, + &proposal.desired_outcome, + &proposal.constraints, + &proposal.scope_hints, + &proposal.evidence_refs, + ))?; + if selection.schema_version != REPO_FRONTIER_PROPOSAL_MODELING_REQUEST_SCHEMA_VERSION + || selection.contract != REPO_FRONTIER_PROPOSAL_MODELING_REQUEST_CONTRACT + || selection.request_id != request_id + || selection.proposal_payload_sha256 != proposal.payload_sha256 + || proposal.payload_sha256 != format!("{:x}", Sha256::digest(content)) + || selection.runtime_id != identity.runtime_id + || selection.runtime_id != proposal.runtime_id + || selection.thread_id != persisted_state.thread_id + || selection.thread_id != proposal.thread_id + || persisted_state_value != *state + || selection.repository != proposal.repository + || selection.workspace != proposal.workspace + { + return Err(anyhow!( + "proposal Modeling launch provenance binding mismatch" + )); + } + Ok((selection, proposal, identity)) +} + fn terminal_runtime_link_for_binding( state: &EpiphanyThreadState, binding_id: &str, @@ -196,6 +388,102 @@ fn terminal_runtime_link_for_binding( mod tests { use super::*; + fn proposal_launch_fixture( + root: &Path, + suffix: &str, + ) -> Result<( + std::path::PathBuf, + EpiphanyThreadState, + EpiphanyJobLaunchRequest, + crate::RepoFrontierProposalModelingRequest, + )> { + let store = root.join(format!("proposal-launch-{suffix}.cc")); + let runtime_id = format!("proposal-launch-runtime-{suffix}"); + let thread_id = format!("proposal-launch-thread-{suffix}"); + initialize_runtime_spine( + &store, + RuntimeSpineInitOptions { + runtime_id: runtime_id.clone(), + display_name: "Proposal launch fixture".into(), + created_at: "2026-07-13T05:00:00Z".into(), + }, + )?; + let state = EpiphanyThreadState::default(); + let cache = coordinator_acceptance_cache(&store)?; + cache.put( + THREAD_STATE_KEY, + &EpiphanyThreadStateEntry::from_state(&thread_id, &state)?, + )?; + crate::ensure_runtime_repo_model( + &store, + root.join(format!("proposal-launch-{suffix}.legacy.cc")), + &crate::EpiphanyMemoryGraphSnapshot { + schema_version: Some(crate::MEMORY_GRAPH_SCHEMA_VERSION.to_string()), + graph_id: format!("proposal-launch-model-{suffix}"), + 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: format!("proposal-launch-claim-{suffix}"), + domain_id: "repo".to_string(), + profile: crate::EpiphanyMemoryProfile::RepoArchitecture, + kind: crate::EpiphanyMemoryNodeKind::RuntimeContract, + title: "Proposal launch".to_string(), + claim: "Coordinator launch owns the proposal context projection.".to_string(), + question: "Does every selected proposal reach the exact bound worker?" + .to_string(), + action_implication: "Inject and seal the projection in the launch transaction." + .to_string(), + source_hashes: vec!["anchor:missing".to_string()], + lifecycle: crate::EpiphanyMemoryLifecycle::Accepted, + ..Default::default() + }], + ..Default::default() + }, + "2026-07-13T05:00:00Z", + )?; + let proposal_id = format!("proposal-launch-{suffix}"); + crate::intake_user_repo_frontier_proposal( + &store, + crate::RepoFrontierUserProposalInput { + proposal_id: proposal_id.clone(), + source_actor: "fixture-user".into(), + source_ref: format!("fixture:{suffix}"), + repository: "EpiphanyAgent".into(), + workspace: "E:/Projects/EpiphanyAgent".into(), + thread_id: thread_id.clone(), + runtime_id, + title: "Launch selected Modeling".into(), + body: "Bind one backend job".into(), + desired_outcome: "One immutable launch binding".into(), + constraints: vec!["No duplicate backend".into()], + scope_hints: vec!["epiphany-core/src".into()], + evidence_refs: vec![format!("fixture:{suffix}")], + private_state_included: false, + proposed_at: "2026-07-13T05:00:01Z".into(), + }, + )?; + let selection = crate::select_repo_frontier_work_proposal_for_modeling( + &store, + &proposal_id, + "2026-07-13T05:00:02Z", + )?; + let mut request = build_epiphany_role_launch_request( + &thread_id, + EpiphanyRoleResultRoleId::Modeling, + Some(state.revision), + Some(60), + &state, + ) + .map_err(|error| anyhow!(error))?; + request.proposal_modeling_request_id = Some(selection.request_id.clone()); + Ok((store, state, request, selection)) + } + fn research_launch(state: &EpiphanyThreadState) -> EpiphanyJobLaunchRequest { build_epiphany_role_launch_request( "thread-1", @@ -236,6 +524,14 @@ mod tests { fn launch_documents_state_and_grant_share_one_snapshot() -> Result<()> { let temp = tempfile::tempdir()?; let store = temp.path().join("launch.cc"); + initialize_runtime_spine( + &store, + RuntimeSpineInitOptions { + runtime_id: "coordinator-launch-test".to_string(), + display_name: "Coordinator Launch Test".to_string(), + created_at: "2026-07-10T23:59:59Z".to_string(), + }, + )?; let state = EpiphanyThreadState::default(); let request = research_launch(&state); let plan = plan_coordinator_job_launch( @@ -349,4 +645,95 @@ mod tests { ); Ok(()) } + + #[test] + fn proposal_launch_refuses_corrupt_selection_and_second_backend_without_writes() -> Result<()> { + let root = tempfile::tempdir()?; + let (store, state, request, mut selection) = + proposal_launch_fixture(root.path(), "corrupt")?; + selection.contract = "corrupt".into(); + let mut cache = coordinator_acceptance_cache(&store)?; + cache.put(&selection.request_id, &selection)?; + let before = std::fs::read(&store)?; + assert!( + plan_coordinator_job_launch( + &state, + &request, + &store, + "launcher-corrupt".into(), + "backend-corrupt".into(), + ) + .is_err() + ); + assert_eq!(std::fs::read(&store)?, before); + + let (store, state, request, selection) = proposal_launch_fixture(root.path(), "duplicate")?; + let first = plan_coordinator_job_launch( + &state, + &request, + &store, + "launcher-first".into(), + "backend-first".into(), + )?; + let committed = commit_coordinator_job_launch( + &store, + &selection.thread_id, + &state, + &request, + &first, + "2026-07-13T05:00:03Z".into(), + )?; + let persisted_launch = runtime_worker_launch_request(&store, "backend-first")? + .expect("coordinator persisted worker launch"); + let persisted_document = persisted_launch.launch_document()?; + let projection = match persisted_document { + EpiphanyWorkerLaunchDocument::Role(document) => document + .proposal_modeling_context + .expect("coordinator injected typed proposal context"), + EpiphanyWorkerLaunchDocument::Reorient(_) => { + panic!("proposal selection cannot produce reorient launch") + } + }; + assert_eq!(projection.request_id, selection.request_id); + assert_eq!(projection.proposal_id, selection.proposal_id); + assert_eq!( + projection.proposal_payload_sha256, + selection.proposal_payload_sha256 + ); + let mut cache = coordinator_acceptance_cache(&store)?; + let binding = cache + .get::( + "repo-frontier-proposal-modeling-launch-backend-first", + )? + .expect("coordinator persisted proposal launch binding"); + assert_eq!( + binding.worker_launch_document_sha256, + format!( + "{:x}", + Sha256::digest(&persisted_launch.launch_document_msgpack) + ) + ); + let mut second_request = build_epiphany_role_launch_request( + &selection.thread_id, + EpiphanyRoleResultRoleId::Modeling, + Some(committed.epiphany_state.revision), + Some(60), + &committed.epiphany_state, + ) + .map_err(|error| anyhow!(error))?; + second_request.proposal_modeling_request_id = request.proposal_modeling_request_id.clone(); + let before = std::fs::read(&store)?; + assert!( + plan_coordinator_job_launch( + &committed.epiphany_state, + &second_request, + &store, + "launcher-second".into(), + "backend-second".into(), + ) + .is_err() + ); + assert_eq!(std::fs::read(&store)?, before); + Ok(()) + } } diff --git a/epiphany-core/src/coordinator_launch_context.rs b/epiphany-core/src/coordinator_launch_context.rs index d6da10c9..cadc2704 100644 --- a/epiphany-core/src/coordinator_launch_context.rs +++ b/epiphany-core/src/coordinator_launch_context.rs @@ -831,6 +831,64 @@ mod tests { let legacy = store.with_extension("context-legacy.msgpack"); let (current, _) = crate::ensure_runtime_repo_model(store, legacy, &bootstrap, "2026-06-12T00:00:00Z")?; + let thread_id = "context-proposal-thread"; + let state = EpiphanyThreadState::default(); + let mut cache = crate::runtime_spine_cache(store)?; + cache.pull_all_backing_stores()?; + cache.put( + crate::THREAD_STATE_KEY, + &crate::EpiphanyThreadStateEntry::from_state(thread_id, &state)?, + )?; + let proposal_id = "context-route-proposal"; + crate::intake_user_repo_frontier_proposal( + store, + crate::RepoFrontierUserProposalInput { + proposal_id: proposal_id.to_string(), + source_actor: "context-test".to_string(), + source_ref: "test:verification-context".to_string(), + repository: "EpiphanyAgent".to_string(), + workspace: "E:/Projects/EpiphanyAgent".to_string(), + thread_id: thread_id.to_string(), + runtime_id: "epiphany-hands-context-test".to_string(), + title: "Verify routed Hands receipts".to_string(), + body: "The verification context needs one admitted frontier.".to_string(), + desired_outcome: "Bind verification to the exact Hands chain.".to_string(), + constraints: vec!["No direct execution authority".to_string()], + scope_hints: intent.requested_paths.clone(), + evidence_refs: vec!["context-route-evidence".to_string()], + private_state_included: false, + proposed_at: "2026-06-12T00:00:01Z".to_string(), + }, + )?; + let selection = crate::select_repo_frontier_work_proposal_for_modeling( + store, + proposal_id, + "2026-06-12T00:00:02Z", + )?; + let mut launch = crate::build_epiphany_role_launch_request( + thread_id, + crate::EpiphanyRoleResultRoleId::Modeling, + Some(state.revision), + Some(60), + &state, + ) + .map_err(anyhow::Error::msg)?; + launch.proposal_modeling_request_id = Some(selection.request_id.clone()); + let plan = crate::plan_coordinator_job_launch( + &state, + &launch, + store, + "context-route-launcher".to_string(), + "context-route-job".to_string(), + )?; + crate::commit_coordinator_job_launch( + store, + thread_id, + &state, + &launch, + &plan, + "2026-06-12T00:00:03Z".to_string(), + )?; let patch = crate::RepoModelPatch { patch_id: "context-route-patch".to_string(), base_revision: current.model_revision, @@ -846,6 +904,7 @@ mod tests { target_claim_ids: vec!["claim-context-hands".to_string()], source_scope: intent.requested_paths.clone(), recommended_next_organ: "Hands".to_string(), + evidence_refs: vec![proposal_id.to_string()], status: RepoFrontierStatus::Active, ..Default::default() }, @@ -864,7 +923,7 @@ mod tests { 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()], + evidence_ids: vec![proposal_id.to_string()], artifact_refs: Vec::new(), open_questions: Vec::new(), evidence_gaps: Vec::new(), @@ -877,6 +936,7 @@ mod tests { verification_request_id: None, frontier_route_id: None, repo_frontier_modeling_request_id: None, + proposal_modeling_request_id: Some(selection.request_id), }; crate::put_runtime_role_worker_result(store, &result)?; crate::commit_repo_model_admission( @@ -1070,6 +1130,7 @@ mod tests { launch_document: launch_request.launch_document.clone(), output_contract_id: launch_request.output_contract_id.clone(), organ_launch_contract: launch_request.organ_launch_contract.clone(), + proposal_modeling_request_id: None, created_at: "2026-06-02T00:00:00Z".to_string(), }, )?; @@ -1353,6 +1414,7 @@ mod tests { verification_request_id: Some(verification_request.request_id.clone()), frontier_route_id: Some(verification_request.route_id.clone()), repo_frontier_modeling_request_id: None, + proposal_modeling_request_id: None, }; crate::put_runtime_role_worker_result(&runtime_store, &verification_result)?; crate::put_soul_verdict_receipt( diff --git a/epiphany-core/src/coordinator_service.rs b/epiphany-core/src/coordinator_service.rs index d07b79e6..3cb3b584 100644 --- a/epiphany-core/src/coordinator_service.rs +++ b/epiphany-core/src/coordinator_service.rs @@ -38,6 +38,25 @@ impl EpiphanyCoordinatorService { pub fn store(&self) -> &Path { &self.store } + + pub fn intake_user_repo_frontier_proposal( + &self, + input: crate::RepoFrontierUserProposalInput, + ) -> Result { + crate::intake_user_repo_frontier_proposal(&self.store, input) + } + + pub fn select_repo_frontier_proposal_for_modeling( + &self, + proposal_id: &str, + selected_at: &str, + ) -> Result { + crate::select_repo_frontier_work_proposal_for_modeling( + &self.store, + proposal_id, + selected_at, + ) + } pub fn state(&self) -> Result> { read_coordinator_state(&self.store) } diff --git a/epiphany-core/src/lib.rs b/epiphany-core/src/lib.rs index 4b58e986..1bf17be4 100644 --- a/epiphany-core/src/lib.rs +++ b/epiphany-core/src/lib.rs @@ -786,6 +786,7 @@ 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::intake_user_repo_frontier_proposal; pub use runtime_spine::open_runtime_spine_heartbeat_job; pub use runtime_spine::plan_runtime_spine_heartbeat_launch; pub use runtime_spine::prepare_runtime_spine_heartbeat_job; @@ -812,6 +813,7 @@ pub use runtime_spine::put_soul_verdict_receipt; pub use runtime_spine::put_substrate_gate_repo_access_grant_receipt; pub use runtime_spine::replace_or_append_epiphany_job_binding; pub use runtime_spine::runtime_continuity_recovery_receipt; +pub use runtime_spine::runtime_current_repo_model; pub use runtime_spine::runtime_eyes_evidence_packet; pub use runtime_spine::runtime_hands_action_intent; pub use runtime_spine::runtime_hands_action_review; @@ -828,8 +830,10 @@ 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_proposal_modeling_request; pub use runtime_spine::runtime_repo_frontier_route; pub use runtime_spine::runtime_repo_frontier_verification_request; +pub use runtime_spine::runtime_repo_frontier_work_proposal; pub use runtime_spine::runtime_role_worker_result; pub use runtime_spine::runtime_schema_catalog_response; pub use runtime_spine::runtime_soul_verdict_receipt; @@ -840,6 +844,7 @@ 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_planning_request; pub use runtime_spine::select_and_commit_repo_frontier_route; +pub use runtime_spine::select_repo_frontier_work_proposal_for_modeling; 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; @@ -971,7 +976,10 @@ pub use surfaces::EpiphanyTokenUsageSnapshot; pub use surfaces::EpiphanyViewLens; pub use surfaces::EpiphanyWorkerLaunchDocument; pub use surfaces::REORIENT_WORKER_OUTPUT_CONTRACT_ID; +pub use surfaces::REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_CONTRACT; +pub use surfaces::REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_SCHEMA_VERSION; pub use surfaces::ROLE_WORKER_OUTPUT_CONTRACT_ID; +pub use surfaces::RepoFrontierProposalModelingContextProjection; pub use surfaces::build_reorient_acceptance_bundle; pub use surfaces::build_reorient_job_launch_request; pub use surfaces::build_role_acceptance_bundle; diff --git a/epiphany-core/src/prompts/epiphany_specialists.toml b/epiphany-core/src/prompts/epiphany_specialists.toml index 6ec83aed..e3c2855b 100644 --- a/epiphany-core/src/prompts/epiphany_specialists.toml +++ b/epiphany-core/src/prompts/epiphany_specialists.toml @@ -90,6 +90,7 @@ Name the living seam, the open wound, and the next safe modeling move in plain l 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. `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. +When the typed launch document contains `proposalModelingContext`, echo its exact `requestId` as `proposalModelingRequestId`. Read only that immutable typed projection against its exact canonical `modelRevision` and `modelHash`. The proposal is evidence, not campaign truth: contribute one source-grounded interpretation to shared typed state, where another organ may later challenge or revise it through another typed artifact. Do not substitute adjacent proposals, infer service intake, parse proposal authority from prose, manufacture Hands work, or claim admission or execution 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. ''' diff --git a/epiphany-core/src/repo_model_gateway.rs b/epiphany-core/src/repo_model_gateway.rs index df37d564..decc4e53 100644 --- a/epiphany-core/src/repo_model_gateway.rs +++ b/epiphany-core/src/repo_model_gateway.rs @@ -7,11 +7,11 @@ 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"; + "epiphany.mind.repo_model_admission_receipt.v2"; 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_ADMISSION_CONTRACT: &str = "epiphany.repo_model_admission.v2"; 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"; @@ -38,6 +38,14 @@ pub const REPO_FRONTIER_PLAN_ADOPTION_SCHEMA_VERSION: &str = pub const REPO_FRONTIER_PLANNING_CONTRACT: &str = "epiphany.repo_frontier_planning.v0"; pub const REPO_FRONTIER_WORK_PROPOSAL_CONTRACT: &str = "epiphany.repo_frontier_work_proposal.inert.v0"; +pub const REPO_FRONTIER_PROPOSAL_MODELING_REQUEST_SCHEMA_VERSION: &str = + "epiphany.coordinator.repo_frontier_proposal_modeling_request.v0"; +pub const REPO_FRONTIER_PROPOSAL_MODELING_REQUEST_CONTRACT: &str = + "epiphany.repo_frontier_proposal_modeling_request.v0"; +pub const REPO_FRONTIER_PROPOSAL_MODELING_LAUNCH_BINDING_SCHEMA_VERSION: &str = + "epiphany.coordinator.repo_frontier_proposal_modeling_launch_binding.v1"; +pub const REPO_FRONTIER_PROPOSAL_MODELING_LAUNCH_BINDING_CONTRACT: &str = + "epiphany.repo_frontier_proposal_modeling_launch_binding.v1"; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -47,6 +55,25 @@ pub enum RepoFrontierProposalSourceKind { Bifrost, } +#[derive(Debug, Clone)] +pub struct RepoFrontierUserProposalInput { + pub proposal_id: String, + pub source_actor: String, + pub source_ref: String, + pub repository: String, + pub workspace: String, + pub thread_id: String, + pub runtime_id: String, + pub title: String, + pub body: String, + pub desired_outcome: String, + pub constraints: Vec, + pub scope_hints: Vec, + pub evidence_refs: Vec, + pub proposed_at: String, + pub private_state_included: bool, +} + #[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] #[cultcache( type = "epiphany.evidence.repo_frontier_work_proposal", @@ -72,7 +99,7 @@ pub struct RepoFrontierWorkProposal { #[cultcache(key = 8)] pub runtime_id: String, #[cultcache(key = 9)] - pub content_sha256: String, + pub payload_sha256: String, #[cultcache(key = 10)] pub title: String, #[cultcache(key = 11)] @@ -93,6 +120,66 @@ pub struct RepoFrontierWorkProposal { pub contract: String, } +#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.coordinator.repo_frontier_proposal_modeling_request", + schema = "RepoFrontierProposalModelingRequest" +)] +pub struct RepoFrontierProposalModelingRequest { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub request_id: String, + #[cultcache(key = 2)] + pub proposal_id: String, + #[cultcache(key = 3)] + pub proposal_payload_sha256: String, + #[cultcache(key = 4)] + pub runtime_id: String, + #[cultcache(key = 5)] + pub thread_id: String, + #[cultcache(key = 6)] + pub repository: String, + #[cultcache(key = 7)] + pub workspace: String, + #[cultcache(key = 8)] + pub selected_at: String, + #[cultcache(key = 9)] + pub contract: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.coordinator.repo_frontier_proposal_modeling_launch_binding", + schema = "RepoFrontierProposalModelingLaunchBinding" +)] +pub struct RepoFrontierProposalModelingLaunchBinding { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub binding_record_id: String, + #[cultcache(key = 2)] + pub proposal_modeling_request_id: String, + #[cultcache(key = 3)] + pub proposal_id: String, + #[cultcache(key = 4)] + pub proposal_payload_sha256: String, + #[cultcache(key = 5)] + pub job_id: String, + #[cultcache(key = 6)] + pub binding_id: String, + #[cultcache(key = 7)] + pub runtime_id: String, + #[cultcache(key = 8)] + pub thread_id: String, + #[cultcache(key = 9)] + pub launched_at: String, + #[cultcache(key = 10)] + pub worker_launch_document_sha256: String, + #[cultcache(key = 11)] + pub contract: String, +} + #[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] #[cultcache( type = "epiphany.self.repo_frontier_planning_request", @@ -400,6 +487,8 @@ pub struct RepoModelAdmissionReceipt { pub soul_verdict_receipt_id: String, #[cultcache(key = 16, default)] pub frontier_modeling_request_id: String, + #[cultcache(key = 17, default)] + pub proposal_modeling_request_id: String, } #[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] diff --git a/epiphany-core/src/runtime_spine.rs b/epiphany-core/src/runtime_spine.rs index c48f529c..fd75a74c 100644 --- a/epiphany-core/src/runtime_spine.rs +++ b/epiphany-core/src/runtime_spine.rs @@ -1,4 +1,5 @@ use crate::EpiphanyWorkerLaunchDocument; +use crate::agent_launch::{EPIPHANY_MODELING_OWNER_ROLE, EPIPHANY_MODELING_ROLE_BINDING_ID}; use crate::agent_memory::AGENT_MEMORY_TYPE; use crate::continuity_gateway::ContinuityRecoveryReceipt; use crate::continuity_gateway::*; @@ -45,17 +46,21 @@ use crate::repo_model_gateway::{ REPO_FRONTIER_MODELING_REQUEST_CONTRACT, REPO_FRONTIER_MODELING_REQUEST_SCHEMA_VERSION, REPO_FRONTIER_PLAN_ADOPTION_SCHEMA_VERSION, REPO_FRONTIER_PLAN_CANDIDATE_SCHEMA_VERSION, REPO_FRONTIER_PLANNING_CONTRACT, REPO_FRONTIER_PLANNING_REQUEST_SCHEMA_VERSION, - REPO_FRONTIER_ROUTE_CONTRACT, REPO_FRONTIER_ROUTE_SCHEMA_VERSION, - REPO_FRONTIER_WORK_PROPOSAL_CONTRACT, REPO_FRONTIER_WORK_PROPOSAL_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, - RepoFrontierPlanAdoption, RepoFrontierPlanCandidate, RepoFrontierPlanDecision, - RepoFrontierPlanningRequest, RepoFrontierRoute, RepoFrontierVerdictDisposition, - RepoFrontierWorkProposal, RepoModelAdmissionReceipt, RepoModelAdmissionReview, - RepoModelMigrationReceipt, + REPO_FRONTIER_PROPOSAL_MODELING_LAUNCH_BINDING_CONTRACT, + REPO_FRONTIER_PROPOSAL_MODELING_LAUNCH_BINDING_SCHEMA_VERSION, + REPO_FRONTIER_PROPOSAL_MODELING_REQUEST_CONTRACT, + REPO_FRONTIER_PROPOSAL_MODELING_REQUEST_SCHEMA_VERSION, REPO_FRONTIER_ROUTE_CONTRACT, + REPO_FRONTIER_ROUTE_SCHEMA_VERSION, REPO_FRONTIER_WORK_PROPOSAL_CONTRACT, + REPO_FRONTIER_WORK_PROPOSAL_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, RepoFrontierPlanAdoption, RepoFrontierPlanCandidate, + RepoFrontierPlanDecision, RepoFrontierPlanningRequest, + RepoFrontierProposalModelingLaunchBinding, RepoFrontierProposalModelingRequest, + RepoFrontierRoute, RepoFrontierVerdictDisposition, RepoFrontierWorkProposal, + RepoModelAdmissionReceipt, RepoModelAdmissionReview, RepoModelMigrationReceipt, }; use crate::soul_gateway::SoulVerdictReceipt; use crate::soul_gateway::*; @@ -160,7 +165,7 @@ pub const SURFACE_REPO_BIRTH_RUNNER_TYPE: &str = "epiphany.surface.repo_birth_ru pub const RUNTIME_IDENTITY_KEY: &str = "self"; pub const RUNTIME_SPINE_SCHEMA_VERSION: &str = "epiphany.runtime_spine.v0"; pub const RUNTIME_WORKER_LAUNCH_REQUEST_SCHEMA_VERSION: &str = - "epiphany.runtime.worker_launch_request.v0"; + "epiphany.runtime.worker_launch_request.v1"; pub const RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION: &str = "epiphany.runtime.role_worker_result.v0"; pub const RUNTIME_REORIENT_WORKER_RESULT_SCHEMA_VERSION: &str = @@ -315,6 +320,8 @@ pub struct EpiphanyRuntimeWorkerLaunchRequest { pub metadata: BTreeMap, #[cultcache(key = 10, default)] pub organ_launch_contract: EpiphanyLaunchOrganContract, + #[cultcache(key = 11, default)] + pub proposal_modeling_request_id: Option, } impl EpiphanyRuntimeWorkerLaunchRequest { @@ -388,6 +395,8 @@ pub struct EpiphanyRuntimeRoleWorkerResult { pub frontier_route_id: Option, #[cultcache(key = 23, default)] pub repo_frontier_modeling_request_id: Option, + #[cultcache(key = 24, default)] + pub proposal_modeling_request_id: Option, } impl EpiphanyRuntimeRoleWorkerResult { @@ -664,6 +673,7 @@ pub struct RuntimeSpineHeartbeatJobOptions { pub launch_document: EpiphanyWorkerLaunchDocument, pub output_contract_id: String, pub organ_launch_contract: EpiphanyLaunchOrganContract, + pub proposal_modeling_request_id: Option, pub created_at: String, } @@ -711,6 +721,7 @@ pub struct EpiphanyJobLaunchRequest { pub output_contract_id: String, pub organ_launch_contract: EpiphanyLaunchOrganContract, pub max_runtime_seconds: Option, + pub proposal_modeling_request_id: Option, } #[derive(Debug, Clone, PartialEq)] @@ -759,6 +770,8 @@ 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::()?; @@ -1016,6 +1029,12 @@ pub fn open_runtime_spine_heartbeat_job( validate_non_empty(&options.objective, "objective")?; validate_non_empty(&options.job_id, "job id")?; validate_non_empty(&options.role, "role")?; + validate_proposal_modeling_launch_carrier( + &options.role, + &options.binding_id, + options.proposal_modeling_request_id.as_deref(), + &options.launch_document, + )?; validate_non_empty(&options.binding_id, "binding id")?; validate_non_empty(&options.authority_scope, "authority scope")?; validate_non_empty(&options.instruction, "instruction")?; @@ -1099,6 +1118,7 @@ pub fn open_runtime_spine_heartbeat_job( launch_document_msgpack: encode_worker_launch_document(&launch_document)?, metadata: BTreeMap::new(), organ_launch_contract, + proposal_modeling_request_id: options.proposal_modeling_request_id, }; cache.put(&job_id, &request)?; Ok(job) @@ -1114,6 +1134,12 @@ pub fn prepare_runtime_spine_heartbeat_job( validate_non_empty(&options.objective, "objective")?; validate_non_empty(&options.job_id, "job id")?; validate_non_empty(&options.role, "role")?; + validate_proposal_modeling_launch_carrier( + &options.role, + &options.binding_id, + options.proposal_modeling_request_id.as_deref(), + &options.launch_document, + )?; validate_non_empty(&options.binding_id, "binding id")?; validate_non_empty(&options.authority_scope, "authority scope")?; validate_non_empty(&options.instruction, "instruction")?; @@ -1223,6 +1249,7 @@ pub fn prepare_runtime_spine_heartbeat_job( launch_document_msgpack: encode_worker_launch_document(&options.launch_document)?, metadata: BTreeMap::new(), organ_launch_contract: options.organ_launch_contract, + proposal_modeling_request_id: options.proposal_modeling_request_id, }; let envelopes = vec![ cache.prepare_entry(RUNTIME_IDENTITY_KEY, &identity)?.0, @@ -1234,6 +1261,39 @@ pub fn prepare_runtime_spine_heartbeat_job( Ok(PreparedRuntimeSpineHeartbeatJob { job, envelopes }) } +fn validate_proposal_modeling_launch_carrier( + role: &str, + binding_id: &str, + proposal_modeling_request_id: Option<&str>, + launch_document: &EpiphanyWorkerLaunchDocument, +) -> Result<()> { + let projection = match launch_document { + EpiphanyWorkerLaunchDocument::Role(document) => document.proposal_modeling_context.as_ref(), + EpiphanyWorkerLaunchDocument::Reorient(_) => None, + }; + let Some(request_id) = proposal_modeling_request_id else { + if projection.is_some() { + return Err(anyhow!( + "proposal Modeling context requires its typed request id" + )); + } + return Ok(()); + }; + validate_non_empty(request_id, "proposal Modeling request id")?; + if role != EPIPHANY_MODELING_OWNER_ROLE || binding_id != EPIPHANY_MODELING_ROLE_BINDING_ID { + return Err(anyhow!( + "proposal Modeling request id may only be transported by the Modeling role launch" + )); + } + let projection = projection.ok_or_else(|| { + anyhow!("proposal Modeling request id requires coordinator-owned typed context") + })?; + if projection.request_id != request_id { + return Err(anyhow!("proposal Modeling context/request mismatch")); + } + Ok(()) +} + pub fn runtime_job_snapshot( store_path: impl AsRef, job_id: &str, @@ -1303,6 +1363,58 @@ pub fn put_runtime_role_worker_result( "only Modeling results may echo a frontier Modeling request" )); } + if result.proposal_modeling_request_id.is_some() + && !result.role_id.eq_ignore_ascii_case("modeling") + { + return Err(anyhow!( + "only Modeling results may echo a proposal Modeling request" + )); + } + let is_modeling = result.role_id.eq_ignore_ascii_case("modeling"); + if is_modeling + && result.repo_frontier_modeling_request_id.is_some() + && result.proposal_modeling_request_id.is_some() + { + return Err(anyhow!( + "Modeling result authority echoes are mutually exclusive" + )); + } + if is_modeling + && (result.verification_request_id.is_some() || result.frontier_route_id.is_some()) + { + return Err(anyhow!( + "Modeling result cannot claim Verification route authority" + )); + } + if is_modeling + && (result.repo_frontier_modeling_request_id.is_some() + || result.proposal_modeling_request_id.is_some()) + { + let patch_bytes = result + .repo_model_patch_msgpack + .as_deref() + .ok_or_else(|| anyhow!("Modeling authority echo requires repoModelPatch"))?; + let patch: crate::RepoModelPatch = rmp_serde::from_slice(patch_bytes) + .context("decode Modeling authority echo repoModelPatch")?; + match patch.purpose { + crate::RepoModelPatchPurpose::Evolution + if result.repo_frontier_modeling_request_id.is_some() => + { + return Err(anyhow!( + "Evolution result cannot echo a verdict-incorporation request" + )); + } + crate::RepoModelPatchPurpose::IncorporateFrontierVerdict { .. } + if result.proposal_modeling_request_id.is_some() + || result.repo_frontier_modeling_request_id.is_none() => + { + return Err(anyhow!( + "verdict incorporation requires only its frontier Modeling request echo" + )); + } + _ => {} + } + } let mut cache = runtime_spine_cache(store_path)?; cache.pull_all_backing_stores()?; cache.put(&result.job_id, result)?; @@ -1470,14 +1582,293 @@ pub fn commit_repo_model_admission( verification_request_id, soul_verdict_receipt_id, frontier_modeling_request_id, + proposal_modeling_request_id, ) = match &patch.purpose { crate::RepoModelPatchPurpose::Evolution => { - (String::new(), String::new(), String::new(), String::new()) + if result.repo_frontier_modeling_request_id.is_some() + || result.verification_request_id.is_some() + || result.frontier_route_id.is_some() + { + return Err(anyhow!( + "Evolution result cannot claim verdict-incorporation or Verification authority" + )); + } + let echoed = result.proposal_modeling_request_id.as_deref(); + let upserts = patch + .operations + .iter() + .filter_map(|operation| match operation { + crate::RepoModelPatchOperation::UpsertFrontier { item } => Some(item), + _ => None, + }) + .collect::>(); + let frontier_operation_count = patch + .operations + .iter() + .filter(|operation| { + matches!( + operation, + crate::RepoModelPatchOperation::UpsertFrontier { .. } + | crate::RepoModelPatchOperation::ReviseFrontier { .. } + | crate::RepoModelPatchOperation::RetireFrontier { .. } + ) + }) + .count(); + let request_id = if let Some(request_id) = echoed { + let request = cache + .get::(request_id)? + .ok_or_else(|| { + anyhow!("proposal Evolution requires exact selection request") + })?; + validate_repo_frontier_proposal_modeling_request(&request)?; + let proposal = cache + .get::(&request.proposal_id)? + .ok_or_else(|| anyhow!("proposal Evolution requires exact proposal"))?; + validate_repo_frontier_work_proposal(&proposal)?; + let launch_bindings = cache + .get_all::()? + .into_iter() + .filter(|binding| binding.job_id == result.job_id) + .collect::>(); + if launch_bindings.len() != 1 { + return Err(anyhow!( + "proposal Evolution requires exactly one coordinator launch binding for its job" + )); + } + let launch_binding = &launch_bindings[0]; + let worker_launch = cache + .get::(&result.job_id)? + .ok_or_else(|| { + anyhow!("proposal Evolution requires its runtime worker launch request") + })?; + let worker_launch_document = worker_launch.launch_document()?; + let launch_projection = match &worker_launch_document { + EpiphanyWorkerLaunchDocument::Role(document) => { + document.proposal_modeling_context.as_ref() + } + EpiphanyWorkerLaunchDocument::Reorient(_) => None, + }; + let identity = require_identity(&cache)?; + let thread = cache + .get::(crate::THREAD_STATE_KEY)? + .ok_or_else(|| { + anyhow!("proposal Evolution requires authoritative thread state") + })?; + thread.state()?; + let content = rmp_serde::to_vec_named(&( + &proposal.title, + &proposal.body, + &proposal.desired_outcome, + &proposal.constraints, + &proposal.scope_hints, + &proposal.evidence_refs, + ))?; + let payload_hash = format!("{:x}", Sha256::digest(content)); + let expected_projection = crate::RepoFrontierProposalModelingContextProjection { + schema_version: crate::REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_SCHEMA_VERSION + .into(), + contract: crate::REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_CONTRACT.into(), + request_id: request.request_id.clone(), + proposal_id: proposal.proposal_id.clone(), + proposal_payload_sha256: proposal.payload_sha256.clone(), + runtime_id: request.runtime_id.clone(), + thread_id: request.thread_id.clone(), + repository: request.repository.clone(), + workspace: request.workspace.clone(), + source_kind: proposal.source_kind, + source_actor: proposal.source_actor.clone(), + source_ref: proposal.source_ref.clone(), + title: proposal.title.clone(), + body: proposal.body.clone(), + desired_outcome: proposal.desired_outcome.clone(), + constraints: proposal.constraints.clone(), + scope_hints: proposal.scope_hints.clone(), + evidence_refs: proposal.evidence_refs.clone(), + private_state_included: proposal.private_state_included, + model_revision: patch.base_revision, + model_hash: patch.base_hash.clone(), + }; + let launch_document_sha256 = format!( + "{:x}", + Sha256::digest(&worker_launch.launch_document_msgpack) + ); + let mismatches = [ + ( + "request.schema", + request.schema_version + != REPO_FRONTIER_PROPOSAL_MODELING_REQUEST_SCHEMA_VERSION, + ), + ( + "request.contract", + request.contract != REPO_FRONTIER_PROPOSAL_MODELING_REQUEST_CONTRACT, + ), + ( + "proposal.schema", + proposal.schema_version != REPO_FRONTIER_WORK_PROPOSAL_SCHEMA_VERSION, + ), + ( + "proposal.contract", + proposal.contract != REPO_FRONTIER_WORK_PROPOSAL_CONTRACT, + ), + ("proposal.private", proposal.private_state_included), + ( + "request.payload", + request.proposal_payload_sha256 != proposal.payload_sha256, + ), + ("proposal.payload", proposal.payload_sha256 != payload_hash), + ( + "request.proposal_runtime", + request.runtime_id != proposal.runtime_id, + ), + ("request.runtime", request.runtime_id != identity.runtime_id), + ( + "request.proposal_thread", + request.thread_id != proposal.thread_id, + ), + ("request.thread", request.thread_id != thread.thread_id), + ( + "request.repository", + request.repository != proposal.repository, + ), + ("request.workspace", request.workspace != proposal.workspace), + ( + "binding.id", + launch_binding.binding_record_id + != format!("repo-frontier-proposal-modeling-launch-{}", result.job_id), + ), + ( + "binding.schema", + launch_binding.schema_version + != REPO_FRONTIER_PROPOSAL_MODELING_LAUNCH_BINDING_SCHEMA_VERSION, + ), + ( + "binding.contract", + launch_binding.contract + != REPO_FRONTIER_PROPOSAL_MODELING_LAUNCH_BINDING_CONTRACT, + ), + ( + "binding.request", + launch_binding.proposal_modeling_request_id != request.request_id, + ), + ( + "binding.proposal", + launch_binding.proposal_id != proposal.proposal_id, + ), + ( + "binding.payload", + launch_binding.proposal_payload_sha256 != proposal.payload_sha256, + ), + ("binding.job", launch_binding.job_id != result.job_id), + ( + "binding.role", + launch_binding.binding_id != EPIPHANY_MODELING_ROLE_BINDING_ID, + ), + ( + "binding.runtime", + launch_binding.runtime_id != identity.runtime_id, + ), + ( + "binding.thread", + launch_binding.thread_id != thread.thread_id, + ), + ( + "binding.launched_at", + chrono::DateTime::parse_from_rfc3339(&launch_binding.launched_at).is_err(), + ), + ( + "binding.launch_document_hash", + launch_binding.worker_launch_document_sha256 != launch_document_sha256, + ), + ( + "launch.schema", + worker_launch.schema_version + != RUNTIME_WORKER_LAUNCH_REQUEST_SCHEMA_VERSION, + ), + ("launch.job", worker_launch.job_id != result.job_id), + ( + "launch.binding", + worker_launch.binding_id != launch_binding.binding_id, + ), + ( + "launch.role", + worker_launch.role != EPIPHANY_MODELING_OWNER_ROLE, + ), + ( + "launch.request", + worker_launch.proposal_modeling_request_id.as_deref() + != Some(request.request_id.as_str()), + ), + ( + "launch.projection", + launch_projection != Some(&expected_projection), + ), + ( + "launch.model_base", + expected_projection.model_revision != patch.base_revision + || expected_projection.model_hash != patch.base_hash, + ), + ] + .into_iter() + .filter_map(|(name, failed)| failed.then_some(name)) + .collect::>(); + if !mismatches.is_empty() { + return Err(anyhow!( + "proposal Evolution provenance binding mismatch: {}", + mismatches.join(", ") + )); + } + if cache + .get_all::()? + .iter() + .any(|receipt| { + receipt.proposal_modeling_request_id == request.request_id + && (receipt.review_id != review.review_id + || receipt.result_id != result.result_id) + }) + { + return Err(anyhow!("proposal Modeling request already incorporated")); + } + if !result_evidence.iter().any(|id| id == &proposal.proposal_id) + || frontier_operation_count != 1 + || upserts.len() != 1 + || !upserts[0] + .evidence_refs + .iter() + .any(|id| id == &proposal.proposal_id) + { + return Err(anyhow!( + "proposal Evolution requires one proposal-citing frontier upsert and exact evidence" + )); + } + request.request_id + } else { + if frontier_operation_count != 0 { + return Err(anyhow!( + "ordinary Evolution cannot mutate frontier without explicit proposal request" + )); + } + String::new() + }; + ( + String::new(), + String::new(), + String::new(), + String::new(), + request_id, + ) } crate::RepoModelPatchPurpose::IncorporateFrontierVerdict { route_id, soul_verdict_receipt_id, } => { + if result.proposal_modeling_request_id.is_some() + || result.verification_request_id.is_some() + || result.frontier_route_id.is_some() + { + return Err(anyhow!( + "verdict incorporation result may carry only its frontier Modeling request echo" + )); + } let route = cache.get::(route_id)?.ok_or_else(|| { anyhow!("frontier verdict incorporation requires its exact route") })?; @@ -1636,6 +2027,7 @@ pub fn commit_repo_model_admission( request.request_id, verdict.receipt_id, modeling_request.request_id, + String::new(), ) } }; @@ -1656,6 +2048,7 @@ pub fn commit_repo_model_admission( || 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 + || existing_receipt.proposal_modeling_request_id != proposal_modeling_request_id { return Err(anyhow!("repo model admission receipt identity collision")); } @@ -1773,6 +2166,7 @@ pub fn commit_repo_model_admission( verification_request_id, soul_verdict_receipt_id, frontier_modeling_request_id, + proposal_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)?; @@ -1816,8 +2210,7 @@ fn put_immutable_planning_entry, +pub(crate) fn validate_repo_frontier_work_proposal( proposal: &RepoFrontierWorkProposal, ) -> Result<()> { if proposal.schema_version != REPO_FRONTIER_WORK_PROPOSAL_SCHEMA_VERSION @@ -1845,18 +2238,199 @@ pub fn put_repo_frontier_work_proposal( &proposal.scope_hints, &proposal.evidence_refs, ))?; - if proposal.content_sha256 != format!("{:x}", Sha256::digest(content)) { + if proposal.payload_sha256 != format!("{:x}", Sha256::digest(content)) { return Err(anyhow!("proposal content hash mismatch")); } + Ok(()) +} + +pub(crate) fn validate_repo_frontier_proposal_modeling_request( + request: &RepoFrontierProposalModelingRequest, +) -> Result<()> { + if request.schema_version != REPO_FRONTIER_PROPOSAL_MODELING_REQUEST_SCHEMA_VERSION + || request.contract != REPO_FRONTIER_PROPOSAL_MODELING_REQUEST_CONTRACT + || request.request_id.trim().is_empty() + || request.proposal_id.trim().is_empty() + || request.proposal_payload_sha256.trim().is_empty() + || request.runtime_id.trim().is_empty() + || request.thread_id.trim().is_empty() + || request.repository.trim().is_empty() + || request.workspace.trim().is_empty() + || chrono::DateTime::parse_from_rfc3339(&request.selected_at).is_err() + { + return Err(anyhow!( + "invalid coordinator repo frontier proposal Modeling request" + )); + } + Ok(()) +} + +pub fn put_repo_frontier_work_proposal( + store_path: impl AsRef, + proposal: &RepoFrontierWorkProposal, +) -> Result<()> { + validate_repo_frontier_work_proposal(proposal)?; let mut cache = runtime_spine_cache(store_path.as_ref())?; cache.pull_all_backing_stores()?; let identity = require_identity(&cache)?; if identity.runtime_id != proposal.runtime_id { return Err(anyhow!("proposal runtime identity mismatch")); } + let thread = cache + .get::(crate::THREAD_STATE_KEY)? + .ok_or_else(|| anyhow!("proposal intake requires authoritative thread state"))?; + if thread.thread_id != proposal.thread_id { + return Err(anyhow!("proposal thread identity mismatch")); + } put_immutable_planning_entry(store_path.as_ref(), &proposal.proposal_id, proposal) } +pub fn intake_user_repo_frontier_proposal( + store_path: impl AsRef, + input: crate::RepoFrontierUserProposalInput, +) -> Result { + let content = rmp_serde::to_vec_named(&( + &input.title, + &input.body, + &input.desired_outcome, + &input.constraints, + &input.scope_hints, + &input.evidence_refs, + ))?; + let proposal = RepoFrontierWorkProposal { + schema_version: REPO_FRONTIER_WORK_PROPOSAL_SCHEMA_VERSION.into(), + proposal_id: input.proposal_id, + source_kind: crate::RepoFrontierProposalSourceKind::User, + source_actor: input.source_actor, + source_ref: input.source_ref, + repository: input.repository, + workspace: input.workspace, + thread_id: input.thread_id, + runtime_id: input.runtime_id, + payload_sha256: format!("{:x}", Sha256::digest(content)), + title: input.title, + body: input.body, + desired_outcome: input.desired_outcome, + constraints: input.constraints, + scope_hints: input.scope_hints, + evidence_refs: input.evidence_refs, + private_state_included: input.private_state_included, + proposed_at: input.proposed_at, + contract: REPO_FRONTIER_WORK_PROPOSAL_CONTRACT.into(), + }; + put_repo_frontier_work_proposal(store_path, &proposal)?; + Ok(proposal) +} + +pub fn runtime_repo_frontier_work_proposal( + store_path: impl AsRef, + proposal_id: &str, +) -> Result> { + validate_non_empty(proposal_id, "repo frontier work proposal id")?; + let mut cache = runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + cache.get::(proposal_id) +} + +pub fn runtime_repo_frontier_proposal_modeling_request( + store_path: impl AsRef, + request_id: &str, +) -> Result> { + validate_non_empty(request_id, "repo frontier proposal Modeling request id")?; + let mut cache = runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + cache.get::(request_id) +} + +pub fn runtime_current_repo_model( + store_path: impl AsRef, +) -> Result> { + let mut cache = runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + cache + .get::(crate::MEMORY_GRAPH_KEY)? + .map(|entry| entry.snapshot()) + .transpose() +} + +pub fn select_repo_frontier_work_proposal_for_modeling( + store_path: impl AsRef, + proposal_id: &str, + selected_at: &str, +) -> Result { + chrono::DateTime::parse_from_rfc3339(selected_at) + .map_err(|_| anyhow!("proposal selection timestamp must be RFC3339"))?; + let store_path = store_path.as_ref(); + let mut cache = runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + let identity = require_identity(&cache)?; + let proposal = cache + .get::(proposal_id)? + .ok_or_else(|| anyhow!("proposal selection requires exact persisted proposal"))?; + validate_repo_frontier_work_proposal(&proposal)?; + let thread = cache + .get::(crate::THREAD_STATE_KEY)? + .ok_or_else(|| anyhow!("proposal selection requires authoritative thread state"))?; + if proposal.runtime_id != identity.runtime_id || proposal.thread_id != thread.thread_id { + return Err(anyhow!("proposal selection provenance mismatch")); + } + let existing_requests = cache.get_all::()?; + for receipt in cache.get_all::()? { + if receipt.proposal_modeling_request_id.is_empty() { + continue; + } + let incorporated = cache + .get::(&receipt.proposal_modeling_request_id)? + .ok_or_else(|| anyhow!("admission names missing proposal request"))?; + validate_repo_frontier_proposal_modeling_request(&incorporated)?; + if incorporated.proposal_id == proposal_id { + return Err(anyhow!("proposal already incorporated")); + } + } + let request_id = format!( + "repo-frontier-proposal-modeling-{:x}", + Sha256::digest(format!("{}:{}", proposal.proposal_id, proposal.payload_sha256).as_bytes()) + ); + let request = RepoFrontierProposalModelingRequest { + schema_version: REPO_FRONTIER_PROPOSAL_MODELING_REQUEST_SCHEMA_VERSION.into(), + request_id: request_id.clone(), + proposal_id: proposal.proposal_id.clone(), + proposal_payload_sha256: proposal.payload_sha256.clone(), + runtime_id: proposal.runtime_id.clone(), + thread_id: proposal.thread_id.clone(), + repository: proposal.repository.clone(), + workspace: proposal.workspace.clone(), + selected_at: selected_at.into(), + contract: REPO_FRONTIER_PROPOSAL_MODELING_REQUEST_CONTRACT.into(), + }; + if let Some(existing) = existing_requests + .into_iter() + .find(|r| r.proposal_id == proposal_id) + { + validate_repo_frontier_proposal_modeling_request(&existing)?; + return if existing == request { + Ok(existing) + } else { + Err(anyhow!("proposal selection identity conflict")) + }; + } + match put_immutable_planning_entry(store_path, &request_id, &request) { + Ok(()) => Ok(request), + Err(error) => { + let mut reloaded = runtime_spine_cache(store_path)?; + reloaded.pull_all_backing_stores()?; + if let Some(existing) = + reloaded.get::(&request_id)? + { + if existing == request { + return Ok(existing); + } + } + Err(error) + } + } +} + pub fn select_and_commit_repo_frontier_planning_request( runtime_store: impl AsRef, at: &str, @@ -5378,11 +5952,71 @@ mod tests { } fn repo_model_result_and_review( + store: &Path, result_id: &str, job_id: &str, current: &crate::EpiphanyMemoryGraphSnapshot, review_id: &str, ) -> Result<(EpiphanyRuntimeRoleWorkerResult, RepoModelAdmissionReview)> { + let mut cache = runtime_spine_cache(store)?; + cache.pull_all_backing_stores()?; + let identity = require_identity(&cache)?; + let thread_id = format!("fixture-thread-{result_id}"); + let state = epiphany_state_model::EpiphanyThreadState::default(); + cache.put( + crate::THREAD_STATE_KEY, + &crate::EpiphanyThreadStateEntry::from_state(&thread_id, &state)?, + )?; + let proposal_id = format!("proposal-{result_id}"); + intake_user_repo_frontier_proposal( + store, + crate::RepoFrontierUserProposalInput { + proposal_id: proposal_id.clone(), + source_actor: "fixture-user".into(), + source_ref: format!("fixture:{result_id}"), + repository: "EpiphanyAgent".into(), + workspace: "E:/Projects/EpiphanyAgent".into(), + thread_id: thread_id.clone(), + runtime_id: identity.runtime_id, + title: format!("Frontier seed {result_id}"), + body: "Typed fixture proposal".into(), + desired_outcome: "One contestable frontier contribution".into(), + constraints: vec!["No direct Hands authority".into()], + scope_hints: vec!["epiphany-core/src/runtime_spine.rs".into()], + evidence_refs: vec![format!("evidence-{result_id}")], + private_state_included: false, + proposed_at: "2026-07-13T03:59:58Z".into(), + }, + )?; + let selection = select_repo_frontier_work_proposal_for_modeling( + store, + &proposal_id, + "2026-07-13T03:59:59Z", + )?; + let mut launch = crate::build_epiphany_role_launch_request( + &thread_id, + crate::EpiphanyRoleResultRoleId::Modeling, + Some(state.revision), + Some(60), + &state, + ) + .map_err(|error| anyhow!(error))?; + launch.proposal_modeling_request_id = Some(selection.request_id.clone()); + let plan = crate::plan_coordinator_job_launch( + &state, + &launch, + store, + format!("launcher-{job_id}"), + job_id.to_string(), + )?; + crate::commit_coordinator_job_launch( + store, + &thread_id, + &state, + &launch, + &plan, + "2026-07-13T04:00:00Z".into(), + )?; let patch = crate::RepoModelPatch { patch_id: format!("patch-{result_id}"), base_revision: current.model_revision, @@ -5398,6 +6032,7 @@ mod tests { 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(), + evidence_refs: vec![proposal_id.clone()], status: crate::RepoFrontierStatus::Active, ..Default::default() }, @@ -5416,7 +6051,7 @@ mod tests { 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()], + evidence_ids: vec![proposal_id], artifact_refs: Vec::new(), open_questions: Vec::new(), evidence_gaps: Vec::new(), @@ -5429,6 +6064,7 @@ mod tests { verification_request_id: None, frontier_route_id: None, repo_frontier_modeling_request_id: None, + proposal_modeling_request_id: Some(selection.request_id), }; let review = RepoModelAdmissionReview { schema_version: REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION.to_string(), @@ -5447,6 +6083,408 @@ mod tests { Ok((result, review)) } + fn proposal_admission_fixture( + root: &Path, + suffix: &str, + ) -> Result<( + PathBuf, + EpiphanyRuntimeRoleWorkerResult, + RepoModelAdmissionReview, + )> { + let store = root.join(format!("proposal-admission-{suffix}.cc")); + initialize_runtime_spine( + &store, + RuntimeSpineInitOptions { + runtime_id: format!("proposal-runtime-{suffix}"), + display_name: "Proposal admission fixture".into(), + created_at: "2026-07-13T03:00:00Z".into(), + }, + )?; + let bootstrap = repo_model_bootstrap(); + let legacy = root.join(format!("proposal-{suffix}.legacy")); + let (current, _) = + ensure_runtime_repo_model(&store, &legacy, &bootstrap, "2026-07-13T03:30:00Z")?; + let (result, review) = repo_model_result_and_review( + &store, + &format!("proposal-result-{suffix}"), + &format!("proposal-job-{suffix}"), + ¤t, + &format!("proposal-review-{suffix}"), + )?; + Ok((store, result, review)) + } + + fn assert_proposal_admission_refused_without_state_mutation( + store: &Path, + result: &EpiphanyRuntimeRoleWorkerResult, + review: &RepoModelAdmissionReview, + ) -> Result<()> { + put_runtime_role_worker_result(store, result)?; + let before = std::fs::read(store)?; + assert!(commit_repo_model_admission(store, &result.result_id, review).is_err()); + assert_eq!(std::fs::read(store)?, before); + Ok(()) + } + + fn proposal_selection_fixture(root: &Path, suffix: &str) -> Result<(PathBuf, String)> { + let store = root.join(format!("proposal-selection-{suffix}.cc")); + let runtime_id = format!("selection-runtime-{suffix}"); + let thread_id = format!("selection-thread-{suffix}"); + let proposal_id = format!("selection-proposal-{suffix}"); + initialize_runtime_spine( + &store, + RuntimeSpineInitOptions { + runtime_id: runtime_id.clone(), + display_name: "Proposal selection fixture".into(), + created_at: "2026-07-13T02:00:00Z".into(), + }, + )?; + let mut cache = runtime_spine_cache(&store)?; + cache.put( + crate::THREAD_STATE_KEY, + &crate::EpiphanyThreadStateEntry::from_state( + &thread_id, + &epiphany_state_model::EpiphanyThreadState::default(), + )?, + )?; + intake_user_repo_frontier_proposal( + &store, + crate::RepoFrontierUserProposalInput { + proposal_id: proposal_id.clone(), + source_actor: "fixture-user".into(), + source_ref: format!("fixture:{suffix}"), + repository: "EpiphanyAgent".into(), + workspace: "E:/Projects/EpiphanyAgent".into(), + thread_id, + runtime_id, + title: "Selected contribution".into(), + body: "Model this exact proposal".into(), + desired_outcome: "One typed frontier".into(), + constraints: vec!["No implicit selection".into()], + scope_hints: vec!["epiphany-core/src".into()], + evidence_refs: vec![format!("fixture:{suffix}")], + private_state_included: false, + proposed_at: "2026-07-13T02:01:00Z".into(), + }, + )?; + Ok((store, proposal_id)) + } + + fn overwrite_test_entry( + store: &Path, + key: &str, + value: &T, + ) -> Result<()> { + let mut cache = runtime_spine_cache(store)?; + cache.pull_all_backing_stores()?; + cache.put(key, value).map(|_| ()) + } + + #[test] + fn proposal_intake_selection_and_admission_refuse_hostile_substitution_without_mutation() + -> Result<()> { + let root = tempdir()?; + for (suffix, mutate) in [ + ("missing-request", 0u8), + ("swapped-request", 1), + ("missing-evidence", 2), + ("zero-upsert", 3), + ("multiple-upsert", 4), + ("ordinary-bypass", 5), + ("dual-echo", 6), + ("unrelated-job", 7), + ] { + let (store, mut result, mut review) = proposal_admission_fixture(root.path(), suffix)?; + match mutate { + 0 => result.proposal_modeling_request_id = Some("missing-request".into()), + 1 => result.proposal_modeling_request_id = Some("swapped-request".into()), + 2 => { + result.evidence_ids = vec!["wrong-evidence".into()]; + review.evidence_ids = result.evidence_ids.clone(); + } + 3 => { + let mut patch: crate::RepoModelPatch = + rmp_serde::from_slice(result.repo_model_patch_msgpack.as_deref().unwrap())?; + patch.operations.clear(); + let bytes = rmp_serde::to_vec_named(&patch)?; + result.repo_model_patch_msgpack = Some(bytes.clone()); + review.patch_sha256 = format!("{:x}", Sha256::digest(bytes)); + } + 4 => { + let mut patch: crate::RepoModelPatch = + rmp_serde::from_slice(result.repo_model_patch_msgpack.as_deref().unwrap())?; + patch.operations.push(patch.operations[0].clone()); + let bytes = rmp_serde::to_vec_named(&patch)?; + result.repo_model_patch_msgpack = Some(bytes.clone()); + review.patch_sha256 = format!("{:x}", Sha256::digest(bytes)); + } + 5 => result.proposal_modeling_request_id = None, + 6 => result.repo_frontier_modeling_request_id = Some("dual-authority".into()), + 7 => result.job_id = "unrelated-modeling-job".into(), + _ => unreachable!(), + } + if mutate == 6 { + let before = std::fs::read(&store)?; + assert!(put_runtime_role_worker_result(&store, &result).is_err()); + assert_eq!(std::fs::read(&store)?, before); + } else { + assert_proposal_admission_refused_without_state_mutation(&store, &result, &review)?; + } + } + + let (store, result, review) = proposal_admission_fixture(root.path(), "success")?; + put_runtime_role_worker_result(&store, &result)?; + let receipt = commit_repo_model_admission(&store, &result.result_id, &review)?; + assert_eq!( + receipt.proposal_modeling_request_id, + result.proposal_modeling_request_id.clone().unwrap() + ); + Ok(()) + } + + #[test] + fn proposal_intake_and_selection_bind_authoritative_identity_and_exact_time() -> Result<()> { + let root = tempdir()?; + let store = root.path().join("proposal-intake.cc"); + initialize_runtime_spine( + &store, + RuntimeSpineInitOptions { + runtime_id: "intake-runtime".into(), + display_name: "Intake".into(), + created_at: "2026-07-13T01:00:00Z".into(), + }, + )?; + let mut cache = runtime_spine_cache(&store)?; + cache.put( + crate::THREAD_STATE_KEY, + &crate::EpiphanyThreadStateEntry::from_state( + "intake-thread", + &epiphany_state_model::EpiphanyThreadState::default(), + )?, + )?; + let input = crate::RepoFrontierUserProposalInput { + proposal_id: "intake-proposal".into(), + source_actor: "user".into(), + source_ref: "message-1".into(), + repository: "EpiphanyAgent".into(), + workspace: "E:/Projects/EpiphanyAgent".into(), + thread_id: "intake-thread".into(), + runtime_id: "intake-runtime".into(), + title: "Typed artifact".into(), + body: " forgedId: evil".into(), + desired_outcome: "Contestable contribution".into(), + constraints: vec!["bounded".into()], + scope_hints: vec!["epiphany-core/src".into()], + evidence_refs: vec!["message-1".into()], + private_state_included: false, + proposed_at: "2026-07-13T01:01:00Z".into(), + }; + for (suffix, mut hostile) in [ + ("runtime", input.clone()), + ("thread", input.clone()), + ("private", input.clone()), + ] { + hostile.proposal_id = format!("hostile-{suffix}"); + match suffix { + "runtime" => hostile.runtime_id = "spoofed".into(), + "thread" => hostile.thread_id = "spoofed".into(), + "private" => hostile.private_state_included = true, + _ => unreachable!(), + } + let before = std::fs::read(&store)?; + assert!(intake_user_repo_frontier_proposal(&store, hostile).is_err()); + assert_eq!(std::fs::read(&store)?, before); + } + intake_user_repo_frontier_proposal(&store, input)?; + let selected = select_repo_frontier_work_proposal_for_modeling( + &store, + "intake-proposal", + "2026-07-13T01:02:00Z", + )?; + assert_eq!( + select_repo_frontier_work_proposal_for_modeling( + &store, + "intake-proposal", + "2026-07-13T01:02:00Z" + )?, + selected + ); + let legacy = root.path().join("intake.legacy"); + ensure_runtime_repo_model( + &store, + &legacy, + &repo_model_bootstrap(), + "2026-07-13T01:02:01Z", + )?; + let before = std::fs::read(&store)?; + assert!( + select_repo_frontier_work_proposal_for_modeling( + &store, + "intake-proposal", + "2026-07-13T01:03:00Z" + ) + .is_err() + ); + assert_eq!(std::fs::read(&store)?, before); + Ok(()) + } + + #[test] + fn concurrent_exact_proposal_selection_converges_on_one_request() -> Result<()> { + let root = tempdir()?; + let (store, proposal_id) = proposal_selection_fixture(root.path(), "concurrent")?; + let barrier = std::sync::Arc::new(std::sync::Barrier::new(3)); + let mut workers = Vec::new(); + for _ in 0..2 { + let store = store.clone(); + let proposal_id = proposal_id.clone(); + let barrier = barrier.clone(); + workers.push(std::thread::spawn(move || { + barrier.wait(); + select_repo_frontier_work_proposal_for_modeling( + store, + &proposal_id, + "2026-07-13T02:02:00Z", + ) + })); + } + barrier.wait(); + let first = workers.remove(0).join().expect("selector thread")?; + let second = workers.remove(0).join().expect("selector thread")?; + assert_eq!(first, second); + let mut cache = runtime_spine_cache(&store)?; + cache.pull_all_backing_stores()?; + assert_eq!( + cache + .get_all::()? + .len(), + 1 + ); + Ok(()) + } + + #[test] + fn corrupted_proposal_or_selection_is_fail_closed_without_authority_mutation() -> Result<()> { + let root = tempdir()?; + for corruption in ["schema", "contract", "hash", "privacy"] { + let (store, proposal_id) = proposal_selection_fixture(root.path(), corruption)?; + let mut proposal = runtime_repo_frontier_work_proposal(&store, &proposal_id)? + .expect("fixture proposal"); + match corruption { + "schema" => proposal.schema_version = "corrupt".into(), + "contract" => proposal.contract = "corrupt".into(), + "hash" => proposal.payload_sha256 = "00".repeat(32), + "privacy" => proposal.private_state_included = true, + _ => unreachable!(), + } + overwrite_test_entry(&store, &proposal_id, &proposal)?; + let before = std::fs::read(&store)?; + assert!( + select_repo_frontier_work_proposal_for_modeling( + &store, + &proposal_id, + "2026-07-13T02:02:00Z" + ) + .is_err() + ); + assert_eq!(std::fs::read(&store)?, before); + } + + let (store, proposal_id) = proposal_selection_fixture(root.path(), "request-contract")?; + let mut request = select_repo_frontier_work_proposal_for_modeling( + &store, + &proposal_id, + "2026-07-13T02:02:00Z", + )?; + request.contract = "corrupt".into(); + overwrite_test_entry(&store, &request.request_id, &request)?; + let before = std::fs::read(&store)?; + assert!( + select_repo_frontier_work_proposal_for_modeling( + &store, + &proposal_id, + "2026-07-13T02:02:00Z" + ) + .is_err() + ); + assert_eq!(std::fs::read(&store)?, before); + Ok(()) + } + + #[test] + fn proposal_admission_refuses_stale_model_swapped_launch_and_companion_collision() -> Result<()> + { + let root = tempdir()?; + for corruption in ["stale-model", "swapped-launch", "wrong-receipt"] { + let (store, mut result, mut review) = + proposal_admission_fixture(root.path(), corruption)?; + match corruption { + "stale-model" => { + let mut patch: crate::RepoModelPatch = + rmp_serde::from_slice(result.repo_model_patch_msgpack.as_deref().unwrap())?; + patch.base_revision += 1; + patch.base_hash = "00".repeat(32); + let bytes = rmp_serde::to_vec_named(&patch)?; + review.base_revision = patch.base_revision; + review.base_hash = patch.base_hash; + review.patch_sha256 = format!("{:x}", Sha256::digest(&bytes)); + result.repo_model_patch_msgpack = Some(bytes); + } + "swapped-launch" => { + let mut cache = runtime_spine_cache(&store)?; + cache.pull_all_backing_stores()?; + let mut binding = cache + .get_all::()? + .into_iter() + .next() + .expect("proposal launch binding"); + binding.job_id = "swapped-backend-job".into(); + let key = binding.binding_record_id.clone(); + overwrite_test_entry(&store, &key, &binding)?; + } + "wrong-receipt" => { + put_runtime_role_worker_result(&store, &result)?; + let current = runtime_current_repo_model(&store)?.expect("current model"); + let patch: crate::RepoModelPatch = + rmp_serde::from_slice(result.repo_model_patch_msgpack.as_deref().unwrap())?; + let next = crate::derive_repo_model_patch(¤t, &patch)?; + let receipt_id = format!("repo-model-admission-{}", review.review_id); + let receipt = RepoModelAdmissionReceipt { + schema_version: REPO_MODEL_ADMISSION_RECEIPT_SCHEMA_VERSION.into(), + receipt_id: receipt_id.clone(), + review_id: review.review_id.clone(), + result_id: result.result_id.clone(), + patch_id: patch.patch_id, + patch_sha256: review.patch_sha256.clone(), + previous_revision: current.model_revision, + previous_hash: crate::memory_graph_model_hash(¤t)?, + admitted_revision: next.model_revision, + admitted_hash: next.model_hash, + admitted_at: review.reviewed_at.clone(), + contract: REPO_MODEL_ADMISSION_CONTRACT.into(), + purpose: patch.purpose, + frontier_route_id: String::new(), + verification_request_id: String::new(), + soul_verdict_receipt_id: String::new(), + frontier_modeling_request_id: String::new(), + proposal_modeling_request_id: "wrong-proposal-request".into(), + }; + overwrite_test_entry(&store, &review.review_id, &review)?; + overwrite_test_entry(&store, &receipt_id, &receipt)?; + let before = std::fs::read(&store)?; + assert!( + commit_repo_model_admission(&store, &result.result_id, &review).is_err() + ); + assert_eq!(std::fs::read(&store)?, before); + continue; + } + _ => unreachable!(), + } + assert_proposal_admission_refused_without_state_mutation(&store, &result, &review)?; + } + Ok(()) + } + #[test] fn typed_frontier_planning_chain_is_exact_immutable_and_non_routing() -> Result<()> { let temp = tempdir()?; @@ -5459,6 +6497,14 @@ mod tests { created_at: "2026-07-13T03:00:00Z".into(), }, )?; + let mut state_cache = runtime_spine_cache(&store)?; + state_cache.put( + crate::THREAD_STATE_KEY, + &crate::EpiphanyThreadStateEntry::from_state( + "thread-1", + &epiphany_state_model::EpiphanyThreadState::default(), + )?, + )?; let proposal_content = rmp_serde::to_vec_named(&( "Plan typed frontier work", "Build the inert planning foundation.", @@ -5477,7 +6523,7 @@ mod tests { workspace: "E:/Projects/EpiphanyAgent".into(), thread_id: "thread-1".into(), runtime_id: "planning-test".into(), - content_sha256: format!("{:x}", Sha256::digest(proposal_content)), + payload_sha256: format!("{:x}", Sha256::digest(proposal_content)), title: "Plan typed frontier work".into(), body: "Build the inert planning foundation.".into(), desired_outcome: "Produce typed planning documents.".into(), @@ -5499,6 +6545,7 @@ mod tests { let (current, _) = ensure_runtime_repo_model(&store, &legacy, &bootstrap, "2026-07-13T04:00:00Z")?; let (mut result, mut review) = repo_model_result_and_review( + &store, "planning-result", "planning-job", ¤t, @@ -5685,6 +6732,7 @@ mod tests { 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( + store, &format!("route-result-{suffix}"), &format!("route-job-{suffix}"), ¤t, @@ -5845,6 +6893,7 @@ mod tests { verification_request_id: Some(request.request_id.clone()), frontier_route_id: Some(route.route_id.clone()), repo_frontier_modeling_request_id: None, + proposal_modeling_request_id: None, }; put_runtime_role_worker_result(&store, &verification_result)?; let verdict = SoulVerdictReceipt { @@ -5962,6 +7011,7 @@ mod tests { verification_request_id: None, frontier_route_id: None, repo_frontier_modeling_request_id: Some(fixture.modeling_request.request_id.clone()), + proposal_modeling_request_id: None, }; let review = RepoModelAdmissionReview { schema_version: REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION.to_string(), @@ -6000,8 +7050,13 @@ mod tests { "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")?; + let (result, review) = repo_model_result_and_review( + &store, + "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); @@ -6035,6 +7090,7 @@ mod tests { .expect("admitted model") .snapshot()?; let (stale_result, stale_review) = repo_model_result_and_review( + &store, "model-result-stale", "model-job-stale", ¤t, @@ -6049,6 +7105,7 @@ mod tests { assert_eq!(admitted.model_revision, 1); let (collision_result, collision_review) = repo_model_result_and_review( + &store, "model-result-collision", "model-job-collision", &admitted, @@ -6074,6 +7131,7 @@ mod tests { 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( + &routed.store, "evolution-after-route", "evolution-after-route-job", &routed.current, @@ -6104,6 +7162,7 @@ mod tests { "2026-07-13T08:00:00Z", )?; let (seed_result, seed_review) = repo_model_result_and_review( + &store, "evolution-seed", "evolution-seed-job", &base, @@ -6133,6 +7192,7 @@ mod tests { ), ] { let (mut result, mut review) = repo_model_result_and_review( + &store, &format!("evolution-{suffix}"), &format!("evolution-{suffix}-job"), ¤t, @@ -6226,6 +7286,7 @@ mod tests { verification_request_id: Some(fixture.request.request_id.clone()), frontier_route_id: Some(fixture.route.route_id.clone()), repo_frontier_modeling_request_id: None, + proposal_modeling_request_id: None, }; put_runtime_role_worker_result(&fixture.store, &adjacent)?; put_soul_verdict_receipt( @@ -6380,25 +7441,30 @@ mod tests { ) -> Result<()> { let bootstrap = repo_model_bootstrap(); let legacy = store.with_extension(format!("{suffix}.legacy.msgpack")); - let (current, _) = + let (mut 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)?; + for (index, item) in items.into_iter().enumerate() { + let identity = format!("{suffix}-{index}"); + let (mut result, mut review) = repo_model_result_and_review( + store, + &format!("eligibility-result-{identity}"), + &format!("eligibility-job-{identity}"), + ¤t, + &format!("eligibility-review-{identity}"), + )?; + let mut patch: crate::RepoModelPatch = + rmp_serde::from_slice(result.repo_model_patch_msgpack.as_deref().unwrap())?; + let proposal_id = result.evidence_ids[0].clone(); + let mut item = item; + item.evidence_refs.push(proposal_id); + patch.operations = vec![crate::RepoModelPatchOperation::UpsertFrontier { item }]; + 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)?; + current = runtime_current_repo_model(store)?.expect("admitted model"); + } Ok(()) } @@ -7427,6 +8493,7 @@ mod tests { state_revision: 7, objective: Some("Run heartbeat worker.".to_string()), dynamic_prompt_context: None, + proposal_modeling_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: vec!["node-model".to_string()], @@ -7448,6 +8515,7 @@ mod tests { "role", crate::ROLE_WORKER_OUTPUT_CONTRACT_ID, ), + proposal_modeling_request_id: None, created_at: "2026-05-06T00:02:00Z".to_string(), }, )?; @@ -7516,6 +8584,7 @@ mod tests { state_revision: 7, objective: Some("keep state typed".to_string()), dynamic_prompt_context: None, + proposal_modeling_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: vec!["runtime-spine".to_string()], @@ -7600,6 +8669,7 @@ mod tests { state_revision: 7, objective: None, dynamic_prompt_context: None, + proposal_modeling_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: Vec::new(), diff --git a/epiphany-core/src/substrate_gate.rs b/epiphany-core/src/substrate_gate.rs index 0b968bca..04ef8730 100644 --- a/epiphany-core/src/substrate_gate.rs +++ b/epiphany-core/src/substrate_gate.rs @@ -249,6 +249,7 @@ mod tests { state_revision: 7, objective: None, dynamic_prompt_context: None, + proposal_modeling_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: Vec::new(), @@ -271,6 +272,7 @@ mod tests { crate::ROLE_WORKER_OUTPUT_CONTRACT_ID, ), max_runtime_seconds: None, + proposal_modeling_request_id: None, }; let grant = substrate_gate_repo_access_grant_for_launch( "grant-1".to_string(), diff --git a/epiphany-core/src/surfaces/mod.rs b/epiphany-core/src/surfaces/mod.rs index 2c2c1097..07d2828e 100644 --- a/epiphany-core/src/surfaces/mod.rs +++ b/epiphany-core/src/surfaces/mod.rs @@ -144,5 +144,8 @@ pub use worker_launch::EpiphanyReorientWorkerLaunchDocument; pub use worker_launch::EpiphanyRoleWorkerLaunchDocument; pub use worker_launch::EpiphanyWorkerLaunchDocument; pub use worker_launch::REORIENT_WORKER_OUTPUT_CONTRACT_ID; +pub use worker_launch::REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_CONTRACT; +pub use worker_launch::REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_SCHEMA_VERSION; pub use worker_launch::ROLE_WORKER_OUTPUT_CONTRACT_ID; +pub use worker_launch::RepoFrontierProposalModelingContextProjection; pub use worker_launch::build_reorient_job_launch_request; diff --git a/epiphany-core/src/surfaces/worker_launch.rs b/epiphany-core/src/surfaces/worker_launch.rs index 82ffd2a2..d9b66335 100644 --- a/epiphany-core/src/surfaces/worker_launch.rs +++ b/epiphany-core/src/surfaces/worker_launch.rs @@ -26,6 +26,10 @@ use crate::EpiphanyReorientReason; pub const ROLE_WORKER_OUTPUT_CONTRACT_ID: &str = "epiphany.worker.role_result.v0"; pub const REORIENT_WORKER_OUTPUT_CONTRACT_ID: &str = "epiphany.worker.reorient_result.v0"; +pub const REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_SCHEMA_VERSION: &str = + "epiphany.worker.repo_frontier_proposal_modeling_context.v0"; +pub const REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_CONTRACT: &str = + "epiphany.repo_frontier_proposal_modeling_context.v0"; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", tag = "documentKind")] @@ -77,6 +81,8 @@ pub struct EpiphanyRoleWorkerLaunchDocument { #[serde(default, skip_serializing_if = "Option::is_none")] pub dynamic_prompt_context: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub proposal_modeling_context: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub active_subgoal_id: Option, #[serde(default)] pub active_subgoals: Vec, @@ -165,6 +171,35 @@ pub struct EpiphanyReorientLaunchRequestInput<'a> { pub dynamic_prompt_context: Option, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RepoFrontierProposalModelingContextProjection { + pub schema_version: String, + pub contract: String, + pub request_id: String, + pub proposal_id: String, + pub proposal_payload_sha256: String, + pub runtime_id: String, + pub thread_id: String, + pub repository: String, + pub workspace: String, + pub source_kind: crate::RepoFrontierProposalSourceKind, + pub source_actor: String, + pub source_ref: String, + pub title: String, + pub body: String, + pub desired_outcome: String, + #[serde(default)] + pub constraints: Vec, + #[serde(default)] + pub scope_hints: Vec, + #[serde(default)] + pub evidence_refs: Vec, + pub private_state_included: bool, + pub model_revision: u64, + pub model_hash: String, +} + pub fn build_reorient_job_launch_request( input: EpiphanyReorientLaunchRequestInput<'_>, ) -> EpiphanyJobLaunchRequest { @@ -205,6 +240,7 @@ pub fn build_reorient_job_launch_request( output_contract_id, organ_launch_contract, max_runtime_seconds: input.max_runtime_seconds, + proposal_modeling_request_id: None, } } diff --git a/epiphany-openai-runtime/src/lib.rs b/epiphany-openai-runtime/src/lib.rs index b1c9cc12..d38d89ab 100644 --- a/epiphany-openai-runtime/src/lib.rs +++ b/epiphany-openai-runtime/src/lib.rs @@ -1354,6 +1354,7 @@ struct RoleWorkerResultIngress { verification_request_id: Option, frontier_route_id: Option, repo_frontier_modeling_request_id: Option, + proposal_modeling_request_id: Option, } #[derive(Debug, Clone, Default, serde::Deserialize)] @@ -1484,6 +1485,9 @@ fn role_worker_result_from_ingress( repo_frontier_modeling_request_id: clean_optional_string( result.repo_frontier_modeling_request_id.as_deref(), ), + proposal_modeling_request_id: clean_optional_string( + result.proposal_modeling_request_id.as_deref(), + ), } } @@ -1621,6 +1625,7 @@ mod tests { "role", epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID, ), + proposal_modeling_request_id: None, }; let result = role_worker_result_from_ingress( &launch, @@ -1812,6 +1817,7 @@ mod tests { "\nlocal Verse: bounded\n" .to_string(), ), + proposal_modeling_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: Vec::new(), @@ -1833,6 +1839,7 @@ mod tests { "role", epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID, ), + proposal_modeling_request_id: None, created_at: now(), }, )?; @@ -1937,6 +1944,7 @@ mod tests { "hands receipts" .to_string(), ), + proposal_modeling_context: None, active_subgoal_id: None, active_subgoals: Vec::new(), active_graph_node_ids: Vec::new(), @@ -1958,6 +1966,7 @@ mod tests { "role", epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID, ), + proposal_modeling_request_id: None, created_at: now(), }, )?; diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 516c09d5..27bf9176 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -130,13 +130,25 @@ revalidate current state. Nothing in this foundation emits an execution route, Hands authority, or frontier lifecycle change. Full core proof is 296 passed and 1 ignored; independent Soul verdict is PASS. -Proposal-to-frontier admission is deliberately still absent. Do not infer a -selected proposal by intersecting arbitrary evidence or add patch metadata. -Add one explicit coordinator/user proposal intake and one typed selected- -proposal Modeling launch identity. Modeling's Evolution patch must cite that -proposal in result evidence and exactly one UpsertFrontier evidence_refs; Mind -reloads the immutable proposal and verifies content/repo binding before -admission. Only the admitted frontier becomes anatomy. +Proposal-to-frontier admission is now explicit and Soul-approved. Direct User +intake writes one immutable, payload-hashed, nonprivate proposal bound to the +persisted runtime and thread; repository/workspace remain provenance, not +counterfeit authority. Coordinator selection is immutable, exact-time, and +race-safe. The coordinator launch transaction is the sole writer of a typed +`proposalModelingContext` projection inside the persisted Modeling launch +document; it atomically seals that document's MessagePack SHA-256 in an +immutable job/request/proposal launch binding. Prompt prose and result echoes +are correlation only. Admission reconstructs and compares the exact proposal, +selection, projection, patch base, launch hash, job, binding, runtime, and +thread chain. Ordinary Evolution cannot mutate frontier. Proposal Evolution +gets exactly one frontier operation and it must be one proposal-citing +`UpsertFrontier`; it emits no plan, route, Hands authority, or lifecycle +decision. Core proof is 302 passed/1 ignored, OpenAI runtime proof is 8 passed, +and final independent Soul verdict is PASS. Hostile proofs cover privacy +laundering, changed selection time, concurrent selection, corrupt persisted +contracts and hashes, caller-prepopulated context, stale base, swapped launch +and companions, duplicate backend binding, dual authority echoes, request +reuse, delimiter injection, and byte-identical no-write refusals. The next artifact-contract audit then cut `epiphany.persona_chat.v0`: its live writer and catalog had disagreed on field vocabulary, evidence fields, and @@ -187,10 +199,14 @@ receipt; a swapped valid adoption chain fails. Six compiled smokes that taught the deleted caller-writable `mindAdoptionDecision` model were removed. Next architectural cut: finish the Aetheria lesson rather than polishing this -gate. Modeling needs a production semantic index over typed graph claims and a -durable frontier projection that automatically targets later Eyes/Hands -prompts. Memory-graph freshness is substrate; the living migration map is the -organ. +gate. The proposal now reaches Modeling as an exact contestable artifact, but +the larger corrective ecology is still incomplete. Add typed Eyes challenges +that can invalidate model claims and create evidence pressure without direct +RepoModel write authority; then bind the existing planning request to an +Imagination worker result and Mind adoption. A production semantic index over +typed graph claims remains required so those organs can retrieve the living +migration map instead of rediscovering it from files. Memory-graph freshness is +substrate; durable multi-organ correction is the organism. Epiphany is in an authority-provenance purification pass. The live question is not whether a command can produce a plausible document; it is whether the subsystem writing that document owns the fact it asserts. diff --git a/notes/repo-frontier-organ-loop.md b/notes/repo-frontier-organ-loop.md index b191bbf1..f75692ef 100644 --- a/notes/repo-frontier-organ-loop.md +++ b/notes/repo-frontier-organ-loop.md @@ -78,6 +78,24 @@ authority. split-retry, and terminal nonselection proofs pass, and independent Soul review accepts this slice. +## Selected Proposal Ingress + +The first upstream contribution path now preserves the same causal discipline. +A User proposal is inert evidence. Coordinator selection does not make it true, +and a request ID is not a bearer token. The coordinator launch transaction +atomically binds one selection to one Modeling job and injects the exact typed +proposal projection into that job's persisted launch document. The binding +seals the worker-visible document hash. Modeling may contribute one contestable +frontier interpretation; Mind admits it only after replaying the exact +proposal/request/projection/job/result/review chain. No plan, route, Hands +authority, or frontier lifecycle decision is created by this ingress. + +Ordinary Modeling Evolution may revise repository anatomy but cannot mutate +frontier. Selected-proposal Evolution may perform exactly one frontier +operation: one proposal-citing `UpsertFrontier`. This keeps the proposal mouth +from becoming a hidden workflow engine while leaving later Eyes, Imagination, +Self, Hands, and Soul artifacts free to challenge and reshape the contribution. + ## Next Deletion Line `RepoWorkModelingRequest`, `RepoWorkModelingRoute`, and diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 89f8a8ed912d49dcb2770ab4d9a06a9d16b3c132..fe4cc9ae294f2a2690236924a34525d36e616854 100644 GIT binary patch delta 500 zcmYk2%}N6?6osAo)2*l=2!#u7JMFY1Dhfgo1sBqVcH=Ic+_ov1Buz4{U3DSo3s{Oz z;8MYb_yRtNdl$ZenQ>@c90Iu~-#Irg-=+JX((9(TzIPo2M?rWLhBu!}#ZmOO>kT~b z>26whm=-*z^jvmYaawg+d!H7HJ9gvoqaM`v{ouf_<&0V5%PUQuuaIM8k@`wYB@N-g zL|3CRAmj+zOi%+XjuT7T9HC`U!`$hg0N#?wLL{Z8Ob{zIm<-6I05|?O@R{gmNnpnc zDO^g5eB#1@lSD+l6q_3Imwnl;Y8)hJY;_)(JA1_RQhJ6+39fI#t@95Vjxv_ng| zzp>{zrUIu@BZNU66fp*AwFay!+#M}LQ=w>FfKJ4gWii5G?kPjZeIlR5y14xdaWS!O delta 66 zcmZqsD7*fx>;_>*0pk!OBP&yLD^ruGV+`!2J)4yo3mF+_G*4w}pUT7t#2}g(h*^M` O6^PlkPi114Dggk=Llh