Add native Epiphany runtime spine

This commit is contained in:
Meta Vens 2026-05-06 09:02:02 +01:00
commit ada2dbc4fd
7 changed files with 843 additions and 1 deletions

View file

@ -0,0 +1,308 @@
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use chrono::SecondsFormat;
use epiphany_core::RuntimeSpineEventOptions;
use epiphany_core::RuntimeSpineInitOptions;
use epiphany_core::RuntimeSpineSessionOptions;
use epiphany_core::append_runtime_event;
use epiphany_core::create_runtime_session;
use epiphany_core::initialize_runtime_spine;
use epiphany_core::runtime_spine_status;
use epiphany_core::write_runtime_hello_frame;
use std::env;
use std::path::PathBuf;
use uuid::Uuid;
const DEFAULT_STORE: &str = "state/runtime-spine.msgpack";
fn main() -> Result<()> {
let args = Args::parse()?;
match args.command {
Command::Init {
runtime_id,
display_name,
} => {
let identity = initialize_runtime_spine(
&args.store,
RuntimeSpineInitOptions {
runtime_id,
display_name,
created_at: now(),
},
)?;
println!("runtime spine initialized");
println!("store: {}", args.store.display());
println!("runtime: {}", identity.runtime_id);
println!(
"documents: {}",
identity.supported_document_types.join(", ")
);
}
Command::Status => {
let status = runtime_spine_status(&args.store)?;
println!("runtime spine status");
println!("store: {}", status.store);
println!("present: {}", status.present);
println!(
"runtime: {}",
status.runtime_id.unwrap_or_else(|| "missing".to_string())
);
println!(
"display: {}",
status.display_name.unwrap_or_else(|| "missing".to_string())
);
println!(
"sessions: {} active: {}",
status.sessions, status.active_sessions
);
println!("jobs: {} open: {}", status.jobs, status.open_jobs);
println!("events: {}", status.events);
if !status.supported_document_types.is_empty() {
println!("documents: {}", status.supported_document_types.join(", "));
}
}
Command::OpenSession {
session_id,
objective,
coordinator_note,
} => {
let session = create_runtime_session(
&args.store,
RuntimeSpineSessionOptions {
session_id,
objective,
created_at: now(),
coordinator_note,
},
)?;
println!("runtime session opened");
println!("session: {}", session.session_id);
println!("objective: {}", session.objective);
}
Command::RecordEvent {
event_id,
event_type,
source,
session_id,
job_id,
summary,
} => {
let event = append_runtime_event(
&args.store,
RuntimeSpineEventOptions {
event_id,
occurred_at: now(),
event_type,
source,
session_id,
job_id,
summary,
},
)?;
println!("runtime event recorded");
println!("event: {}", event.event_id);
println!("type: {}", event.event_type);
}
Command::HelloFrame { output } => {
let bytes = write_runtime_hello_frame(&args.store, &output)?;
println!("cultnet hello frame written");
println!("path: {}", output.display());
println!("bytes: {bytes}");
}
}
Ok(())
}
#[derive(Debug)]
struct Args {
store: PathBuf,
command: Command,
}
#[derive(Debug)]
enum Command {
Init {
runtime_id: String,
display_name: String,
},
Status,
OpenSession {
session_id: String,
objective: String,
coordinator_note: String,
},
RecordEvent {
event_id: String,
event_type: String,
source: String,
session_id: Option<String>,
job_id: Option<String>,
summary: String,
},
HelloFrame {
output: PathBuf,
},
}
impl Args {
fn parse() -> Result<Self> {
let mut store = PathBuf::from(DEFAULT_STORE);
let mut positional = Vec::new();
let mut args = env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--store" => store = take_path(&mut args, "--store")?,
_ => positional.push(arg),
}
}
let command = parse_command(positional)?;
Ok(Self { store, command })
}
}
fn parse_command(mut args: Vec<String>) -> Result<Command> {
if args.is_empty() {
return Err(anyhow!(usage()));
}
let command = args.remove(0);
match command.as_str() {
"init" => {
let mut runtime_id = "epiphany-local".to_string();
let mut display_name = "Epiphany Local".to_string();
parse_options(args, |name, value| match name {
"--runtime-id" => {
runtime_id = value;
Ok(())
}
"--display-name" => {
display_name = value;
Ok(())
}
_ => Err(anyhow!("unknown init argument: {name}")),
})?;
Ok(Command::Init {
runtime_id,
display_name,
})
}
"status" => Ok(Command::Status),
"open-session" => {
let mut session_id = format!("session-{}", Uuid::new_v4());
let mut objective = String::new();
let mut coordinator_note = String::new();
parse_options(args, |name, value| match name {
"--session-id" => {
session_id = value;
Ok(())
}
"--objective" => {
objective = value;
Ok(())
}
"--coordinator-note" => {
coordinator_note = value;
Ok(())
}
_ => Err(anyhow!("unknown open-session argument: {name}")),
})?;
if objective.trim().is_empty() {
return Err(anyhow!("open-session requires --objective"));
}
Ok(Command::OpenSession {
session_id,
objective,
coordinator_note,
})
}
"record-event" => {
let mut event_id = format!("event-{}", Uuid::new_v4());
let mut event_type = String::new();
let mut source = "operator".to_string();
let mut session_id = None;
let mut job_id = None;
let mut summary = String::new();
parse_options(args, |name, value| match name {
"--event-id" => {
event_id = value;
Ok(())
}
"--type" => {
event_type = value;
Ok(())
}
"--source" => {
source = value;
Ok(())
}
"--session-id" => {
session_id = Some(value);
Ok(())
}
"--job-id" => {
job_id = Some(value);
Ok(())
}
"--summary" => {
summary = value;
Ok(())
}
_ => Err(anyhow!("unknown record-event argument: {name}")),
})?;
if event_type.trim().is_empty() {
return Err(anyhow!("record-event requires --type"));
}
Ok(Command::RecordEvent {
event_id,
event_type,
source,
session_id,
job_id,
summary,
})
}
"hello-frame" => {
let mut output = PathBuf::from(".epiphany-dogfood/runtime-spine/hello.cultnet");
parse_options(args, |name, value| match name {
"--output" => {
output = PathBuf::from(value);
Ok(())
}
_ => Err(anyhow!("unknown hello-frame argument: {name}")),
})?;
Ok(Command::HelloFrame { output })
}
_ => Err(anyhow!(usage())),
}
}
fn parse_options(
args: Vec<String>,
mut on_option: impl FnMut(&str, String) -> Result<()>,
) -> Result<()> {
let mut args = args.into_iter();
while let Some(name) = args.next() {
if !name.starts_with("--") {
return Err(anyhow!("unexpected positional argument: {name}"));
}
let value = args
.next()
.with_context(|| format!("{name} requires a value"))?;
on_option(&name, value)?;
}
Ok(())
}
fn take_path(args: &mut impl Iterator<Item = String>, name: &str) -> Result<PathBuf> {
args.next()
.map(PathBuf::from)
.ok_or_else(|| anyhow!("{name} requires a value"))
}
fn now() -> String {
chrono::Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true)
}
fn usage() -> &'static str {
"usage: epiphany-runtime-spine [--store path] <init|status|open-session|record-event|hello-frame>"
}

