diff --git a/epiphany-core/src/coordinator_launch.rs b/epiphany-core/src/coordinator_launch.rs index 821a7538..80890019 100644 --- a/epiphany-core/src/coordinator_launch.rs +++ b/epiphany-core/src/coordinator_launch.rs @@ -409,7 +409,7 @@ mod tests { }, )?; let state = EpiphanyThreadState::default(); - let cache = coordinator_acceptance_cache(&store)?; + let mut cache = coordinator_acceptance_cache(&store)?; cache.put( THREAD_STATE_KEY, &EpiphanyThreadStateEntry::from_state(&thread_id, &state)?, @@ -700,7 +700,7 @@ mod tests { projection.proposal_payload_sha256, selection.proposal_payload_sha256 ); - let mut cache = coordinator_acceptance_cache(&store)?; + let cache = coordinator_acceptance_cache(&store)?; let binding = cache .get::( "repo-frontier-proposal-modeling-launch-backend-first", diff --git a/epiphany-core/src/lib.rs b/epiphany-core/src/lib.rs index 1bf17be4..6ffc718e 100644 --- a/epiphany-core/src/lib.rs +++ b/epiphany-core/src/lib.rs @@ -780,6 +780,7 @@ pub use runtime_spine::clear_epiphany_job_binding_backend; pub use runtime_spine::commit_repo_frontier_modeling_request; pub use runtime_spine::commit_repo_frontier_verification_request_for_chain; pub use runtime_spine::commit_repo_model_admission; +pub use runtime_spine::commit_repo_model_claim_challenge; pub use runtime_spine::complete_runtime_job; pub use runtime_spine::create_runtime_job; pub use runtime_spine::create_runtime_session; diff --git a/epiphany-core/src/repo_model_gateway.rs b/epiphany-core/src/repo_model_gateway.rs index decc4e53..20f99295 100644 --- a/epiphany-core/src/repo_model_gateway.rs +++ b/epiphany-core/src/repo_model_gateway.rs @@ -46,6 +46,61 @@ 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"; +pub const REPO_MODEL_CLAIM_CHALLENGE_SCHEMA_VERSION: &str = + "epiphany.eyes.repo_model_claim_challenge.v0"; +pub const REPO_MODEL_CLAIM_CHALLENGE_CONTRACT: &str = "epiphany.repo_model_claim_challenge.v0"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RepoModelClaimChallengeDisposition { + Contradicted, + Stale, + EvidenceInsufficient, +} + +#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.eyes.repo_model_claim_challenge", + schema = "RepoModelClaimChallenge" +)] +pub struct RepoModelClaimChallenge { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub challenge_id: String, + #[cultcache(key = 2)] + pub eyes_evidence_packet_id: String, + #[cultcache(key = 3)] + pub eyes_evidence_packet_sha256: String, + #[cultcache(key = 4)] + pub source_result_id: String, + #[cultcache(key = 5)] + pub source_job_id: String, + #[cultcache(key = 6)] + pub model_revision: u64, + #[cultcache(key = 7)] + pub model_hash: String, + #[cultcache(key = 8)] + pub admission_receipt_id: String, + #[cultcache(key = 9)] + pub target_claim_id: String, + #[cultcache(key = 10)] + pub target_claim_sha256: String, + #[cultcache(key = 11)] + pub disposition: RepoModelClaimChallengeDisposition, + #[cultcache(key = 12)] + pub finding: String, + #[cultcache(key = 13)] + pub uncertainty: String, + #[cultcache(key = 14)] + pub source_refs: Vec, + #[cultcache(key = 15)] + pub evidence_ids: Vec, + #[cultcache(key = 16)] + pub challenged_at: String, + #[cultcache(key = 17)] + pub contract: String, +} #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] diff --git a/epiphany-core/src/runtime_spine.rs b/epiphany-core/src/runtime_spine.rs index fd75a74c..5f51c163 100644 --- a/epiphany-core/src/runtime_spine.rs +++ b/epiphany-core/src/runtime_spine.rs @@ -54,13 +54,15 @@ use crate::repo_model_gateway::{ 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_CLAIM_CHALLENGE_CONTRACT, REPO_MODEL_CLAIM_CHALLENGE_SCHEMA_VERSION, 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, + RepoModelAdmissionReceipt, RepoModelAdmissionReview, RepoModelClaimChallenge, + RepoModelMigrationReceipt, }; use crate::soul_gateway::SoulVerdictReceipt; use crate::soul_gateway::*; @@ -766,6 +768,7 @@ 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::()?; @@ -2466,7 +2469,8 @@ pub fn select_and_commit_repo_frontier_planning_request( "planning requires exactly one current admission receipt" )); } - let item = actionable_imagination_frontier_item(&model) + let challenges = current_repo_model_claim_challenges(&cache, &model, &model_hash)?; + let item = actionable_imagination_frontier_item(&model, &challenges) .ok_or_else(|| anyhow!("planning requires an actionable Imagination frontier"))?; let item_hash = format!("{:x}", Sha256::digest(rmp_serde::to_vec_named(item)?)); if cache @@ -2525,9 +2529,10 @@ pub fn select_and_commit_repo_frontier_planning_request( Ok(request) } -fn actionable_imagination_frontier_item( - model: &crate::EpiphanyMemoryGraphSnapshot, -) -> Option<&crate::RepoFrontierItem> { +fn actionable_imagination_frontier_item<'a>( + model: &'a crate::EpiphanyMemoryGraphSnapshot, + challenges: &[RepoModelClaimChallenge], +) -> Option<&'a crate::RepoFrontierItem> { let terminal = |id: &str| { model .frontier @@ -2550,6 +2555,7 @@ fn actionable_imagination_frontier_item( && item.recommended_next_organ == "Imagination" && !item.source_scope.is_empty() && safe_sorted_unique_paths(&item.source_scope) + && frontier_target_claims_unchallenged(item, challenges) && item.dependency_item_ids.iter().all(|id| terminal(id)) }) .collect::>(); @@ -2557,6 +2563,176 @@ fn actionable_imagination_frontier_item( eligible.into_iter().next() } +fn frontier_target_claims_unchallenged( + item: &crate::RepoFrontierItem, + challenges: &[RepoModelClaimChallenge], +) -> bool { + !challenges + .iter() + .any(|challenge| item.target_claim_ids.contains(&challenge.target_claim_id)) +} + +fn current_repo_model_claim_challenges( + cache: &CultCache, + model: &crate::EpiphanyMemoryGraphSnapshot, + model_hash: &str, +) -> Result> { + let mut current = Vec::new(); + for challenge in cache.get_all::()? { + if challenge.model_revision == model.model_revision && challenge.model_hash == model_hash { + validate_repo_model_claim_challenge_chain(cache, model, model_hash, &challenge)?; + current.push(challenge); + } + } + Ok(current) +} + +fn validate_repo_model_claim_challenge_chain( + cache: &CultCache, + model: &crate::EpiphanyMemoryGraphSnapshot, + model_hash: &str, + challenge: &RepoModelClaimChallenge, +) -> Result<()> { + if challenge.schema_version != REPO_MODEL_CLAIM_CHALLENGE_SCHEMA_VERSION + || challenge.contract != REPO_MODEL_CLAIM_CHALLENGE_CONTRACT + || challenge.challenge_id.trim().is_empty() + || challenge.finding.trim().is_empty() + || challenge.uncertainty.trim().is_empty() + || challenge.source_refs.is_empty() + || challenge.evidence_ids.is_empty() + || chrono::DateTime::parse_from_rfc3339(&challenge.challenged_at).is_err() + { + return Err(anyhow!("invalid repo model claim challenge")); + } + let packet = cache + .get::(&challenge.eyes_evidence_packet_id)? + .ok_or_else(|| anyhow!("claim challenge requires exact Eyes evidence packet"))?; + if packet.schema_version != EYES_EVIDENCE_PACKET_SCHEMA_VERSION + || packet.contract.trim().is_empty() + || chrono::DateTime::parse_from_rfc3339(&packet.emitted_at).is_err() + || packet.source_result_id != challenge.source_result_id + || packet.source_job_id != challenge.source_job_id + || packet.source_refs != challenge.source_refs + || packet.evidence_ids != challenge.evidence_ids + || format!("{:x}", Sha256::digest(rmp_serde::to_vec_named(&packet)?)) + != challenge.eyes_evidence_packet_sha256 + { + return Err(anyhow!("claim challenge substituted Eyes evidence")); + } + if challenge.model_revision != model.model_revision || challenge.model_hash != model_hash { + return Err(anyhow!("claim challenge model revision is stale")); + } + let receipts = cache + .get_all::()? + .into_iter() + .filter(|receipt| { + receipt.schema_version == REPO_MODEL_ADMISSION_RECEIPT_SCHEMA_VERSION + && receipt.contract == REPO_MODEL_ADMISSION_CONTRACT + && receipt.admitted_revision == model.model_revision + && receipt.admitted_hash == model_hash + }) + .collect::>(); + if receipts.len() != 1 || receipts[0].receipt_id != challenge.admission_receipt_id { + return Err(anyhow!( + "claim challenge requires the unique current admission receipt" + )); + } + let claim = model + .nodes + .iter() + .find(|node| node.id == challenge.target_claim_id) + .ok_or_else(|| anyhow!("claim challenge target claim is missing"))?; + if format!("{:x}", Sha256::digest(rmp_serde::to_vec_named(claim)?)) + != challenge.target_claim_sha256 + { + return Err(anyhow!("claim challenge target claim identity mismatch")); + } + Ok(()) +} + +pub fn commit_repo_model_claim_challenge( + store_path: impl AsRef, + challenge: &RepoModelClaimChallenge, +) -> Result<()> { + if challenge.schema_version != REPO_MODEL_CLAIM_CHALLENGE_SCHEMA_VERSION + || challenge.contract != REPO_MODEL_CLAIM_CHALLENGE_CONTRACT + || challenge.challenge_id.trim().is_empty() + || challenge.finding.trim().is_empty() + || challenge.uncertainty.trim().is_empty() + || challenge.source_refs.is_empty() + || challenge.evidence_ids.is_empty() + || chrono::DateTime::parse_from_rfc3339(&challenge.challenged_at).is_err() + { + return Err(anyhow!("invalid repo model claim challenge")); + } + let store_path = store_path.as_ref(); + let mut cache = runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + let backing = SingleFileMessagePackBackingStore::new(store_path); + let envelopes = backing.pull_all()?; + let model_envelope = envelopes + .iter() + .find(|entry| { + entry.r#type == crate::MEMORY_GRAPH_TYPE && entry.key == crate::MEMORY_GRAPH_KEY + }) + .cloned() + .ok_or_else(|| anyhow!("claim challenge requires canonical model"))?; + let entry: crate::EpiphanyMemoryGraphEntry = rmp_serde::from_slice(&model_envelope.payload)?; + crate::validate_memory_graph_entry(&entry)?; + let model = entry.snapshot()?; + let model_hash = crate::memory_graph_model_hash(&model)?; + if challenge.model_revision != model.model_revision || challenge.model_hash != model_hash { + return Err(anyhow!("claim challenge model revision is stale")); + } + validate_repo_model_claim_challenge_chain(&cache, &model, &model_hash, challenge)?; + if let Some(existing) = cache.get::(&challenge.challenge_id)? { + return if existing == *challenge { + Ok(()) + } else { + Err(anyhow!("claim challenge ids are immutable")) + }; + } + let packet_envelope = envelopes + .iter() + .find(|entry| { + entry.r#type == EYES_EVIDENCE_PACKET_TYPE + && entry.key == challenge.eyes_evidence_packet_id + }) + .cloned() + .ok_or_else(|| anyhow!("claim challenge packet envelope is missing"))?; + let admission_envelope = envelopes + .iter() + .find(|entry| { + entry.r#type == REPO_MODEL_ADMISSION_RECEIPT_TYPE + && entry.key == challenge.admission_receipt_id + }) + .cloned() + .ok_or_else(|| anyhow!("claim challenge admission envelope is missing"))?; + let (challenge_envelope, _) = cache.prepare_entry(&challenge.challenge_id, challenge)?; + if !backing.compare_and_swap_batch( + &[ + model_envelope.clone(), + packet_envelope.clone(), + admission_envelope.clone(), + ], + vec![ + model_envelope, + packet_envelope, + admission_envelope, + challenge_envelope, + ], + )? { + let mut reloaded = runtime_spine_cache(store_path)?; + reloaded.pull_all_backing_stores()?; + return match reloaded.get::(&challenge.challenge_id)? { + Some(existing) if existing == *challenge => Ok(()), + Some(_) => Err(anyhow!("claim challenge immutable collision")), + None => Err(anyhow!("claim challenge lost exact model/packet CAS")), + }; + } + Ok(()) +} + pub fn put_repo_frontier_plan_candidate( store_path: impl AsRef, candidate: &RepoFrontierPlanCandidate, @@ -2775,7 +2951,8 @@ pub fn select_and_commit_repo_frontier_route( )); } let receipt = &receipts[0]; - let item = actionable_hands_frontier_item(¤t) + let challenges = current_repo_model_claim_challenges(&cache, ¤t, ¤t_hash)?; + let item = actionable_hands_frontier_item(¤t, &challenges) .ok_or_else(|| anyhow!("current repo model has no eligible Hands frontier route"))?; if !safe_sorted_unique_paths(&item.source_scope) || item.source_scope.is_empty() { return Err(anyhow!( @@ -2828,9 +3005,10 @@ pub fn select_and_commit_repo_frontier_route( Ok(route) } -fn actionable_hands_frontier_item( - model: &crate::EpiphanyMemoryGraphSnapshot, -) -> Option<&crate::RepoFrontierItem> { +fn actionable_hands_frontier_item<'a>( + model: &'a crate::EpiphanyMemoryGraphSnapshot, + challenges: &[RepoModelClaimChallenge], +) -> Option<&'a crate::RepoFrontierItem> { let terminal = |status: crate::RepoFrontierStatus| { matches!( status, @@ -2844,6 +3022,7 @@ fn actionable_hands_frontier_item( && item.recommended_next_organ == "Hands" && !item.source_scope.is_empty() && safe_sorted_unique_paths(&item.source_scope) + && frontier_target_claims_unchallenged(item, challenges) && item.dependency_item_ids.iter().all(|dependency_id| { model .frontier @@ -2879,7 +3058,8 @@ pub fn runtime_has_actionable_hands_frontier(runtime_store: impl AsRef) -> && receipt.admitted_hash == model_hash }) .count(); - Ok(admission_count == 1 && actionable_hands_frontier_item(&model).is_some()) + let challenges = current_repo_model_claim_challenges(&cache, &model, &model_hash)?; + Ok(admission_count == 1 && actionable_hands_frontier_item(&model, &challenges).is_some()) } pub fn put_runtime_reorient_worker_result( @@ -6711,16 +6891,185 @@ mod tests { item("b-ready", vec![]), ]; assert_eq!( - actionable_imagination_frontier_item(&model).unwrap().id, + actionable_imagination_frontier_item(&model, &[]) + .unwrap() + .id, "b-ready" ); model.frontier[2].status = crate::RepoFrontierStatus::Resolved; assert_eq!( - actionable_imagination_frontier_item(&model).unwrap().id, + actionable_imagination_frontier_item(&model, &[]) + .unwrap() + .id, "a-blocked" ); } + fn claim_challenge_fixture( + root: &Path, + suffix: &str, + next_organ: &str, + ) -> Result<(PathBuf, RepoModelClaimChallenge)> { + let (store, result, review) = proposal_admission_fixture(root, suffix)?; + let mut result = result; + if next_organ != "Hands" { + let mut patch: crate::RepoModelPatch = + rmp_serde::from_slice(result.repo_model_patch_msgpack.as_deref().unwrap())?; + let crate::RepoModelPatchOperation::UpsertFrontier { item } = &mut patch.operations[0] + else { + unreachable!() + }; + item.recommended_next_organ = next_organ.into(); + let bytes = rmp_serde::to_vec_named(&patch)?; + result.repo_model_patch_msgpack = Some(bytes.clone()); + let mut amended = review; + amended.patch_sha256 = format!("{:x}", Sha256::digest(&bytes)); + put_runtime_role_worker_result(&store, &result)?; + commit_repo_model_admission(&store, &result.result_id, &amended)?; + } else { + put_runtime_role_worker_result(&store, &result)?; + commit_repo_model_admission(&store, &result.result_id, &review)?; + } + let model = runtime_current_repo_model(&store)?.unwrap(); + let model_hash = crate::memory_graph_model_hash(&model)?; + let mut cache = runtime_spine_cache(&store)?; + cache.pull_all_backing_stores()?; + let admission = cache + .get_all::()? + .into_iter() + .find(|receipt| { + receipt.admitted_revision == model.model_revision + && receipt.admitted_hash == model_hash + }) + .unwrap(); + let packet = EyesEvidencePacket { + schema_version: EYES_EVIDENCE_PACKET_SCHEMA_VERSION.into(), + packet_id: format!("eyes-packet-{suffix}"), + source_result_id: format!("eyes-result-{suffix}"), + source_job_id: format!("eyes-job-{suffix}"), + source_role_id: "research".into(), + evidence_ids: vec![format!("evidence-{suffix}")], + observation_ids: vec![format!("observation-{suffix}")], + source_refs: vec!["epiphany-core/src/runtime_spine.rs:1".into()], + summary: "The exact claim is challenged by inspected source.".into(), + uncertainty: "Bounded to the cited source.".into(), + emitted_at: "2026-07-13T04:01:00Z".into(), + contract: "Eyes packet emitted from a reviewed Research lane finding; it makes the source-gathering evidence claim citable before Mind admission.".into(), + }; + put_eyes_evidence_packet(&store, &packet)?; + let claim = model + .nodes + .iter() + .find(|node| node.id == "claim-runtime-model") + .unwrap(); + let challenge = RepoModelClaimChallenge { + schema_version: REPO_MODEL_CLAIM_CHALLENGE_SCHEMA_VERSION.into(), + challenge_id: format!("claim-challenge-{suffix}"), + eyes_evidence_packet_id: packet.packet_id.clone(), + eyes_evidence_packet_sha256: format!( + "{:x}", + Sha256::digest(rmp_serde::to_vec_named(&packet)?) + ), + source_result_id: packet.source_result_id, + source_job_id: packet.source_job_id, + model_revision: model.model_revision, + model_hash, + admission_receipt_id: admission.receipt_id, + target_claim_id: claim.id.clone(), + target_claim_sha256: format!("{:x}", Sha256::digest(rmp_serde::to_vec_named(claim)?)), + disposition: crate::RepoModelClaimChallengeDisposition::Contradicted, + finding: "The exact admitted claim is contradicted.".into(), + uncertainty: packet.uncertainty, + source_refs: packet.source_refs, + evidence_ids: packet.evidence_ids, + challenged_at: "2026-07-13T04:01:01Z".into(), + contract: REPO_MODEL_CLAIM_CHALLENGE_CONTRACT.into(), + }; + Ok((store, challenge)) + } + + #[test] + fn claim_challenge_is_exact_immutable_and_rejects_substitution() -> Result<()> { + let root = tempdir()?; + let (store, challenge) = claim_challenge_fixture(root.path(), "exact", "Hands")?; + commit_repo_model_claim_challenge(&store, &challenge)?; + commit_repo_model_claim_challenge(&store, &challenge)?; + let mut collision = challenge.clone(); + collision.finding = "substituted finding".into(); + assert!(commit_repo_model_claim_challenge(&store, &collision).is_err()); + + for (suffix, mutate) in [ + "packet-id", + "packet-hash", + "provenance", + "admission", + "claim", + "claim-hash", + "revision", + ] + .into_iter() + .enumerate() + { + let (hostile_store, mut hostile) = + claim_challenge_fixture(root.path(), &format!("hostile-{suffix}"), "Hands")?; + match mutate { + "packet-id" => hostile.eyes_evidence_packet_id = "swapped-packet".into(), + "packet-hash" => hostile.eyes_evidence_packet_sha256 = "00".repeat(32), + "provenance" => hostile.source_job_id = "swapped-job".into(), + "admission" => hostile.admission_receipt_id = "swapped-admission".into(), + "claim" => hostile.target_claim_id = "swapped-claim".into(), + "claim-hash" => hostile.target_claim_sha256 = "11".repeat(32), + "revision" => hostile.model_revision += 1, + _ => unreachable!(), + } + let before = std::fs::read(&hostile_store)?; + assert!(commit_repo_model_claim_challenge(&hostile_store, &hostile).is_err()); + assert_eq!(std::fs::read(&hostile_store)?, before); + } + Ok(()) + } + + #[test] + fn current_claim_challenge_withholds_planning_and_hands_but_not_unrelated_frontier() + -> Result<()> { + let root = tempdir()?; + let (hands_store, hands_challenge) = + claim_challenge_fixture(root.path(), "hands-gate", "Hands")?; + commit_repo_model_claim_challenge(&hands_store, &hands_challenge)?; + assert!(!runtime_has_actionable_hands_frontier(&hands_store)?); + assert!( + select_and_commit_repo_frontier_route(&hands_store, "2026-07-13T04:02:00Z").is_err() + ); + + let (planning_store, planning_challenge) = + claim_challenge_fixture(root.path(), "planning-gate", "Imagination")?; + commit_repo_model_claim_challenge(&planning_store, &planning_challenge)?; + assert!( + select_and_commit_repo_frontier_planning_request( + &planning_store, + "2026-07-13T04:02:00Z" + ) + .is_err() + ); + + let mut unrelated = repo_model_bootstrap(); + unrelated.frontier = vec![crate::RepoFrontierItem { + id: "unrelated".into(), + target_claim_ids: vec!["another-claim".into()], + source_scope: vec!["epiphany-core/src".into()], + recommended_next_organ: "Hands".into(), + status: crate::RepoFrontierStatus::Active, + ..Default::default() + }]; + assert_eq!( + actionable_hands_frontier_item(&unrelated, &[hands_challenge]) + .unwrap() + .id, + "unrelated" + ); + Ok(()) + } + fn admit_route_and_authorize_hands( store: &Path, intent: &HandsActionIntent, diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 27bf9176..64e0b644 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -27,6 +27,18 @@ roadmap mythology, and Self preserved dependency order without flattening those different judgments into one running narrative. This is positive evidence for durable organ-to-organ cognition, not merely for role-flavored prompting. +That lesson now has a concrete corrective nerve. Eyes can persist an immutable +`RepoModelClaimChallenge` against one serialized claim at one exact admitted +model revision. Runtime-spine admission replays the canonical Eyes packet, +unique current Mind receipt, model identity, and claim hash in one absent-only +CAS. Self uses the same validated challenge predicate for Imagination planning, +Hands route selection, and actionable-Hands status; only frontier targeting the +challenged claim is withheld. Eyes never writes RepoModel, and no challenge ids +were stuffed into the graph. Core proof is 304 passed/1 ignored and independent +Soul review passes. Next build a typed Modeling challenge-repair request and +Mind admission path so evidence can reshape the shared map, then let Self and +Imagination revise the campaign from that repaired substrate. + Do not substitute the first linear frontier identity proof for that larger lesson. `Modeling -> Mind -> Self -> Hands -> Soul -> Modeling` proves that one wound can retain causal identity. The awakened anatomy also needs typed backward diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index fe4cc9ae..6261d99d 100644 Binary files a/state/ledgers.msgpack and b/state/ledgers.msgpack differ diff --git a/state/map.yaml b/state/map.yaml index 923505ad..fdea1d08 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -377,6 +377,21 @@ current_status: replay -> one proposal-citing frontier contribution. Verification: core 302 passed/1 ignored, OpenAI runtime 8 passed, hostile no-write proofs, and independent Soul PASS. + eyes_claim_challenge_authority: > + Eyes may persist one immutable challenge against one serialized RepoModel + claim at one exact admitted model revision. Runtime-spine admission replays + the canonical Eyes packet hash and provenance, the unique current Mind + admission receipt, the model revision/hash, and the target claim hash; the + model, packet, and admission envelopes participate in the absent-only CAS. + Eyes does not write RepoModel. Self derives pressure from fully revalidated + current challenges through one shared target-overlap predicate: challenged + frontier is withheld from Imagination planning, Hands routing, and + actionable-Hands status while unrelated frontier remains eligible. Prompt + prose, UI state, direct CultCache challenge rows, latest-challenge helpers, + challenge metadata inside RepoModel, and generic evidence routers are + forbidden authorities. Verification: core 304 passed/1 ignored, hostile + immutable/substitution/no-write proofs, identical planning/Hands gates, and + independent Soul PASS. latest_cut: > The bridge host seam has been cut down: target-specific Hands prompt scar tissue was purged, bridge manifests carry the retained Codex websocket fork @@ -678,4 +693,4 @@ current_status: - Service lifecycle receipt history is now event-only: `load_epiphany_cultmesh_daemon_service_lifecycle_receipts` filters out the `epiphany-local/daemon-service-lifecycle-receipt/latest` mirror, while `load_latest_epiphany_cultmesh_daemon_service_lifecycle_receipt` remains the latest-state reader. Focused unit test `service_lifecycle_receipt_history_excludes_latest_mirror` proves two per-receipt writes return exactly two history records and latest points at the second. Fresh smoke `verse-query-lifecycle-history-filter-smoke` and live overview/triage `local-20260618-145632-497-efac6f0d` / `local-20260618-145632-321-867947af` prove compact swarm sights still expose service lifecycle attention plus the present elevated runbook artifact without daemon pokes or service mutation. Do not use a broad unscoped cargo test filter for this check on Windows; it fans out across bin harnesses and hit a broken pipe during this pass. - `epiphany-verse-query seed-compact` is the compact seed receipt for wrappers and read-only rites. The older `seed` command still prints the full local Verse context for explicit debugging, but compact wrapper modes now avoid full `seed`/final `query` readback after the combined overview proof exposed Windows debug stack overflow from full-context serialization pressure. last_completed_repo_swarm_cut: Huginn live-fire has crossed the first real repo Body from Persona pressure to branch-local Hands commit and Soul/Mind closure, and now has repeatable closure and redacted-public-proof mouths. On `E:\Projects\Huginn` branch `epiphany/huginn/first-awakening`, `tools/epiphany_local_run.ps1 -Mode repo-persona-intake` accepted item `huginn-inspection-readback-livefire`, Weksa/Persona intake remained sealed, and `epiphany-work derive-plan` lowered it to model-authored safe family `repo.status_section` for `README.md` only. `tools/epiphany_local_run.ps1 -Mode repo-swarm-run` advanced four bounded pulses: run-from-plan, adopt-from-plan, execute-from-plan, and close-from-execute. Huginn commit `e9b07c38fceabbdc89ea35c2156326c80c0393a2` updated only `README.md`; closure receipt `.epiphany\work\work-close-huginn-inspection-readback-livefire.json` records status `closed`, Soul verdict `passed`, Mind state commit `repo-work-close-huginn-inspection-readback-livefire-mind-commit`, safe family `repo.status_section`, path scope `README.md`, Bifrost publication gate, and `privateStateExposed=false`. Verification also ran Huginn's TypeScript compiler through concrete Node path: `C:\Program Files\nodejs\node.exe E:\Projects\Huginn\node_modules\typescript\bin\tsc -p tsconfig.json` passed. Native verifier `epiphany-repo-livefire-closure-smoke --workspace E:\Projects\Huginn --item huginn-inspection-readback-livefire --expected-path README.md --expected-family repo.status_section --expected-gate awaiting-publication --expected-blocker bifrost-publication-missing` checks closure receipt, Mind map entry, overview gate/blocker, authority seals, and `git show` changed paths, returning `status=ok`, `soulVerdict=passed`, `publicationGate=Bifrost`, and `privateStateExposed=false`. Wrapper `tools/epiphany_local_run.ps1 -Mode repo-livefire-closure -Workspace E:\Projects\Huginn -RepoWorkItem huginn-inspection-readback-livefire -SkipBuild` exposes the same read-only proof through the local operator mouth; artifact `local-20260621-190156-803-2f85ba15` printed commit, path, gate, blocker, Soul verdict, Mind commit receipt, and `privateStateExposed=False` without push, merge, publication, or target mutation. New wrapper `tools/epiphany_local_run.ps1 -Mode repo-work-public-proof -Workspace E:\Projects\Huginn -RepoWorkItem huginn-inspection-readback-livefire -SkipBuild` exports the redacted proof bundle and repo-local Verse index without publishing; artifact `local-20260621-190645-345-4555b1dc` wrote `.epiphany\public\proof-bundles\repo-work-public-proof-huginn-inspection-readback-livefire.json`, proof id `repo-work-public-proof-huginn-inspection-readback-livefire`, SHA-256 `857545b199f3e37eb06c2e757e5cfc201958e46667bb1f8403fd333a22620f63`, nine artifact rows, zero publication rows, and `privateStateExposed=False`. swarm overview readback `local-20260621-190701-140-b9519ec6` sees the `REPO-WORK-PUBLIC-PROOF` row and action row from Huginn's repo-local Verse with the same SHA-256 while Bifrost publication remains missing. Launcher compact modes are now named compact operator-context modes instead of read-only modes, and `bifrost-public-proof` summaries print item, target public Verse, room, proof SHA-256, review receipts, and credit receipts so publication receipts are auditable without claiming upstream sync. - next_action: Extend the Aetheria-shaped correction ecology from the now-landed selected-proposal ingress. User proposal intake, exact coordinator selection, coordinator-owned typed Modeling launch projection, immutable job/request/proposal binding, and proposal-bound Mind admission are Soul-approved; core proof is 302 passed/1 ignored and OpenAI runtime proof is 8 passed. The proposal remains contestable evidence and creates only one admitted frontier contribution—never a plan, route, Hands grant, or lifecycle decision. Next add typed Eyes challenge artifacts that can invalidate or stale model claims and create evidence pressure without direct RepoModel write authority; Self must route that pressure from admitted artifacts. Then bind the existing Self planning request to a real Imagination worker result and Mind Adopt/Refuse/Hold path before inventing any execution route. Add the production semantic index over typed graph claims so organs retrieve the living migration map instead of rediscovering files. Do not infer identities from prose/evidence intersection, revive epiphany-work, add metadata adapters, or let a prompt projection become authority. The intentionally absent Other-world action still requires a typed intent and real owner. Keep closure's zero-substring invariant. + next_action: Continue the Aetheria-shaped correction ecology from the landed selected-proposal ingress and Eyes claim-challenge pressure. Eyes can now challenge one exact admitted claim without RepoModel write authority, and Self withholds only frontier targeting that challenged claim from planning and Hands. Next create the typed Modeling challenge-repair request/result/admission path so Modeling can revise the exact claim and affected frontier through Mind, after which the old challenge naturally loses current-revision pressure. Then bind the existing Self planning request to a real Imagination worker result and Mind Adopt/Refuse/Hold path before inventing any execution route. Add the production semantic index over typed graph claims so organs retrieve the living migration map instead of rediscovering files. Do not infer identities from prose/evidence intersection, revive epiphany-work, add metadata adapters, store challenge ids inside RepoModel, or let prompt projections become authority. The intentionally absent Other-world action still requires a typed intent and real owner. Keep closure's zero-substring invariant.