Seal immutable frontier planning results

This commit is contained in:
Meta Vens 2026-07-15 01:29:35 +01:00
commit 05300c8add
12 changed files with 645 additions and 14 deletions

View file

@ -412,6 +412,61 @@ pub fn epiphany_role_launch_output_schema(role_id: EpiphanyRoleResultRoleId) ->
schema
}
pub fn epiphany_frontier_planning_output_schema() -> serde_json::Value {
let mut schema = epiphany_role_launch_output_schema(EpiphanyRoleResultRoleId::Imagination);
let properties = schema["properties"]
.as_object_mut()
.expect("role output schema properties");
properties.remove("statePatch");
properties.remove("selfPatch");
properties.insert(
"frontierPlanningRequestId".to_string(),
serde_json::json!({
"type": "string",
"minLength": 1,
"description": "Exact echo of the coordinator-bound repo frontier planning request."
}),
);
properties.insert(
"frontierPlanCandidate".to_string(),
serde_json::json!({
"type": "object",
"required": [
"planning_request_id", "model_revision", "model_hash",
"frontier_item_id", "frontier_item_hash", "safe_paths", "action", "command",
"checks", "stop_conditions", "rollback_steps", "commit_message", "proposed_at"
],
"properties": {
"planning_request_id": {"type": "string", "minLength": 1},
"model_revision": {"type": "integer", "minimum": 0},
"model_hash": {"type": "string", "minLength": 1},
"frontier_item_id": {"type": "string", "minLength": 1},
"frontier_item_hash": {"type": "string", "minLength": 1},
"safe_paths": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
"action": {"type": "string", "minLength": 1},
"command": {"type": "string", "minLength": 1},
"checks": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
"stop_conditions": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
"rollback_steps": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
"commit_message": {"type": "string", "minLength": 1},
"proposed_at": {"type": "string", "minLength": 1}
},
"additionalProperties": false
}),
);
schema["required"] = serde_json::json!([
"roleId",
"verdict",
"summary",
"nextSafeMove",
"filesInspected",
"frontierPlanningRequestId",
"frontierPlanCandidate"
]);
schema["additionalProperties"] = serde_json::Value::Bool(false);
schema
}
pub fn epiphany_reorient_launch_output_schema() -> serde_json::Value {
serde_json::json!({
"type": "object",
@ -956,6 +1011,21 @@ mod tests {
);
}
#[test]
fn frontier_planning_schema_exposes_candidate_without_generic_patch_mouths() {
let schema = epiphany_frontier_planning_output_schema();
assert!(
schema["properties"]
.get("frontierPlanningRequestId")
.is_some()
);
assert!(schema["properties"].get("frontierPlanCandidate").is_some());
assert!(schema["properties"].get("statePatch").is_none());
assert!(schema["properties"].get("selfPatch").is_none());
assert!(schema["properties"].get("repoModelPatch").is_none());
assert_eq!(schema["additionalProperties"], false);
}
#[test]
fn reorient_worker_prompt_is_bounded_not_full_persistent_memory() {
let prompt = build_epiphany_reorient_launch_instruction(CoreEpiphanyReorientAction::Resume);

View file

@ -838,6 +838,62 @@ pub(crate) mod tests {
Ok((store, state, launch, planning))
}
fn frontier_planning_result(
planning: &RepoFrontierPlanningRequest,
job_id: &str,
proposed_at: &str,
) -> Result<EpiphanyRuntimeRoleWorkerResult> {
let mut candidate = RepoFrontierPlanCandidate {
schema_version: REPO_FRONTIER_PLAN_CANDIDATE_SCHEMA_VERSION.into(),
candidate_id: String::new(),
planning_request_id: planning.request_id.clone(),
model_revision: planning.model_revision,
model_hash: planning.model_hash.clone(),
frontier_item_id: planning.frontier_item_id.clone(),
frontier_item_hash: planning.frontier_item_hash.clone(),
safe_paths: vec![planning.source_scope[0].clone()],
action: "Implement the bounded frontier plan.".into(),
command: "cargo test --lib".into(),
checks: vec!["Focused tests pass.".into()],
stop_conditions: vec!["Authority scope changes.".into()],
rollback_steps: vec!["Revert the bounded commit.".into()],
commit_message: "Implement bounded frontier plan".into(),
proposed_at: proposed_at.into(),
contract: REPO_FRONTIER_PLANNING_CONTRACT.into(),
};
candidate.candidate_id = canonical_repo_frontier_plan_candidate_id(&candidate)?;
Ok(EpiphanyRuntimeRoleWorkerResult {
schema_version: RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION.into(),
result_id: format!("frontier-planning-result-{job_id}"),
job_id: job_id.into(),
role_id: "imagination".into(),
verdict: "draft-ready".into(),
summary: "Produced one bounded candidate.".into(),
next_safe_move: "Mind admission.".into(),
checkpoint_summary: None,
scratch_summary: None,
files_inspected: planning.source_scope.clone(),
frontier_node_ids: vec![planning.frontier_item_id.clone()],
evidence_ids: vec![planning.admission_receipt_id.clone()],
artifact_refs: Vec::new(),
open_questions: Vec::new(),
evidence_gaps: Vec::new(),
risks: Vec::new(),
state_patch_msgpack: None,
self_patch_msgpack: None,
item_error: None,
metadata: std::collections::BTreeMap::new(),
repo_model_patch_msgpack: None,
verification_request_id: None,
frontier_route_id: None,
repo_frontier_modeling_request_id: None,
proposal_modeling_request_id: None,
claim_repair_request_id: None,
frontier_planning_request_id: Some(planning.request_id.clone()),
frontier_plan_candidate_msgpack: Some(rmp_serde::to_vec_named(&candidate)?),
})
}
fn research_launch(state: &EpiphanyThreadState) -> EpiphanyJobLaunchRequest {
build_epiphany_role_launch_request(
"thread-1",
@ -1321,6 +1377,107 @@ pub(crate) mod tests {
Ok(())
}
#[test]
fn frontier_planning_result_is_exact_immutable_and_patch_sealed() -> Result<()> {
let root = tempfile::tempdir()?;
let (store, state, launch, planning) =
frontier_planning_launch_fixture(root.path(), "result-exact")?;
let plan = plan_coordinator_job_launch(
&state,
&launch,
&store,
"launcher-result".into(),
"backend-result".into(),
)?;
commit_coordinator_job_launch(
&store,
&planning.thread_id,
&state,
&launch,
&plan,
"2026-07-15T09:00:03Z".into(),
)?;
let result = frontier_planning_result(&planning, "backend-result", "2026-07-15T09:00:04Z")?;
put_runtime_role_worker_result(&store, &result)?;
put_runtime_role_worker_result(&store, &result)?;
let stored = runtime_role_worker_result(&store, "backend-result")?
.expect("immutable planning result");
assert_eq!(stored, result);
let candidate = stored
.frontier_plan_candidate()?
.expect("typed candidate payload");
assert_eq!(candidate.planning_request_id, planning.request_id);
assert_eq!(
candidate.candidate_id,
canonical_repo_frontier_plan_candidate_id(&candidate)?
);
assert!(
coordinator_acceptance_cache(&store)?
.get_all::<RepoFrontierPlanCandidate>()?
.is_empty(),
"candidate must remain inside immutable result until Mind admission"
);
let mut counterfeit = result;
counterfeit.summary = "different result bytes".into();
let before = std::fs::read(&store)?;
assert!(put_runtime_role_worker_result(&store, &counterfeit).is_err());
assert_eq!(std::fs::read(&store)?, before);
Ok(())
}
#[test]
fn frontier_planning_result_refuses_hostile_substitution_before_persistence() -> Result<()> {
let root = tempfile::tempdir()?;
for mutation in 0..9 {
let (store, state, launch, planning) =
frontier_planning_launch_fixture(root.path(), &format!("result-{mutation}"))?;
let job_id = format!("backend-result-{mutation}");
let plan = plan_coordinator_job_launch(
&state,
&launch,
&store,
format!("launcher-result-{mutation}"),
job_id.clone(),
)?;
commit_coordinator_job_launch(
&store,
&planning.thread_id,
&state,
&launch,
&plan,
"2026-07-15T09:00:03Z".into(),
)?;
let mut result = frontier_planning_result(&planning, &job_id, "2026-07-15T09:00:04Z")?;
match mutation {
0 => result.frontier_planning_request_id = Some("swapped-request".into()),
1 => result.frontier_planning_request_id = None,
2 => result.frontier_plan_candidate_msgpack = None,
3 => result.role_id = "modeling".into(),
4 => result.state_patch_msgpack = Some(vec![0]),
5 => result.self_patch_msgpack = Some(vec![0]),
6 => result.repo_model_patch_msgpack = Some(vec![0]),
_ => {
let mut candidate = result.frontier_plan_candidate()?.unwrap();
if mutation == 7 {
candidate.frontier_item_hash = "adjacent-frontier".into();
} else {
candidate.safe_paths = vec!["outside/authority".into()];
}
result.frontier_plan_candidate_msgpack =
Some(rmp_serde::to_vec_named(&candidate)?);
}
}
let before = std::fs::read(&store)?;
assert!(
put_runtime_role_worker_result(&store, &result).is_err(),
"hostile planning result mutation {mutation} must fail"
);
assert_eq!(std::fs::read(&store)?, before);
assert!(runtime_role_worker_result(&store, &job_id)?.is_none());
}
Ok(())
}
#[test]
fn concurrent_frontier_planning_launches_leave_only_winner_artifacts() -> Result<()> {
let root = tempfile::tempdir()?;

View file

@ -938,6 +938,8 @@ mod tests {
repo_frontier_modeling_request_id: None,
proposal_modeling_request_id: Some(selection.request_id),
claim_repair_request_id: None,
frontier_planning_request_id: None,
frontier_plan_candidate_msgpack: None,
};
crate::put_runtime_role_worker_result(store, &result)?;
crate::commit_repo_model_admission(
@ -1419,6 +1421,8 @@ mod tests {
repo_frontier_modeling_request_id: None,
proposal_modeling_request_id: None,
claim_repair_request_id: None,
frontier_planning_request_id: None,
frontier_plan_candidate_msgpack: None,
};
crate::put_runtime_role_worker_result(&runtime_store, &verification_result)?;
crate::put_soul_verdict_receipt(

View file

@ -59,6 +59,7 @@ pub use agent_launch::build_epiphany_reorient_launch_request_with_dynamic_contex
pub use agent_launch::build_epiphany_role_launch_request;
pub use agent_launch::build_epiphany_role_launch_request_with_dynamic_context;
pub use agent_launch::epiphany_agent_prompt_with_memory;
pub use agent_launch::epiphany_frontier_planning_output_schema;
pub use agent_launch::epiphany_reorient_launch_output_schema;
pub use agent_launch::epiphany_role_binding_id;
pub use agent_launch::epiphany_role_label;
@ -776,6 +777,7 @@ pub use runtime_spine::RuntimeSpineJobOptions;
pub use runtime_spine::RuntimeSpineJobResultOptions;
pub use runtime_spine::RuntimeSpineSessionOptions;
pub use runtime_spine::append_runtime_event;
pub use runtime_spine::canonical_repo_frontier_plan_candidate_id;
pub use runtime_spine::clear_epiphany_job_binding_backend;
pub use runtime_spine::commit_repo_frontier_modeling_request;
pub use runtime_spine::commit_repo_frontier_verification_request_for_chain;

View file

@ -48,7 +48,9 @@ use crate::repo_model_gateway::{
REPO_FRONTIER_HANDS_AUTHORITY_CONTRACT, REPO_FRONTIER_HANDS_AUTHORITY_SCHEMA_VERSION,
REPO_FRONTIER_MODELING_REQUEST_CONTRACT, REPO_FRONTIER_MODELING_REQUEST_SCHEMA_VERSION,
REPO_FRONTIER_PLAN_ADOPTION_SCHEMA_VERSION, REPO_FRONTIER_PLAN_CANDIDATE_SCHEMA_VERSION,
REPO_FRONTIER_PLANNING_CONTRACT, REPO_FRONTIER_PLANNING_REQUEST_SCHEMA_VERSION,
REPO_FRONTIER_PLANNING_CONTRACT, REPO_FRONTIER_PLANNING_LAUNCH_BINDING_CONTRACT,
REPO_FRONTIER_PLANNING_LAUNCH_BINDING_SCHEMA_VERSION,
REPO_FRONTIER_PLANNING_REQUEST_SCHEMA_VERSION,
REPO_FRONTIER_PROPOSAL_MODELING_LAUNCH_BINDING_CONTRACT,
REPO_FRONTIER_PROPOSAL_MODELING_LAUNCH_BINDING_SCHEMA_VERSION,
REPO_FRONTIER_PROPOSAL_MODELING_REQUEST_CONTRACT,
@ -174,7 +176,7 @@ pub const RUNTIME_SPINE_SCHEMA_VERSION: &str = "epiphany.runtime_spine.v0";
pub const RUNTIME_WORKER_LAUNCH_REQUEST_SCHEMA_VERSION: &str =
"epiphany.runtime.worker_launch_request.v1";
pub const RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION: &str =
"epiphany.runtime.role_worker_result.v1";
"epiphany.runtime.role_worker_result.v2";
pub const RUNTIME_REORIENT_WORKER_RESULT_SCHEMA_VERSION: &str =
"epiphany.runtime.reorient_worker_result.v0";
pub const COORDINATOR_RUN_RECEIPT_SCHEMA_VERSION: &str = "epiphany.coordinator_run_receipt.v0";
@ -410,6 +412,10 @@ pub struct EpiphanyRuntimeRoleWorkerResult {
pub proposal_modeling_request_id: Option<String>,
#[cultcache(key = 25, default)]
pub claim_repair_request_id: Option<String>,
#[cultcache(key = 26, default)]
pub frontier_planning_request_id: Option<String>,
#[cultcache(key = 27, default)]
pub frontier_plan_candidate_msgpack: Option<Vec<u8>>,
}
impl EpiphanyRuntimeRoleWorkerResult {
@ -430,6 +436,13 @@ impl EpiphanyRuntimeRoleWorkerResult {
"role worker repoModelPatch",
)
}
pub fn frontier_plan_candidate(&self) -> Result<Option<RepoFrontierPlanCandidate>> {
decode_optional_msgpack(
self.frontier_plan_candidate_msgpack.as_deref(),
"role worker frontierPlanCandidate",
)
}
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
@ -1457,6 +1470,11 @@ pub fn put_runtime_role_worker_result(
validate_non_empty(&result.job_id, "role worker result job id")?;
validate_non_empty(&result.result_id, "role worker result id")?;
validate_non_empty(&result.role_id, "role worker result role id")?;
if result.schema_version != RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION {
return Err(anyhow!("role worker result schema version mismatch"));
}
let mut cache = runtime_spine_cache(store_path)?;
cache.pull_all_backing_stores()?;
let is_verification = result.role_id == "verification";
if is_verification
!= (result
@ -1553,8 +1571,94 @@ pub fn put_runtime_role_worker_result(
_ => {}
}
}
let mut cache = runtime_spine_cache(store_path)?;
cache.pull_all_backing_stores()?;
let has_planning_echo = result.frontier_planning_request_id.is_some();
let has_planning_candidate = result.frontier_plan_candidate_msgpack.is_some();
if has_planning_echo != has_planning_candidate {
return Err(anyhow!(
"frontier planning result requires both its request echo and typed candidate"
));
}
if has_planning_echo {
if !result.role_id.eq_ignore_ascii_case("imagination") {
return Err(anyhow!(
"only Imagination results may echo a frontier planning request"
));
}
if result.state_patch_msgpack.is_some()
|| result.self_patch_msgpack.is_some()
|| result.repo_model_patch_msgpack.is_some()
|| result.verification_request_id.is_some()
|| result.frontier_route_id.is_some()
|| result.repo_frontier_modeling_request_id.is_some()
|| result.proposal_modeling_request_id.is_some()
|| result.claim_repair_request_id.is_some()
{
return Err(anyhow!(
"frontier planning result may carry only its request echo and typed candidate authority"
));
}
let request_id = result
.frontier_planning_request_id
.as_deref()
.ok_or_else(|| anyhow!("frontier planning request echo disappeared"))?;
let request = cache
.get::<RepoFrontierPlanningRequest>(request_id)?
.ok_or_else(|| anyhow!("frontier planning result requires persisted request"))?;
validate_current_repo_frontier_planning_request(&cache, &request)?;
let candidate = result
.frontier_plan_candidate()?
.ok_or_else(|| anyhow!("frontier planning candidate disappeared"))?;
validate_repo_frontier_plan_candidate_against_request(&cache, &candidate, &request)?;
let bindings = cache
.get_all::<RepoFrontierPlanningLaunchBinding>()?
.into_iter()
.filter(|binding| binding.planning_request_id == request.request_id)
.collect::<Vec<_>>();
if bindings.len() != 1 {
return Err(anyhow!(
"frontier planning result requires exactly one coordinator launch binding"
));
}
let binding = &bindings[0];
let worker_launch = cache
.get::<EpiphanyRuntimeWorkerLaunchRequest>(&result.job_id)?
.ok_or_else(|| anyhow!("frontier planning result requires its worker launch"))?;
let launch_document = worker_launch.launch_document()?;
let projection = match &launch_document {
EpiphanyWorkerLaunchDocument::Role(document) => {
document.frontier_planning_context.as_ref()
}
EpiphanyWorkerLaunchDocument::Reorient(_) => None,
};
let expected_projection =
crate::RepoFrontierPlanningContextProjection::from_request(&request);
let launch_hash = format!(
"{:x}",
Sha256::digest(&worker_launch.launch_document_msgpack)
);
if binding.schema_version != REPO_FRONTIER_PLANNING_LAUNCH_BINDING_SCHEMA_VERSION
|| binding.contract != REPO_FRONTIER_PLANNING_LAUNCH_BINDING_CONTRACT
|| binding.binding_record_id
!= format!("repo-frontier-planning-launch-{}", request.request_id)
|| binding.job_id != result.job_id
|| binding.binding_id != EPIPHANY_IMAGINATION_ROLE_BINDING_ID
|| binding.runtime_id != request.runtime_id
|| binding.thread_id != request.thread_id
|| binding.worker_launch_document_sha256 != launch_hash
|| worker_launch.job_id != result.job_id
|| worker_launch.role != EPIPHANY_IMAGINATION_OWNER_ROLE
|| worker_launch.binding_id != EPIPHANY_IMAGINATION_ROLE_BINDING_ID
|| worker_launch.frontier_planning_request_id.as_deref()
!= Some(request.request_id.as_str())
|| worker_launch.proposal_modeling_request_id.is_some()
|| worker_launch.claim_repair_request_id.is_some()
|| projection != Some(&expected_projection)
{
return Err(anyhow!(
"frontier planning result does not exactly bind request, launch, runtime, thread, and candidate"
));
}
}
if let Some(existing) = cache.get::<EpiphanyRuntimeRoleWorkerResult>(&result.job_id)? {
return if existing == *result {
Ok(())
@ -3540,6 +3644,94 @@ pub(crate) fn put_repo_frontier_plan_candidate(
put_immutable_planning_entry(store_path, &candidate.candidate_id, candidate)
}
fn validate_repo_frontier_plan_candidate_against_request(
cache: &CultCache,
candidate: &RepoFrontierPlanCandidate,
request: &RepoFrontierPlanningRequest,
) -> Result<()> {
if candidate.schema_version != REPO_FRONTIER_PLAN_CANDIDATE_SCHEMA_VERSION
|| candidate.contract != REPO_FRONTIER_PLANNING_CONTRACT
|| candidate.selected_fields_invalid()
|| candidate.planning_request_id != request.request_id
|| candidate.model_revision != request.model_revision
|| candidate.model_hash != request.model_hash
|| candidate.frontier_item_id != request.frontier_item_id
|| candidate.frontier_item_hash != request.frontier_item_hash
{
return Err(anyhow!(
"frontier planning candidate substituted request identity or required cargo"
));
}
let expected_candidate_id = canonical_repo_frontier_plan_candidate_id(candidate)?;
if candidate.candidate_id != expected_candidate_id {
return Err(anyhow!("frontier planning candidate id is not canonical"));
}
let admission = cache
.get::<RepoModelAdmissionReceipt>(&request.admission_receipt_id)?
.ok_or_else(|| anyhow!("frontier planning candidate requires exact admission receipt"))?;
if admission.schema_version != REPO_MODEL_ADMISSION_RECEIPT_SCHEMA_VERSION
|| admission.contract != REPO_MODEL_ADMISSION_CONTRACT
|| admission.admitted_revision != request.model_revision
|| admission.admitted_hash != request.model_hash
{
return Err(anyhow!(
"frontier planning candidate admission binding mismatch"
));
}
let entry = cache
.get::<crate::EpiphanyMemoryGraphEntry>(crate::MEMORY_GRAPH_KEY)?
.ok_or_else(|| anyhow!("frontier planning candidate requires canonical model"))?;
let model = entry.snapshot()?;
if model.model_revision != request.model_revision
|| crate::memory_graph_model_hash(&model)? != request.model_hash
{
return Err(anyhow!("frontier planning candidate model is stale"));
}
let item = model
.frontier
.iter()
.find(|item| item.id == request.frontier_item_id)
.ok_or_else(|| anyhow!("frontier planning candidate frontier disappeared"))?;
if format!("{:x}", Sha256::digest(rmp_serde::to_vec_named(item)?)) != request.frontier_item_hash
|| item.source_scope != request.source_scope
|| !candidate.safe_paths.iter().all(|path| {
request.source_scope.iter().any(|scope| {
path == scope
|| path.starts_with(&format!("{}/", scope.trim_end_matches(['/', '\\'])))
})
})
{
return Err(anyhow!(
"frontier planning candidate exceeds exact frontier authority"
));
}
Ok(())
}
pub fn canonical_repo_frontier_plan_candidate_id(
candidate: &RepoFrontierPlanCandidate,
) -> Result<String> {
let semantic_bytes = rmp_serde::to_vec_named(&(
&candidate.planning_request_id,
candidate.model_revision,
&candidate.model_hash,
&candidate.frontier_item_id,
&candidate.frontier_item_hash,
&candidate.safe_paths,
&candidate.action,
&candidate.command,
&candidate.checks,
&candidate.stop_conditions,
&candidate.rollback_steps,
&candidate.commit_message,
&candidate.proposed_at,
))?;
Ok(format!(
"repo-frontier-plan-candidate-{:x}",
Sha256::digest(semantic_bytes)
))
}
impl RepoFrontierPlanCandidate {
fn selected_fields_invalid(&self) -> bool {
self.candidate_id.trim().is_empty()
@ -6980,6 +7172,8 @@ pub(crate) mod tests {
repo_frontier_modeling_request_id: None,
proposal_modeling_request_id: Some(selection.request_id),
claim_repair_request_id: None,
frontier_planning_request_id: None,
frontier_plan_candidate_msgpack: None,
};
let review = RepoModelAdmissionReview {
schema_version: REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION.to_string(),
@ -7109,6 +7303,8 @@ pub(crate) mod tests {
repo_frontier_modeling_request_id: None,
proposal_modeling_request_id: None,
claim_repair_request_id: Some(repair.request_id.clone()),
frontier_planning_request_id: None,
frontier_plan_candidate_msgpack: None,
};
let review = RepoModelAdmissionReview {
schema_version: REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION.into(),
@ -8382,6 +8578,8 @@ pub(crate) mod tests {
repo_frontier_modeling_request_id: None,
proposal_modeling_request_id: None,
claim_repair_request_id: None,
frontier_planning_request_id: None,
frontier_plan_candidate_msgpack: None,
};
put_runtime_role_worker_result(&store, &verification_result)?;
let verdict = SoulVerdictReceipt {
@ -8501,6 +8699,8 @@ pub(crate) mod tests {
repo_frontier_modeling_request_id: Some(fixture.modeling_request.request_id.clone()),
proposal_modeling_request_id: None,
claim_repair_request_id: None,
frontier_planning_request_id: None,
frontier_plan_candidate_msgpack: None,
};
let review = RepoModelAdmissionReview {
schema_version: REPO_MODEL_ADMISSION_REVIEW_SCHEMA_VERSION.to_string(),
@ -8777,6 +8977,8 @@ pub(crate) mod tests {
repo_frontier_modeling_request_id: None,
proposal_modeling_request_id: None,
claim_repair_request_id: None,
frontier_planning_request_id: None,
frontier_plan_candidate_msgpack: None,
};
put_runtime_role_worker_result(&fixture.store, &adjacent)?;
put_soul_verdict_receipt(

View file

@ -24,7 +24,7 @@ use crate::EpiphanyReorientFreshnessStatus;
use crate::EpiphanyReorientPressureLevel;
use crate::EpiphanyReorientReason;
pub const ROLE_WORKER_OUTPUT_CONTRACT_ID: &str = "epiphany.worker.role_result.v1";
pub const ROLE_WORKER_OUTPUT_CONTRACT_ID: &str = "epiphany.worker.role_result.v2";
pub const REORIENT_WORKER_OUTPUT_CONTRACT_ID: &str = "epiphany.worker.reorient_result.v0";
pub const REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_SCHEMA_VERSION: &str =
"epiphany.worker.repo_frontier_proposal_modeling_context.v0";

View file

@ -971,6 +971,9 @@ mod tests {
state_revision: 1,
objective: Some("Verify the worker loop.".to_string()),
dynamic_prompt_context: None,
proposal_modeling_context: None,
claim_repair_context: None,
frontier_planning_context: None,
active_subgoal_id: None,
active_subgoals: Vec::new(),
active_graph_node_ids: Vec::new(),
@ -992,6 +995,9 @@ mod tests {
"role",
epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID,
),
proposal_modeling_request_id: None,
claim_repair_request_id: None,
frontier_planning_request_id: None,
created_at: now(),
},
)?;
@ -1051,6 +1057,9 @@ mod tests {
state_revision: 1,
objective: Some("Verify runtime error sealing.".to_string()),
dynamic_prompt_context: None,
proposal_modeling_context: None,
claim_repair_context: None,
frontier_planning_context: None,
active_subgoal_id: None,
active_subgoals: Vec::new(),
active_graph_node_ids: Vec::new(),
@ -1072,6 +1081,9 @@ mod tests {
"role",
epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID,
),
proposal_modeling_request_id: None,
claim_repair_request_id: None,
frontier_planning_request_id: None,
created_at: now(),
},
)?;
@ -1119,6 +1131,9 @@ mod tests {
state_revision: 1,
objective: Some("Verify the worker loop ceiling.".to_string()),
dynamic_prompt_context: None,
proposal_modeling_context: None,
claim_repair_context: None,
frontier_planning_context: None,
active_subgoal_id: None,
active_subgoals: Vec::new(),
active_graph_node_ids: Vec::new(),
@ -1140,6 +1155,9 @@ mod tests {
"role",
epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID,
),
proposal_modeling_request_id: None,
claim_repair_request_id: None,
frontier_planning_request_id: None,
created_at: now(),
},
)?;

View file

@ -1298,7 +1298,11 @@ fn worker_output_schema_json(document: &EpiphanyWorkerLaunchDocument) -> Result<
EpiphanyWorkerLaunchDocument::Role(document) => {
let role_id = role_result_id_for_launch_role(&document.role_id)
.with_context(|| format!("unknown role launch id {:?}", document.role_id))?;
epiphany_core::epiphany_role_launch_output_schema(role_id)
if document.frontier_planning_context.is_some() {
epiphany_core::epiphany_frontier_planning_output_schema()
} else {
epiphany_core::epiphany_role_launch_output_schema(role_id)
}
}
EpiphanyWorkerLaunchDocument::Reorient(_) => {
epiphany_core::epiphany_reorient_launch_output_schema()
@ -1323,8 +1327,13 @@ fn role_result_id_for_launch_role(
fn worker_output_contract_text(document: &EpiphanyWorkerLaunchDocument) -> &'static str {
match document {
EpiphanyWorkerLaunchDocument::Role(document)
if document.frontier_planning_context.is_some() =>
{
"Required frontier-planning result fields: roleId, verdict, summary, nextSafeMove, filesInspected, frontierPlanningRequestId, frontierPlanCandidate. Echo the exact request and candidate identity from the typed launch context. Do not emit statePatch, selfPatch, or repoModelPatch."
}
EpiphanyWorkerLaunchDocument::Role(_) => {
"Required role-result fields: roleId, verdict, summary, nextSafeMove, filesInspected. Modeling workers must include repoModelPatch; Imagination workers must include statePatch. Modeling statePatch is optional observations/evidence only. Use arrays for frontierNodeIds, evidenceIds, openQuestions, evidenceGaps, risks, and artifactRefs when present."
"Required role-result fields: roleId, verdict, summary, nextSafeMove, filesInspected. Modeling workers must include repoModelPatch; ordinary Imagination workers must include statePatch. Modeling statePatch is optional observations/evidence only. Use arrays for frontierNodeIds, evidenceIds, openQuestions, evidenceGaps, risks, and artifactRefs when present."
}
EpiphanyWorkerLaunchDocument::Reorient(_) => {
"Required reorient-result fields: mode, summary, nextSafeMove. Include checkpointStillValid, filesInspected, frontierNodeIds, evidenceIds, openQuestions, and continuityRisks when present."
@ -1355,6 +1364,27 @@ struct RoleWorkerResultIngress {
frontier_route_id: Option<String>,
repo_frontier_modeling_request_id: Option<String>,
proposal_modeling_request_id: Option<String>,
claim_repair_request_id: Option<String>,
frontier_planning_request_id: Option<String>,
frontier_plan_candidate: Option<RepoFrontierPlanCandidateIngress>,
}
#[derive(Debug, Clone, Default, serde::Deserialize)]
#[serde(default)]
struct RepoFrontierPlanCandidateIngress {
planning_request_id: String,
model_revision: u64,
model_hash: String,
frontier_item_id: String,
frontier_item_hash: String,
safe_paths: Vec<String>,
action: String,
command: String,
checks: Vec<String>,
stop_conditions: Vec<String>,
rollback_steps: Vec<String>,
commit_message: String,
proposed_at: String,
}
#[derive(Debug, Clone, Default, serde::Deserialize)]
@ -1450,6 +1480,37 @@ fn role_worker_result_from_ingress(
encode_optional_document(&result.repo_model_patch, "repoModelPatch");
let (self_patch_msgpack, self_patch_error) =
encode_optional_document(&result.self_patch, "selfPatch");
let (frontier_plan_candidate_msgpack, frontier_plan_candidate_error) = if let Some(ingress) =
result.frontier_plan_candidate.as_ref()
{
let mut candidate = epiphany_core::RepoFrontierPlanCandidate {
schema_version: epiphany_core::REPO_FRONTIER_PLAN_CANDIDATE_SCHEMA_VERSION.to_string(),
candidate_id: String::new(),
planning_request_id: ingress.planning_request_id.clone(),
model_revision: ingress.model_revision,
model_hash: ingress.model_hash.clone(),
frontier_item_id: ingress.frontier_item_id.clone(),
frontier_item_hash: ingress.frontier_item_hash.clone(),
safe_paths: clean_string_vec(&ingress.safe_paths),
action: ingress.action.trim().to_string(),
command: ingress.command.trim().to_string(),
checks: clean_string_vec(&ingress.checks),
stop_conditions: clean_string_vec(&ingress.stop_conditions),
rollback_steps: clean_string_vec(&ingress.rollback_steps),
commit_message: ingress.commit_message.trim().to_string(),
proposed_at: ingress.proposed_at.trim().to_string(),
contract: epiphany_core::REPO_FRONTIER_PLANNING_CONTRACT.to_string(),
};
match epiphany_core::canonical_repo_frontier_plan_candidate_id(&candidate) {
Ok(candidate_id) => {
candidate.candidate_id = candidate_id;
encode_optional_document(&Some(candidate), "frontierPlanCandidate")
}
Err(error) => (None, Some(format!("frontierPlanCandidate: {error}"))),
}
} else {
(None, None)
};
EpiphanyRuntimeRoleWorkerResult {
schema_version: epiphany_core::RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION.to_string(),
result_id: result_id.to_string(),
@ -1475,8 +1536,11 @@ fn role_worker_result_from_ingress(
state_patch_msgpack,
self_patch_msgpack,
item_error: merge_optional_errors(
merge_optional_errors(state_patch_error, self_patch_error),
repo_model_patch_error,
merge_optional_errors(
merge_optional_errors(state_patch_error, self_patch_error),
repo_model_patch_error,
),
frontier_plan_candidate_error,
),
metadata: std::collections::BTreeMap::new(),
repo_model_patch_msgpack,
@ -1488,6 +1552,11 @@ fn role_worker_result_from_ingress(
proposal_modeling_request_id: clean_optional_string(
result.proposal_modeling_request_id.as_deref(),
),
claim_repair_request_id: clean_optional_string(result.claim_repair_request_id.as_deref()),
frontier_planning_request_id: clean_optional_string(
result.frontier_planning_request_id.as_deref(),
),
frontier_plan_candidate_msgpack,
}
}
@ -1626,6 +1695,8 @@ mod tests {
epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID,
),
proposal_modeling_request_id: None,
claim_repair_request_id: None,
frontier_planning_request_id: None,
};
let result = role_worker_result_from_ingress(
&launch,
@ -1645,6 +1716,80 @@ mod tests {
Ok(())
}
#[test]
fn frontier_planning_ingress_derives_typed_candidate_identity() -> Result<()> {
let parsed = parse_assistant_json::<RoleWorkerResultIngress>(
r#"{
"roleId":"imagination",
"verdict":"draft-ready",
"summary":"bounded plan",
"nextSafeMove":"Mind admission",
"frontierPlanningRequestId":"planning-request-1",
"frontierPlanCandidate":{
"planning_request_id":"planning-request-1",
"model_revision":7,
"model_hash":"model-hash",
"frontier_item_id":"frontier-1",
"frontier_item_hash":"frontier-hash",
"safe_paths":["src"],
"action":"Implement the bounded cut.",
"command":"cargo test --lib",
"checks":["focused test passes"],
"stop_conditions":["scope changes"],
"rollback_steps":["revert commit"],
"commit_message":"Implement bounded cut",
"proposed_at":"2026-07-15T10:00:00Z"
}
}"#,
)?;
let launch = EpiphanyRuntimeWorkerLaunchRequest {
schema_version: epiphany_core::RUNTIME_WORKER_LAUNCH_REQUEST_SCHEMA_VERSION.to_string(),
job_id: "planning-job-1".into(),
binding_id: epiphany_core::EPIPHANY_IMAGINATION_ROLE_BINDING_ID.into(),
role: epiphany_core::EPIPHANY_IMAGINATION_OWNER_ROLE.into(),
authority_scope: "epiphany.role.imagination".into(),
instruction: "plan".into(),
output_contract_id: epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID.into(),
document_kind: "role".into(),
launch_document_msgpack: Vec::new(),
metadata: std::collections::BTreeMap::new(),
organ_launch_contract: epiphany_core::default_launch_organ_contract(
"epiphany.role.imagination",
"role",
epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID,
),
proposal_modeling_request_id: None,
claim_repair_request_id: None,
frontier_planning_request_id: Some("planning-request-1".into()),
};
let result = role_worker_result_from_ingress(
&launch,
"imagination",
"planning-result-1",
&parsed,
Vec::new(),
);
assert_eq!(
result.frontier_planning_request_id.as_deref(),
Some("planning-request-1")
);
assert!(result.state_patch_msgpack.is_none());
assert!(result.self_patch_msgpack.is_none());
let candidate = result
.frontier_plan_candidate()?
.expect("typed frontier candidate");
assert_eq!(candidate.planning_request_id, "planning-request-1");
assert_eq!(
candidate.candidate_id,
epiphany_core::canonical_repo_frontier_plan_candidate_id(&candidate)?
);
assert_eq!(
candidate.schema_version,
epiphany_core::REPO_FRONTIER_PLAN_CANDIDATE_SCHEMA_VERSION
);
Ok(())
}
fn test_openai_event(
request_id: &str,
sequence: u64,
@ -1818,6 +1963,8 @@ mod tests {
.to_string(),
),
proposal_modeling_context: None,
claim_repair_context: None,
frontier_planning_context: None,
active_subgoal_id: None,
active_subgoals: Vec::new(),
active_graph_node_ids: Vec::new(),
@ -1840,6 +1987,8 @@ mod tests {
epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID,
),
proposal_modeling_request_id: None,
claim_repair_request_id: None,
frontier_planning_request_id: None,
created_at: now(),
},
)?;
@ -1945,6 +2094,8 @@ mod tests {
.to_string(),
),
proposal_modeling_context: None,
claim_repair_context: None,
frontier_planning_context: None,
active_subgoal_id: None,
active_subgoals: Vec::new(),
active_graph_node_ids: Vec::new(),
@ -1967,6 +2118,8 @@ mod tests {
epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID,
),
proposal_modeling_request_id: None,
claim_repair_request_id: None,
frontier_planning_request_id: None,
created_at: now(),
},
)?;

View file

@ -965,3 +965,20 @@ and the loser leaves no job, worker launch, or opened event. Full core proof:
322 passed, 1 ignored. Next build the exclusive immutable Imagination result
echo carrying the typed candidate, then Mind-only Adopt/Refuse/Hold admission
and the narrow Adopt model transition that makes Hands routing true.
The immutable Imagination result slice is now complete. Runtime role result v2
adds an exclusive `frontier_planning_request_id` plus dedicated canonical
MessagePack `RepoFrontierPlanCandidate`. Persistence replays the exact planning
request, request-keyed launch binding, stored launch-document hash/projection,
runtime/thread, current model/admission/frontier, deterministic candidate
identity, and bounded safe paths. Planning results reject generic state, Self,
RepoModel, Verification, and Modeling authority cargo before writing. The
OpenAI ingress uses a specialized planning schema, supplies candidate
schema/contract itself, derives candidate identity from semantic cargo, and
does not expose generic patch mouths. The candidate remains embedded in the
immutable result; no standalone candidate document is written. Proof: core
325 passed/1 ignored; OpenAI runtime 9 passed; hostile missing/swapped request,
missing/adjacent/escaped candidate, wrong role, generic patch smuggling, exact
retry, and immutable collision checks pass. Next implement Mind-only terminal
decision plus the dedicated Adopt RepoModel transition from Imagination to
Hands, then delete the old internal candidate/adoption fixture writers.

View file

@ -11,10 +11,18 @@ worker launch document. Caller-authored projections, mixed authority carriers,
stale requests, causal substitutions, and duplicate/racing launches are
refused without loser artifacts.
The downstream nerve is still incomplete. Imagination has no exclusive
immutable result carrying the candidate; Mind has no correlated admission; the
old internal candidate/adoption fixture writers still exist; and Adopt still
changes no RepoModel bytes or Hands truth.
Imagination's output is now a v2 immutable runtime result with an exclusive
planning-request echo and one dedicated canonical MessagePack candidate. Result
persistence replays the current request, launch binding, stored launch bytes,
projection, runtime/thread, model, admission, frontier, candidate identity, and
safe-path scope before its absent-only write. Generic state, Self, RepoModel,
Verification, and Modeling authority cargo are forbidden on this path. The
candidate remains inside the immutable result until Mind admission; no
independent candidate writer participates in production.
The downstream nerve is still incomplete. Mind has no correlated admission;
the old internal candidate/adoption fixture writers still exist for the legacy
test chain; and Adopt still changes no RepoModel bytes or Hands truth.
The public candidate/adoption writers remain removed. The existing internal
functions are test scaffolding only and die when result correlation and Mind

Binary file not shown.

View file

@ -714,4 +714,4 @@ current_status:
- Service lifecycle receipt history is now event-only: `load_epiphany_cultmesh_daemon_service_lifecycle_receipts` filters out the `epiphany-local/daemon-service-lifecycle-receipt/latest` mirror, while `load_latest_epiphany_cultmesh_daemon_service_lifecycle_receipt` remains the latest-state reader. Focused unit test `service_lifecycle_receipt_history_excludes_latest_mirror` proves two per-receipt writes return exactly two history records and latest points at the second. Fresh smoke `verse-query-lifecycle-history-filter-smoke` and live overview/triage `local-20260618-145632-497-efac6f0d` / `local-20260618-145632-321-867947af` prove compact swarm sights still expose service lifecycle attention plus the present elevated runbook artifact without daemon pokes or service mutation. Do not use a broad unscoped cargo test filter for this check on Windows; it fans out across bin harnesses and hit a broken pipe during this pass.
- `epiphany-verse-query seed-compact` is the compact seed receipt for wrappers and read-only rites. The older `seed` command still prints the full local Verse context for explicit debugging, but compact wrapper modes now avoid full `seed`/final `query` readback after the combined overview proof exposed Windows debug stack overflow from full-context serialization pressure.
last_completed_repo_swarm_cut: Huginn live-fire has crossed the first real repo Body from Persona pressure to branch-local Hands commit and Soul/Mind closure, and now has repeatable closure and redacted-public-proof mouths. On `E:\Projects\Huginn` branch `epiphany/huginn/first-awakening`, `tools/epiphany_local_run.ps1 -Mode repo-persona-intake` accepted item `huginn-inspection-readback-livefire`, Weksa/Persona intake remained sealed, and `epiphany-work derive-plan` lowered it to model-authored safe family `repo.status_section` for `README.md` only. `tools/epiphany_local_run.ps1 -Mode repo-swarm-run` advanced four bounded pulses: run-from-plan, adopt-from-plan, execute-from-plan, and close-from-execute. Huginn commit `e9b07c38fceabbdc89ea35c2156326c80c0393a2` updated only `README.md`; closure receipt `.epiphany\work\work-close-huginn-inspection-readback-livefire.json` records status `closed`, Soul verdict `passed`, Mind state commit `repo-work-close-huginn-inspection-readback-livefire-mind-commit`, safe family `repo.status_section`, path scope `README.md`, Bifrost publication gate, and `privateStateExposed=false`. Verification also ran Huginn's TypeScript compiler through concrete Node path: `C:\Program Files\nodejs\node.exe E:\Projects\Huginn\node_modules\typescript\bin\tsc -p tsconfig.json` passed. Native verifier `epiphany-repo-livefire-closure-smoke --workspace E:\Projects\Huginn --item huginn-inspection-readback-livefire --expected-path README.md --expected-family repo.status_section --expected-gate awaiting-publication --expected-blocker bifrost-publication-missing` checks closure receipt, Mind map entry, overview gate/blocker, authority seals, and `git show` changed paths, returning `status=ok`, `soulVerdict=passed`, `publicationGate=Bifrost`, and `privateStateExposed=false`. Wrapper `tools/epiphany_local_run.ps1 -Mode repo-livefire-closure -Workspace E:\Projects\Huginn -RepoWorkItem huginn-inspection-readback-livefire -SkipBuild` exposes the same read-only proof through the local operator mouth; artifact `local-20260621-190156-803-2f85ba15` printed commit, path, gate, blocker, Soul verdict, Mind commit receipt, and `privateStateExposed=False` without push, merge, publication, or target mutation. New wrapper `tools/epiphany_local_run.ps1 -Mode repo-work-public-proof -Workspace E:\Projects\Huginn -RepoWorkItem huginn-inspection-readback-livefire -SkipBuild` exports the redacted proof bundle and repo-local Verse index without publishing; artifact `local-20260621-190645-345-4555b1dc` wrote `.epiphany\public\proof-bundles\repo-work-public-proof-huginn-inspection-readback-livefire.json`, proof id `repo-work-public-proof-huginn-inspection-readback-livefire`, SHA-256 `857545b199f3e37eb06c2e757e5cfc201958e46667bb1f8403fd333a22620f63`, nine artifact rows, zero publication rows, and `privateStateExposed=False`. swarm overview readback `local-20260621-190701-140-b9519ec6` sees the `REPO-WORK-PUBLIC-PROOF` row and action row from Huginn's repo-local Verse with the same SHA-256 while Bifrost publication remains missing. Launcher compact modes are now named compact operator-context modes instead of read-only modes, and `bifrost-public-proof` summaries print item, target public Verse, room, proof SHA-256, review receipts, and credit receipts so publication receipts are auditable without claiming upstream sync.
next_action: Continue the repo-frontier planning nerve from notes/repo-frontier-planning-authority-map.md. The Self-to-Imagination launch artery is now real: planning request v1 binds runtime/thread, coordinator commit replays current model/admission/frontier/challenges/state, injects the typed projection, and atomically persists one request-keyed launch binding with the exact stored launch-document hash; hostile substitutions and a two-launch race leave no loser artifacts. Next add one exclusive planning-request echo on immutable worker result v1 containing the typed candidate, then replace the internal candidate/adoption fixture writers with Mind-only correlated Adopt/Refuse/Hold admission. Adopt must cause a narrow RepoModel transition that makes downstream Hands truth real; Refuse/Hold remain inert immutable receipts. After the nerve lands, add the production semantic index over typed graph claims so Modeling maintains a live searchable body map instead of forcing Hands to rediscover anatomy. Do not revive Proprioception, epiphany-work, metadata adapters, generic work-order routers, or inference from namespaces/prompt prose/evidence coincidence. Keep closure's zero-substring invariant.
next_action: Continue the repo-frontier planning nerve from notes/repo-frontier-planning-authority-map.md. Self-to-Imagination launch and result arteries are real: request v1 binds model/frontier/runtime/thread; coordinator atomically injects and binds the exact launch; immutable worker result v2 exclusively echoes that request and embeds one canonical typed candidate after full request/launch/model/frontier replay, with generic patch mouths sealed. Next replace the internal candidate/adoption fixture writers with one Mind-only correlated Adopt/Refuse/Hold admission. Adopt must atomically install the exact candidate cargo and provenance into the canonical frontier, change only Imagination to Hands through a dedicated patch purpose/operation, and admit the new RepoModel; routes must copy execution cargo from that model, never consult a candidate/adoption shadow store. Refuse/Hold remain inert immutable receipts. After the nerve lands, add the production semantic index over typed graph claims so Modeling maintains a live searchable body map instead of forcing Hands to rediscover anatomy. Do not revive Proprioception, epiphany-work, metadata adapters, generic work-order routers, or inference from namespaces/prompt prose/evidence coincidence. Keep closure's zero-substring invariant.