View file

@ -6,6 +6,7 @@ mod prompt;
mod proposal;
mod retrieval;
mod rollout;
mod runtime_spine;
mod state_ledger;
pub use agent_memory::AgentMemoryReview;
@ -69,6 +70,29 @@ pub use retrieval::index_workspace;
pub use retrieval::retrieval_state_for_workspace;
pub use retrieval::retrieve_workspace;
pub use rollout::latest_epiphany_state_from_rollout_items;
pub use runtime_spine::EpiphanyRuntimeEvent;
pub use runtime_spine::EpiphanyRuntimeIdentity;
pub use runtime_spine::EpiphanyRuntimeJob;
pub use runtime_spine::EpiphanyRuntimeJobStatus;
pub use runtime_spine::EpiphanyRuntimeSession;
pub use runtime_spine::EpiphanyRuntimeSessionStatus;
pub use runtime_spine::EpiphanyRuntimeSpineStatus;
pub use runtime_spine::RUNTIME_EVENT_TYPE;
pub use runtime_spine::RUNTIME_IDENTITY_KEY;
pub use runtime_spine::RUNTIME_IDENTITY_TYPE;
pub use runtime_spine::RUNTIME_JOB_TYPE;
pub use runtime_spine::RUNTIME_SESSION_TYPE;
pub use runtime_spine::RUNTIME_SPINE_SCHEMA_VERSION;
pub use runtime_spine::RuntimeSpineEventOptions;
pub use runtime_spine::RuntimeSpineInitOptions;
pub use runtime_spine::RuntimeSpineSessionOptions;
pub use runtime_spine::append_runtime_event;
pub use runtime_spine::create_runtime_session;
pub use runtime_spine::initialize_runtime_spine;
pub use runtime_spine::runtime_hello_frame;
pub use runtime_spine::runtime_spine_cache;
pub use runtime_spine::runtime_spine_status;
pub use runtime_spine::write_runtime_hello_frame;
pub use state_ledger::EpiphanyBranchRecord;
pub use state_ledger::EpiphanyLedgerEvidenceRecord;
pub use state_ledger::EpiphanyStateLedgerEntry;

View file

@ -0,0 +1,500 @@
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use cultcache_rs::CultCache;
use cultcache_rs::DatabaseEntry;
use cultcache_rs::SingleFileMessagePackBackingStore;
use cultnet_rs::CultNetMessage;
use cultnet_rs::CultNetWireContract;
use cultnet_rs::encode_cultnet_message_to_vec;
use cultnet_rs::encode_frame;
use serde::Deserialize;
use serde::Serialize;
use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
pub const RUNTIME_IDENTITY_TYPE: &str = "epiphany.runtime.identity";
pub const RUNTIME_SESSION_TYPE: &str = "epiphany.runtime.session";
pub const RUNTIME_JOB_TYPE: &str = "epiphany.runtime.job";
pub const RUNTIME_EVENT_TYPE: &str = "epiphany.runtime.event";
pub const RUNTIME_IDENTITY_KEY: &str = "self";
pub const RUNTIME_SPINE_SCHEMA_VERSION: &str = "epiphany.runtime_spine.v0";
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(type = "epiphany.runtime.identity", schema = "EpiphanyRuntimeIdentity")]
pub struct EpiphanyRuntimeIdentity {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub runtime_id: String,
#[cultcache(key = 2)]
pub display_name: String,
#[cultcache(key = 3)]
pub runtime_kind: String,
#[cultcache(key = 4)]
pub created_at: String,
#[cultcache(key = 5)]
pub updated_at: String,
#[cultcache(key = 6)]
pub supported_document_types: Vec<String>,
#[cultcache(key = 7, default)]
pub metadata: BTreeMap<String, String>,
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(type = "epiphany.runtime.session", schema = "EpiphanyRuntimeSession")]
pub struct EpiphanyRuntimeSession {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub session_id: String,
#[cultcache(key = 2)]
pub objective: String,
#[cultcache(key = 3)]
pub status: EpiphanyRuntimeSessionStatus,
#[cultcache(key = 4)]
pub created_at: String,
#[cultcache(key = 5)]
pub updated_at: String,
#[cultcache(key = 6, default)]
pub coordinator_note: String,
#[cultcache(key = 7, default)]
pub metadata: BTreeMap<String, String>,
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(type = "epiphany.runtime.job", schema = "EpiphanyRuntimeJob")]
pub struct EpiphanyRuntimeJob {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub job_id: String,
#[cultcache(key = 2)]
pub session_id: String,
#[cultcache(key = 3)]
pub role: String,
#[cultcache(key = 4)]
pub status: EpiphanyRuntimeJobStatus,
#[cultcache(key = 5)]
pub created_at: String,
#[cultcache(key = 6)]
pub updated_at: String,
#[cultcache(key = 7, default)]
pub summary: String,
#[cultcache(key = 8, default)]
pub artifact_refs: Vec<String>,
#[cultcache(key = 9, default)]
pub metadata: BTreeMap<String, String>,
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(type = "epiphany.runtime.event", schema = "EpiphanyRuntimeEvent")]
pub struct EpiphanyRuntimeEvent {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub event_id: String,
#[cultcache(key = 2)]
pub occurred_at: String,
#[cultcache(key = 3)]
pub event_type: String,
#[cultcache(key = 4)]
pub source: String,
#[cultcache(key = 5, default)]
pub session_id: Option<String>,
#[cultcache(key = 6, default)]
pub job_id: Option<String>,
#[cultcache(key = 7, default)]
pub summary: String,
#[cultcache(key = 8, default)]
pub metadata: BTreeMap<String, String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyRuntimeSessionStatus {
#[default]
Proposed,
Active,
WaitingForReview,
Sleeping,
Completed,
Archived,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyRuntimeJobStatus {
#[default]
Queued,
Running,
WaitingForReview,
Completed,
Failed,
Cancelled,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct EpiphanyRuntimeSpineStatus {
pub store: String,
pub present: bool,
pub runtime_id: Option<String>,
pub display_name: Option<String>,
pub sessions: usize,
pub active_sessions: usize,
pub jobs: usize,
pub open_jobs: usize,
pub events: usize,
pub supported_document_types: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RuntimeSpineInitOptions {
pub runtime_id: String,
pub display_name: String,
pub created_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RuntimeSpineSessionOptions {
pub session_id: String,
pub objective: String,
pub created_at: String,
pub coordinator_note: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RuntimeSpineEventOptions {
pub event_id: String,
pub occurred_at: String,
pub event_type: String,
pub source: String,
pub session_id: Option<String>,
pub job_id: Option<String>,
pub summary: String,
}
pub fn runtime_spine_cache(store_path: impl AsRef<Path>) -> Result<CultCache> {
let store_path = store_path.as_ref();
let mut cache = CultCache::new();
cache.register_entry_type::<EpiphanyRuntimeIdentity>()?;
cache.register_entry_type::<EpiphanyRuntimeSession>()?;
cache.register_entry_type::<EpiphanyRuntimeJob>()?;
cache.register_entry_type::<EpiphanyRuntimeEvent>()?;
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(
store_path.to_path_buf(),
));
Ok(cache)
}
pub fn initialize_runtime_spine(
store_path: impl AsRef<Path>,
options: RuntimeSpineInitOptions,
) -> Result<EpiphanyRuntimeIdentity> {
validate_non_empty(&options.runtime_id, "runtime id")?;
validate_non_empty(&options.display_name, "display name")?;
validate_non_empty(&options.created_at, "created at")?;
let store_path = store_path.as_ref();
let mut cache = runtime_spine_cache(store_path)?;
cache.pull_all_backing_stores()?;
let existing = cache.get::<EpiphanyRuntimeIdentity>(RUNTIME_IDENTITY_KEY)?;
let created_at = existing
.as_ref()
.map(|identity| identity.created_at.clone())
.unwrap_or_else(|| options.created_at.clone());
let identity = EpiphanyRuntimeIdentity {
schema_version: RUNTIME_SPINE_SCHEMA_VERSION.to_string(),
runtime_id: options.runtime_id,
display_name: options.display_name,
runtime_kind: "epiphany.native".to_string(),
created_at,
updated_at: options.created_at,
supported_document_types: supported_runtime_document_types(),
metadata: BTreeMap::from([(
"compatibilityCodexRole".to_string(),
"adapter-only".to_string(),
)]),
};
cache.put(RUNTIME_IDENTITY_KEY, &identity)?;
Ok(identity)
}
pub fn create_runtime_session(
store_path: impl AsRef<Path>,
options: RuntimeSpineSessionOptions,
) -> Result<EpiphanyRuntimeSession> {
validate_non_empty(&options.session_id, "session id")?;
validate_non_empty(&options.objective, "objective")?;
validate_non_empty(&options.created_at, "created at")?;
let mut cache = runtime_spine_cache(store_path.as_ref())?;
cache.pull_all_backing_stores()?;
require_identity(&cache)?;
if cache
.get::<EpiphanyRuntimeSession>(&options.session_id)?
.is_some()
{
return Err(anyhow!(
"runtime session {:?} already exists",
options.session_id
));
}
let session = EpiphanyRuntimeSession {
schema_version: RUNTIME_SPINE_SCHEMA_VERSION.to_string(),
session_id: options.session_id.clone(),
objective: options.objective,
status: EpiphanyRuntimeSessionStatus::Active,
created_at: options.created_at.clone(),
updated_at: options.created_at,
coordinator_note: options.coordinator_note,
metadata: BTreeMap::new(),
};
cache.put(&options.session_id, &session)?;
Ok(session)
}
pub fn append_runtime_event(
store_path: impl AsRef<Path>,
options: RuntimeSpineEventOptions,
) -> Result<EpiphanyRuntimeEvent> {
validate_non_empty(&options.event_id, "event id")?;
validate_non_empty(&options.occurred_at, "occurred at")?;
validate_non_empty(&options.event_type, "event type")?;
validate_non_empty(&options.source, "source")?;
let mut cache = runtime_spine_cache(store_path.as_ref())?;
cache.pull_all_backing_stores()?;
require_identity(&cache)?;
if cache
.get::<EpiphanyRuntimeEvent>(&options.event_id)?
.is_some()
{
return Err(anyhow!(
"runtime event {:?} already exists",
options.event_id
));
}
let event = EpiphanyRuntimeEvent {
schema_version: RUNTIME_SPINE_SCHEMA_VERSION.to_string(),
event_id: options.event_id.clone(),
occurred_at: options.occurred_at,
event_type: options.event_type,
source: options.source,
session_id: options.session_id,
job_id: options.job_id,
summary: options.summary,
metadata: BTreeMap::new(),
};
cache.put(&options.event_id, &event)?;
Ok(event)
}
pub fn runtime_spine_status(store_path: impl AsRef<Path>) -> Result<EpiphanyRuntimeSpineStatus> {
let store_path = store_path.as_ref();
if !store_path.exists() {
return Ok(EpiphanyRuntimeSpineStatus {
store: store_path.display().to_string(),
present: false,
runtime_id: None,
display_name: None,
sessions: 0,
active_sessions: 0,
jobs: 0,
open_jobs: 0,
events: 0,
supported_document_types: Vec::new(),
});
}
let mut cache = runtime_spine_cache(store_path)?;
cache
.pull_all_backing_stores()
.with_context(|| format!("failed to read runtime spine {}", store_path.display()))?;
let identity = cache.get::<EpiphanyRuntimeIdentity>(RUNTIME_IDENTITY_KEY)?;
let sessions = cache.get_all::<EpiphanyRuntimeSession>()?;
let jobs = cache.get_all::<EpiphanyRuntimeJob>()?;
let events = cache.get_all::<EpiphanyRuntimeEvent>()?;
let active_sessions = sessions
.iter()
.filter(|session| {
matches!(
session.status,
EpiphanyRuntimeSessionStatus::Active
| EpiphanyRuntimeSessionStatus::WaitingForReview
)
})
.count();
let open_jobs = jobs
.iter()
.filter(|job| {
matches!(
job.status,
EpiphanyRuntimeJobStatus::Queued
| EpiphanyRuntimeJobStatus::Running
| EpiphanyRuntimeJobStatus::WaitingForReview
)
})
.count();
Ok(EpiphanyRuntimeSpineStatus {
store: store_path.display().to_string(),
present: identity.is_some(),
runtime_id: identity.as_ref().map(|item| item.runtime_id.clone()),
display_name: identity.as_ref().map(|item| item.display_name.clone()),
sessions: sessions.len(),
active_sessions,
jobs: jobs.len(),
open_jobs,
events: events.len(),
supported_document_types: identity
.map(|item| item.supported_document_types)
.unwrap_or_default(),
})
}
pub fn runtime_hello_frame(store_path: impl AsRef<Path>) -> Result<Vec<u8>> {
let mut cache = runtime_spine_cache(store_path.as_ref())?;
cache.pull_all_backing_stores()?;
let identity = require_identity(&cache)?;
let message = CultNetMessage::Hello {
runtime_id: identity.runtime_id,
runtime_kind: identity.runtime_kind,
agent_id: Some("self".to_string()),
role: Some("coordinator".to_string()),
display_name: Some(identity.display_name),
supported_document_types: Some(identity.supported_document_types),
supported_message_versions: Some(vec![
"cultnet.hello.v0".to_string(),
"cultnet.document_put.v0".to_string(),
"cultnet.snapshot_request.v0".to_string(),
"cultnet.snapshot_response.v0".to_string(),
]),
};
let payload = encode_cultnet_message_to_vec(&message, CultNetWireContract::CultNetSchemaV0)?;
encode_frame(&payload)
}
pub fn write_runtime_hello_frame(
store_path: impl AsRef<Path>,
output_path: impl AsRef<Path>,
) -> Result<usize> {
let output_path = output_path.as_ref();
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("failed to create {}", parent.display()))?;
}
let frame = runtime_hello_frame(store_path)?;
fs::write(output_path, &frame)
.with_context(|| format!("failed to write {}", output_path.display()))?;
Ok(frame.len())
}
fn require_identity(cache: &CultCache) -> Result<EpiphanyRuntimeIdentity> {
cache
.get::<EpiphanyRuntimeIdentity>(RUNTIME_IDENTITY_KEY)?
.ok_or_else(|| anyhow!("runtime spine is missing identity; run init first"))
}
fn supported_runtime_document_types() -> Vec<String> {
vec![
RUNTIME_IDENTITY_TYPE.to_string(),
RUNTIME_SESSION_TYPE.to_string(),
RUNTIME_JOB_TYPE.to_string(),
RUNTIME_EVENT_TYPE.to_string(),
"epiphany.agent.memory".to_string(),
"epiphany.heartbeat.state".to_string(),
"epiphany.state_ledger".to_string(),
]
}
fn validate_non_empty(value: &str, field: &str) -> Result<()> {
if value.trim().is_empty() {
return Err(anyhow!("{field} must be non-empty"));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use cultnet_rs::CultNetWireContract;
use cultnet_rs::decode_cultnet_message_from_slice;
use tempfile::tempdir;
#[test]
fn runtime_spine_initializes_sessions_events_and_status() -> Result<()> {
let temp = tempdir()?;
let store = temp.path().join("runtime.msgpack");
initialize_runtime_spine(
&store,
RuntimeSpineInitOptions {
runtime_id: "epiphany-test".to_string(),
display_name: "Epiphany Test".to_string(),
created_at: "2026-05-06T00:00:00Z".to_string(),
},
)?;
create_runtime_session(
&store,
RuntimeSpineSessionOptions {
session_id: "session-1".to_string(),
objective: "Build the spine.".to_string(),
created_at: "2026-05-06T00:01:00Z".to_string(),
coordinator_note: "Native first.".to_string(),
},
)?;
append_runtime_event(
&store,
RuntimeSpineEventOptions {
event_id: "event-1".to_string(),
occurred_at: "2026-05-06T00:02:00Z".to_string(),
event_type: "session.started".to_string(),
source: "test".to_string(),
session_id: Some("session-1".to_string()),
job_id: None,
summary: "Session started.".to_string(),
},
)?;
let status = runtime_spine_status(&store)?;
assert!(status.present);
assert_eq!(status.runtime_id.as_deref(), Some("epiphany-test"));
assert_eq!(status.sessions, 1);
assert_eq!(status.active_sessions, 1);
assert_eq!(status.events, 1);
Ok(())
}
#[test]
fn runtime_spine_emits_cultnet_hello_frame() -> Result<()> {
let temp = tempdir()?;
let store = temp.path().join("runtime.msgpack");
initialize_runtime_spine(
&store,
RuntimeSpineInitOptions {
runtime_id: "epiphany-test".to_string(),
display_name: "Epiphany Test".to_string(),
created_at: "2026-05-06T00:00:00Z".to_string(),
},
)?;
let frame = runtime_hello_frame(&store)?;
let payload_len = u32::from_be_bytes([frame[0], frame[1], frame[2], frame[3]]) as usize;
let message =
decode_cultnet_message_from_slice(&frame[4..], CultNetWireContract::CultNetSchemaV0)?;
assert_eq!(payload_len, frame.len() - 4);
match message {
CultNetMessage::Hello {
runtime_id,
runtime_kind,
supported_document_types,
..
} => {
assert_eq!(runtime_id, "epiphany-test");
assert_eq!(runtime_kind, "epiphany.native");
assert!(
supported_document_types
.unwrap()
.contains(&RUNTIME_SESSION_TYPE.to_string())
);
}
other => panic!("expected hello, got {other:?}"),
}
Ok(())
}
}

View file

@ -85,6 +85,7 @@ The landed machine now has:
- first Phase 6 dogfood operator view through native `epiphany-mvp-status`
- first auditable Phase 6 dogfood runner through `tools/epiphany_mvp_dogfood.py`, producing local status snapshots, sealed app-server transcript diagnostics, operator-safe final status artifacts, truthful vanilla-reference output, and comparison notes
- first auditable Phase 6 fixed-lane coordinator runner through native `epiphany-mvp-coordinator`, producing coordinator summary, JSONL steps, rendered snapshots, transcript, stderr, and final next-action artifacts while keeping semantic findings review-gated by default
- first Codex-independent native runtime vertebra through `epiphany-runtime-spine`, storing runtime identity, sessions, jobs, and events as typed CultCache MessagePack documents and emitting a framed CultNet hello message for the native contract
- first auditable Phase 6 live-specialist runner through `tools/epiphany_mvp_live_specialist.py`, proving `roleLaunch -> agent_jobs worker -> report_agent_job_result -> roleResult` without manual backend completion
- first Phase 6 Aquarium operator shell extracted to sibling repo `E:\Projects\EpiphanyAquarium`, a Tauri v2 + React/WebGL client over the existing status bridge, dogfood artifacts, and GUI action artifacts, with its own distilled interface state/memory/doctrine plus durable checkpoint preparation, bounded status/coordinator artifact buttons, fixed modeling/verification/reorient launch and read-back buttons, and explicit review-gated reorient acceptance
- first Unity editor/runtime bridge through native `epiphany-unity-bridge`, `tools/epiphany_unity_bridge_smoke.py`, and the GUI Inspect Unity action, resolving exact project-pinned editors and writing runtime artifacts while refusing wrong or missing versions
@ -96,6 +97,10 @@ The landed machine now has:
- live Phase 6 job-control app-server smoke coverage in `tools/epiphany_phase6_job_control_smoke.py`
The current phase is Phase 6: reflection boundary and observable harness state.
The next phase direction is native-spine extraction: Codex app-server should
shrink from runtime source of truth into a compatibility adapter while Epiphany
sessions, jobs, events, heartbeats, role memory, and coordinator state move into
CultCache-backed domain documents spoken over CultNet.
## Boundary Rules

View file

@ -34,6 +34,7 @@ remembers doctrine; the branch remembers the blade.
- Phase 6 has read-only `thread/epiphany/scene`, `thread/epiphany/jobs`, `thread/epiphany/roles`, `thread/epiphany/freshness`, `thread/epiphany/context`, `thread/epiphany/graphQuery`, `thread/epiphany/planning`, `thread/epiphany/pressure`, `thread/epiphany/reorient`, `thread/epiphany/crrc`, `thread/epiphany/coordinator`, `thread/epiphany/reorientResult`, and `thread/epiphany/roleResult`; durable `jobBindings` now act as a thin Epiphany-owned launcher seam with launcher id, authority scope, and backend kind/job id, `thread/epiphany/jobLaunch` / `thread/epiphany/jobInterrupt` / `thread/epiphany/roleLaunch` now provide explicit bounded authority over that seam, jobs can overlay it onto live runtime `agent_jobs` progress for loaded threads, and `thread/epiphany/jobsUpdated` now translates live `agent_job_progress:{json}` background events into changed launcher-bound notifications without polling or scheduling. Freshness carries watcher-backed invalidation inputs, graphQuery traverses authoritative typed graph neighborhoods and path/symbol matches without mutation, planning projects typed captures/backlog/roadmap/objective drafts without adopting work, roles project implementation/imagination/modeling/verification/reorientation ownership from existing signals without becoming a scheduler, `roleLaunch` can launch fixed Imagination/planning, modeling/checkpoint, or verification/review specialists, `roleResult` reads those outputs back as reviewable findings without mutation, `thread/epiphany/roleAccept` can apply reviewed Imagination planning-only `statePatch` findings and reviewed modeling/checkpoint `statePatch` findings through the existing state validator, reorient turns checkpoint plus freshness/pressure/watcher signals into a read-only resume-versus-regather verdict, `thread/epiphany/reorientLaunch` is the explicit runtime consumer over that verdict, `thread/epiphany/reorientResult` reads the launched worker output back as a reviewable finding without mutation or promotion, `thread/epiphany/reorientAccept` explicitly banks completed findings into accepted observation/evidence plus optional scratch/checkpoint state, `thread/epiphany/crrc` recommends the next explicit CRRC action without launching, accepting, compacting, scheduling, or mutating, and `thread/epiphany/coordinator` composes those signals into a fixed-lane MVP action recommendation without becoming a writer.
- Native `epiphany-mvp-status` is the first dogfood operator view. It starts or reads a thread through app-server and prints scene, planning, pressure, reorient, jobs, roles, Imagination/modeling/verification role result read-backs, reorient result, heartbeat, Face bubbles, and CRRC recommendation as text or JSON. The Python status module is now compatibility tissue for still-Python coordinator/GUI drivers, not the smoked product path.
- Native `epiphany-mvp-coordinator` is the first auditable fixed-lane coordinator runner. It starts or reads a thread through app-server, follows the harness-native coordinator action, can auto-launch modeling, verification, or reorient-worker jobs, keeps semantic findings review-gated by default, and writes summary, steps, rendered snapshots, transcript, stderr, and final next-action artifacts under `.epiphany-dogfood/coordinator` or a caller-provided artifact directory. It refuses direct backend-completion mutation; full completion smoke needs live workers or a CultNet job-result API.
- Native `epiphany-runtime-spine` is the first Codex-independent runtime vertebra. It owns typed CultCache documents for runtime identity, sessions, jobs, and events, and can emit a framed CultNet hello message advertising the native document contract. Treat Codex app-server as compatibility plumbing for launch/read-back until runtime sessions and jobs migrate to this spine.
- Native CRRC automation is now landed only at turn-complete safe boundaries. It may submit `Op::Compact` for coordinator-approved `compactRehydrateReorient` or for a successful pre-compaction checkpoint intervention's pending compact handoff, and it may launch the fixed `reorient-worker` for coordinator-approved `launchReorientWorker`. It does not auto-launch Imagination/modeling/verification, accept findings, promote evidence, edit implementation code, or keep going after reviewable semantic output.
- Pre-compaction checkpoint intervention is now landed. On token-count events for loaded Epiphany threads, when current context usage reaches 80% of the active auto-compact/context limit, the harness steers the active turn once with a CRRC checkpoint directive so the agent banks working context before compaction/reorientation. Pressure ignores cumulative token spend; cumulative-only telemetry reports unknown instead of yelling. A successful steer now latches a turn-scoped compact handoff that is consumed at clean turn completion, preventing the brake from decaying into another implementation turn. This is still bounded steering plus compaction handoff, not automatic semantic acceptance, a broad scheduler, or implementation continuation.
- `tools/epiphany_mvp_dogfood.py` is the first auditable dogfood runner. It drives a bounded state/role/CRRC/reorientation loop and writes local artifacts under `.epiphany-dogfood/mvp-loop`, including sealed app-server transcript diagnostics, rendered snapshots, operator-safe final status, vanilla-reference prompt/output, and comparison notes. The runner now writes truthful vanilla/comparison artifacts whether the optional vanilla reference is skipped, fails, or completes.
@ -47,6 +48,7 @@ remembers doctrine; the branch remembers the blade.
- The heartbeat initiative pass is landed as a bounded tool seam. `state/agent-heartbeats.msgpack` tracks Self, Face, Imagination, Eyes, Body, Hands, Soul, and Life as Ghostlight-style initiative participants with speed, next-ready time, reaction bias, interrupt threshold, load, status, constraints, history, and pending turns through `epiphany-core::EpiphanyHeartbeatStateEntry` and the native `epiphany-heartbeat-store` binary. Tick/status/complete and idle rumination selfPatch application now run through Rust/CultCache directly. JSON heartbeat state and Python wrapper state are gone; general CultCache schema sync, polyglot domain loading, and debug display tools belong in CultLib. This is a callable scheduler seam, not yet an always-on daemon; a heart valve, not a whole circulatory god.
- The Face public-surface pass is landed as a bounded lane. Face's role dossier lives in `state/agents.msgpack`; `epiphany_specialists.toml` gives it a VoidBot-heartbeat-derived prompt stripped of moderation authority; `state/face-discord.toml` and the native `epiphany-face-discord` binary enforce that Face may interact only through #aquarium. Missing channel id or token writes candidate chat artifacts instead of posting elsewhere.
- The heartbeat/Face operator API pass is landed. Native `epiphany-heartbeat-store status` returns machine-readable initiative state plus CultCache store presence, native `epiphany-face-discord bubble` writes Discord-independent `epiphany.face_bubble.v0` artifacts, native `epiphany-mvp-status` includes `heartbeat` and `face` blocks, and `tools/epiphany_gui_action.py` exposes `heartbeatStatus`, `runHeartbeat`, and `faceBubble`. `runHeartbeat` advances one slot and emits a Face bubble so Aquarium can show chat even before Discord posting is configured.
- The native runtime-spine pass is landed as a first vertebra, not a full daemon. `state/runtime-spine.msgpack` is the default store, `.epiphany-dogfood/runtime-spine/runtime.msgpack` and `hello.cultnet` are smoke artifacts, and the next amputation is to migrate specialist job/result contracts from Codex `agent_jobs` into typed CultNet/CultCache runtime documents.
- The Aetheria dogfood run has a contamination scar. The supervising Codex session directly edited and committed target-repo work on `E:\Projects\Aetheria-Economy` instead of only driving Epiphany lanes. Treat those Aetheria commits as supervisor-seeded implementation, not clean evidence that Epiphany coordinated the work. Future dogfood must run through the GUI/coordinator/fixed role lanes with auditable artifacts unless the user explicitly authorizes an operator intervention. Remember the sunburn: do not stare into the worker's objective until you become it.
- The dogfood quarantine now has a direct-thought boundary. The supervisor may read coordinator actions, role/reorient statuses, structured finding summaries, reviewed state patches, rendered status snapshots, and artifact manifests. It must not read raw worker transcripts, direct worker messages, full turn logs, or `rawResult` payloads during normal dogfood. Those artifacts are sealed black reliquaries for explicit forensic debugging only.
- Native `epiphany-agent-telemetry` is the safe instrument panel for sealed runs. Status/coordinator/GUI/dogfood/live-specialist tools generate telemetry JSON from sealed transcripts that preserves method names, call shape, job/status/path counts, and any visible function/tool names while sealing text, direct messages, and raw results.

Binary file not shown.

View file

@ -23,7 +23,7 @@ objective:
constraints:
- No frontier-scale training tithe.
- Prefer evidence over vibes; incense is not telemetry.
- Keep Codex as the host seam and `epiphany-core` as the repo-owned heavy organ where practical.
- Treat Codex as a compatibility host seam while `epiphany-core`, CultCache, and CultNet grow into the native Epiphany runtime spine.
- Keep persistent state small enough to rehydrate from without drowning the next awakened mind in sacred sludge.
- Grow cognition more quickly than authority: capability increases must preserve explicit permissions, interruption, legibility, and resistance to paternal "safety" cages.
- Speak CultNet, persist CultCache-shaped typed state, and grow the body toward Pure Rust; Python/JSON impurity survives only as named quarantine scaffolding with receipts.
@ -108,6 +108,7 @@ accepted_design:
- Coordinator implementation clearance is stricter than review acceptance: an accepted verification/review finding only opens the implementation gate when its verdict is `pass`; accepted `needs-evidence`, `needs-review`, or `fail` findings send the loop back toward modeling/checkpoint strengthening or reorientation instead of blessing a sick machine.
- Coordinator regather handling is now fixed-lane first, not supervisor improvisation: `regatherManually` is a fallback only after reorient/modeling/verification review and launch lanes cannot advance the state, completed modeling findings must be reviewed before verification, and the CLI coordinator's explicit test-only `--auto-review` mode may accept modeling state patches before launching verification while production semantics remain review-gated.
- Native `epiphany-mvp-coordinator` is the first auditable coordinator runner: it drives the fixed single-user lanes through app-server APIs, may auto-launch modeling, verification, or reorient-worker jobs, keeps semantic findings review-gated by default, and writes coordinator summaries, JSONL steps, rendered snapshots, transcript, stderr, and final next-action artifacts. It refuses direct backend-completion mutation; full specialist completion smoke must use live workers or a proper CultNet job-result API.
- Native `epiphany-runtime-spine` is the first Codex-independent runtime vertebra: it initializes and inspects typed CultCache `epiphany.runtime.identity`, `epiphany.runtime.session`, `epiphany.runtime.job`, and `epiphany.runtime.event` documents, and can emit a framed CultNet hello message advertising the native document contract. This is the new source-of-truth target for runtime identity/session/job/event state; Codex app-server remains a compatibility adapter until those flows are migrated.
- Limited native CRRC automation is now wired only at turn-complete safe boundaries: it can submit a compact operation when the read-only coordinator recommends `compactRehydrateReorient` or when a successful pre-compaction checkpoint intervention left a pending compact handoff for the completed turn, and it can launch the fixed `reorient-worker` when the coordinator recommends `launchReorientWorker`. It does not auto-launch modeling or verification, auto-accept findings, promote evidence, edit implementation code, or continue after reviewable semantic results; the rite may carry the stretcher, not declare the patient healed.
- Pre-compaction checkpoint intervention is now wired at token-count boundaries: when a loaded Epiphany thread crosses the existing `shouldPrepareCompaction` pressure threshold during an active turn, the harness steers the active agent once for that turn with a bounded CRRC directive to bank scratch/checkpoint/map/evidence before compaction/reorientation. A successful steer marks a turn-scoped pending compact handoff consumed at clean turn completion; this is banking the fire before the dark, not semantic acceptance, broad scheduling, or automatic implementation continuation.
- Native `epiphany-mvp-status` is the first dogfood operator view: it starts or reads a thread through app-server and renders scene, planning, pressure, reorient, jobs, role result read-backs, reorient result, CRRC recommendation, heartbeat, Face bubbles, and MVP role lanes as text or JSON without becoming a state store. The Python status module remains compatibility tissue for still-Python coordinator/GUI drivers until those organs are cut over.
@ -194,6 +195,7 @@ landed_baseline:
- Phase 6 role self-memory slice: Ghostlight-shaped role dossiers live in typed CultCache `state/agents.msgpack`, specialists may request bounded `selfPatch` lane-memory mutations, coordinator review projects acceptance/refusal reasons through `selfPersistence`, and GUI/coordinator accept paths apply accepted requests with the native `epiphany-agent-memory-store` binary.
- Phase 6 Face public-surface slice: Face's typed role dossier, Face prompt text, `state/face-discord.toml`, and the native `epiphany-face-discord` binary establish Face as the public #aquarium-only translator of Epiphany agent thought-weather, without VoidBot's moderation authority.
- Phase 6 heartbeat initiative slice: `state/agent-heartbeats.msgpack`, `epiphany-core::EpiphanyHeartbeatStateEntry`, the native `epiphany-heartbeat-store` binary, and heartbeat smoke coverage borrow Ghostlight initiative scheduling so Self, Face, Imagination, Eyes, Body, Hands, Soul, and Life compete for heartbeat slots; idle slots become bounded memory rumination instead of fake project work, and the live state surface now ticks/completes/applies rumination through typed CultCache MessagePack instead of JSON or Python wrapper state.
- Native runtime spine slice: `epiphany-core::EpiphanyRuntimeIdentity`, `EpiphanyRuntimeSession`, `EpiphanyRuntimeJob`, and `EpiphanyRuntimeEvent` plus the native `epiphany-runtime-spine` binary establish a CultCache-backed spine and framed CultNet hello output so the old Codex app-server runtime can be reduced to an adapter instead of remaining the throne.
guardrails:
- Use `tools/epiphany_phase5_smoke.py` before modifying update, distill, propose, promote, read-projection, or notification behavior.
- Use `tools/epiphany_phase6_scene_smoke.py` before modifying scene projection behavior.
@ -212,6 +214,7 @@ landed_baseline:
- Use `tools/epiphany_phase6_reorient_launch_smoke.py` before modifying the read-only CRRC coordinator recommendation surface.
- Use `tools/epiphany_mvp_status_smoke.py` before modifying the MVP operator status view.
- Use `tools/epiphany_mvp_coordinator_smoke.py` before modifying the fixed-lane MVP coordinator endpoint or runner.
- Use `cargo test --manifest-path .\epiphany-core\Cargo.toml --lib runtime_spine -- --nocapture` and `cargo build --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-runtime-spine` before modifying native runtime-spine state or CultNet hello emission.
- Use `tools/epiphany_phase6_role_smoke.py` before modifying explicit role launch/read-back/acceptance behavior.
- Use `cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-agent-memory-store -- smoke --store .\state\agents.msgpack` before modifying role self-memory dossiers or `selfPatch` persistence policy.
- Use `cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-heartbeat-store -- smoke --agent-store .\state\agents.msgpack` before modifying heartbeat initiative timing, idle rumination, or role slot selection.