Remove parallel executable control planes
This commit is contained in:
parent
1d5a1f1793
commit
387afe4941
60 changed files with 49 additions and 25366 deletions
|
|
@ -59,8 +59,6 @@ uuid = { version = "1", features = ["serde", "v4"] }
|
|||
zeroize = "1"
|
||||
|
||||
[features]
|
||||
semantic-recovery-smoke = []
|
||||
workspace-coverage-recovery-smoke = []
|
||||
coordinator-runtime = ["dep:epiphany-self-policy"]
|
||||
openai-runtime = ["dep:epiphany-openai-runtime"]
|
||||
tool-mcp-runtime = ["dep:epiphany-tool-mcp-runtime"]
|
||||
|
|
@ -121,10 +119,6 @@ path = "epiphany-core/src/bin/epiphany-workspace-coverage-projector.rs"
|
|||
name = "epiphany-memory-semantic"
|
||||
path = "epiphany-core/src/bin/epiphany-memory-semantic.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-verse-query"
|
||||
path = "epiphany-core/src/bin/epiphany-verse-query.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-repository-body"
|
||||
path = "epiphany-core/src/bin/epiphany-repository-body.rs"
|
||||
|
|
|
|||
25
README.md
25
README.md
|
|
@ -241,26 +241,13 @@ Read that before touching the machine. It contains the operating law, re-entry
|
|||
rites, and enough anti-Jenga doctrine to keep the next clever patch from
|
||||
becoming tomorrow's cleanup sermon.
|
||||
|
||||
## Run Locally
|
||||
## Operator Surfaces
|
||||
|
||||
The current local operator wrapper is:
|
||||
|
||||
```powershell
|
||||
.\tools\epiphany_local_run.ps1
|
||||
```
|
||||
|
||||
Useful modes:
|
||||
|
||||
```powershell
|
||||
.\tools\epiphany_local_run.ps1 -Mode status
|
||||
.\tools\epiphany_local_run.ps1 -Mode plan
|
||||
.\tools\epiphany_local_run.ps1 -Mode smoke
|
||||
.\tools\epiphany_local_run.ps1 -Mode run -MaxSteps 4
|
||||
.\tools\epiphany_local_run.ps1 -Mode mvp -PersonaInput "Wake the local swarm and report."
|
||||
```
|
||||
|
||||
`status`, `plan`, and `smoke` are operator-safe and do not spend model calls.
|
||||
`run` and `mvp` can use the retained OpenAI/Codex model transport.
|
||||
Eve/CultMesh projections are the normal human interface. Repository surgery
|
||||
uses the narrow native binaries owned by the packaged runtime; there is no
|
||||
umbrella script that assembles a second local control plane. For direct Mind
|
||||
inspection during development, use the shared-target `epiphany-state` command
|
||||
documented in [AGENTS.md](AGENTS.md).
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
|
|
@ -10,134 +10,10 @@ license-file = "../LICENSE"
|
|||
name = "epiphany_core"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
public-source-gate-probe = []
|
||||
semantic-recovery-smoke = []
|
||||
workspace-coverage-recovery-smoke = []
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-semantic-recovery-smoke"
|
||||
path = "src/bin/epiphany-semantic-recovery-smoke.rs"
|
||||
required-features = ["semantic-recovery-smoke"]
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-workspace-coverage-recovery-smoke"
|
||||
path = "src/bin/epiphany-workspace-coverage-recovery-smoke.rs"
|
||||
required-features = ["workspace-coverage-recovery-smoke"]
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-agent-memory-store"
|
||||
path = "src/bin/epiphany-agent-memory-store.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-agent-telemetry"
|
||||
path = "src/bin/epiphany-agent-telemetry.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-character-loop"
|
||||
path = "src/bin/epiphany-character-loop.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-cluster-daemon"
|
||||
path = "src/bin/epiphany-cluster-daemon.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-cultmesh-smoke"
|
||||
path = "src/bin/epiphany-cultmesh-smoke.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-daemon-survival-rehearsal-smoke"
|
||||
path = "src/bin/epiphany-daemon-survival-rehearsal-smoke.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-hands-action-smoke"
|
||||
path = "src/bin/epiphany-hands-action-smoke.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-mvp-coordinator-smoke"
|
||||
path = "src/bin/epiphany-mvp-coordinator-smoke.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-operator-run"
|
||||
path = "src/bin/epiphany-operator-run.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-operator-snapshot"
|
||||
path = "src/bin/epiphany-operator-snapshot.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-persona-discord"
|
||||
path = "src/bin/epiphany-persona-discord.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-persona-discord-crossing-fixture"
|
||||
path = "src/bin/epiphany-persona-discord-crossing-fixture.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-persona-discord-rudp-client-fixture"
|
||||
path = "src/bin/epiphany-persona-discord-rudp-client-fixture.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-persona-feedback-causal-smoke"
|
||||
path = "src/bin/epiphany-persona-feedback-causal-smoke.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-persona-memory-recall-smoke"
|
||||
path = "src/bin/epiphany-persona-memory-recall-smoke.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-public-source-gate-probe"
|
||||
path = "src/bin/epiphany-public-source-gate-probe.rs"
|
||||
required-features = ["public-source-gate-probe"]
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-persona-other"
|
||||
path = "src/bin/epiphany-persona-other.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-persona-reddit"
|
||||
path = "src/bin/epiphany-persona-reddit.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-prepare-compaction"
|
||||
path = "src/bin/epiphany-prepare-compaction.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-rider-bridge"
|
||||
path = "src/bin/epiphany-rider-bridge.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-rider-bridge-smoke"
|
||||
path = "src/bin/epiphany-rider-bridge-smoke.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-runtime-store-benchmark"
|
||||
path = "src/bin/epiphany-runtime-store-benchmark.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-state-ledger-store"
|
||||
path = "src/bin/epiphany-state-ledger-store.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-unity-bridge"
|
||||
path = "src/bin/epiphany-unity-bridge.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-unity-bridge-smoke"
|
||||
path = "src/bin/epiphany-unity-bridge-smoke.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-void-memory"
|
||||
path = "src/bin/epiphany-void-memory.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-weksa-interlingua-smoke"
|
||||
path = "src/bin/epiphany-weksa-interlingua-smoke.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "epiphany-work"
|
||||
path = "src/bin/epiphany-work.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
bm25 = "2.3.2"
|
||||
|
|
|
|||
|
|
@ -1,284 +0,0 @@
|
|||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use epiphany_core::AgentMemoryLifecycleOperation;
|
||||
use epiphany_core::AgentSelfPatch;
|
||||
use epiphany_core::admit_legacy_agent_memory_generation;
|
||||
use epiphany_core::agent_memory_status;
|
||||
use epiphany_core::apply_agent_memory_lifecycle_operation;
|
||||
use epiphany_core::apply_agent_self_patch_document;
|
||||
use epiphany_core::ensure_agent_memory_swarm_identity;
|
||||
use epiphany_core::migrate_agent_memory_json_dir_to_cultcache;
|
||||
use epiphany_core::project_agent_memory_to_json_dir;
|
||||
use epiphany_core::project_persona_state_for_role;
|
||||
use epiphany_core::refresh_agent_state_soa;
|
||||
use epiphany_core::repair_agent_memory_store;
|
||||
use epiphany_core::review_agent_memory_lifecycle_operation;
|
||||
use epiphany_core::review_agent_self_patch_document;
|
||||
use epiphany_core::validate_agent_memory_store;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut args = env::args().skip(1);
|
||||
let Some(command) = args.next() else {
|
||||
print_usage();
|
||||
std::process::exit(2);
|
||||
};
|
||||
match command.as_str() {
|
||||
"migrate-json-dir" => {
|
||||
let agent_dir = require_path_arg(&mut args, "--agent-dir")?;
|
||||
let store = require_path_arg(&mut args, "--store")?;
|
||||
print_json(&migrate_agent_memory_json_dir_to_cultcache(
|
||||
agent_dir, store,
|
||||
)?)?;
|
||||
}
|
||||
"project-json-dir" => {
|
||||
let store = require_path_arg(&mut args, "--store")?;
|
||||
let output_dir = require_path_arg(&mut args, "--output-dir")?;
|
||||
print_json(&project_agent_memory_to_json_dir(store, output_dir)?)?;
|
||||
}
|
||||
"project-persona" => {
|
||||
let store = require_path_arg(&mut args, "--store")?;
|
||||
let role_id = require_string_arg(&mut args, "--role-id")?;
|
||||
print_json(&project_persona_state_for_role(store, &role_id)?)?;
|
||||
}
|
||||
"status" => {
|
||||
let store = require_path_arg(&mut args, "--store")?;
|
||||
print_json(&agent_memory_status(store)?)?;
|
||||
}
|
||||
"set-swarm-identity" => {
|
||||
let store = require_path_arg(&mut args, "--store")?;
|
||||
let swarm_id = require_string_arg(&mut args, "--swarm-id")?;
|
||||
print_json(&ensure_agent_memory_swarm_identity(store, &swarm_id)?)?;
|
||||
}
|
||||
"admit-legacy-mind" => {
|
||||
let store = require_path_arg(&mut args, "--store")?;
|
||||
print_json(&admit_legacy_agent_memory_generation(store)?)?;
|
||||
}
|
||||
"refresh-soa" => {
|
||||
let store = require_path_arg(&mut args, "--store")?;
|
||||
print_json(&refresh_agent_state_soa(store)?)?;
|
||||
}
|
||||
"repair" => {
|
||||
let store = require_path_arg(&mut args, "--store")?;
|
||||
let result = repair_agent_memory_store(store)?;
|
||||
let ok = result["ok"].as_bool().unwrap_or(false);
|
||||
print_json(&result)?;
|
||||
if !ok {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
"validate" => {
|
||||
let store = require_path_arg(&mut args, "--store")?;
|
||||
let errors = validate_agent_memory_store(&store)?;
|
||||
print_json(&serde_json::json!({
|
||||
"ok": errors.is_empty(),
|
||||
"store": store,
|
||||
"errors": errors,
|
||||
}))?;
|
||||
if !errors.is_empty() {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
"review-patch" => {
|
||||
let store = require_path_arg(&mut args, "--store")?;
|
||||
let role_id = require_string_arg(&mut args, "--role-id")?;
|
||||
let patch = read_patch_arg(&require_string_arg(&mut args, "--patch")?)?;
|
||||
print_json(&review_agent_self_patch_document(&role_id, &patch, store))?;
|
||||
}
|
||||
"apply-patch" => {
|
||||
let store = require_path_arg(&mut args, "--store")?;
|
||||
let role_id = require_string_arg(&mut args, "--role-id")?;
|
||||
let patch = read_patch_arg(&require_string_arg(&mut args, "--patch")?)?;
|
||||
let result = apply_agent_self_patch_document(&role_id, patch, store)?;
|
||||
let accepted = result.status == "accepted";
|
||||
print_json(&result)?;
|
||||
if !accepted {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
"review-lifecycle" => {
|
||||
let store = require_path_arg(&mut args, "--store")?;
|
||||
let role_id = require_string_arg(&mut args, "--role-id")?;
|
||||
let operation = read_lifecycle_arg(&require_string_arg(&mut args, "--operation")?)?;
|
||||
print_json(&review_agent_memory_lifecycle_operation(
|
||||
&role_id, &operation, store,
|
||||
))?;
|
||||
}
|
||||
"apply-lifecycle" => {
|
||||
let store = require_path_arg(&mut args, "--store")?;
|
||||
let role_id = require_string_arg(&mut args, "--role-id")?;
|
||||
let operation = read_lifecycle_arg(&require_string_arg(&mut args, "--operation")?)?;
|
||||
let result = apply_agent_memory_lifecycle_operation(&role_id, operation, store)?;
|
||||
let accepted = result.status == "accepted";
|
||||
print_json(&result)?;
|
||||
if !accepted {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
"smoke" => {
|
||||
let store = optional_path_arg(&mut args, "--store")?
|
||||
.unwrap_or_else(|| PathBuf::from("state/agents.msgpack"));
|
||||
let result = run_smoke(&store)?;
|
||||
let ok = result["ok"].as_bool().unwrap_or(false);
|
||||
print_json(&result)?;
|
||||
if !ok {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
print_usage();
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn require_path_arg(args: &mut impl Iterator<Item = String>, name: &str) -> Result<PathBuf> {
|
||||
Ok(PathBuf::from(require_string_arg(args, name)?))
|
||||
}
|
||||
|
||||
fn require_string_arg(args: &mut impl Iterator<Item = String>, name: &str) -> Result<String> {
|
||||
let Some(flag) = args.next() else {
|
||||
anyhow::bail!("missing {name}");
|
||||
};
|
||||
if flag != name {
|
||||
anyhow::bail!("expected {name}, got {flag}");
|
||||
}
|
||||
args.next()
|
||||
.with_context(|| format!("missing value for {name}"))
|
||||
}
|
||||
|
||||
fn optional_path_arg(
|
||||
args: &mut impl Iterator<Item = String>,
|
||||
name: &str,
|
||||
) -> Result<Option<PathBuf>> {
|
||||
let values: Vec<String> = args.collect();
|
||||
if values.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
if values.len() != 2 || values[0] != name {
|
||||
anyhow::bail!("expected optional {name} <path>, got {values:?}");
|
||||
}
|
||||
Ok(Some(PathBuf::from(&values[1])))
|
||||
}
|
||||
|
||||
fn read_patch_arg(value: &str) -> Result<AgentSelfPatch> {
|
||||
let path = PathBuf::from(value);
|
||||
if path.exists() {
|
||||
let raw = read_json_file_lossless(&path, "patch")?;
|
||||
return serde_json::from_str(&raw)
|
||||
.with_context(|| format!("failed to decode typed selfPatch {}", path.display()));
|
||||
}
|
||||
serde_json::from_str(value).context("failed to decode typed selfPatch JSON")
|
||||
}
|
||||
|
||||
fn read_lifecycle_arg(value: &str) -> Result<AgentMemoryLifecycleOperation> {
|
||||
let path = PathBuf::from(value);
|
||||
if path.exists() {
|
||||
let raw = read_json_file_lossless(&path, "lifecycle operation")?;
|
||||
return serde_json::from_str(&raw).with_context(|| {
|
||||
format!(
|
||||
"failed to decode typed memory lifecycle operation {}",
|
||||
path.display()
|
||||
)
|
||||
});
|
||||
}
|
||||
serde_json::from_str(value).context("failed to decode typed memory lifecycle operation JSON")
|
||||
}
|
||||
|
||||
fn read_json_file_lossless(path: &Path, label: &str) -> Result<String> {
|
||||
let raw = fs::read_to_string(path)
|
||||
.with_context(|| format!("failed to read {label} {}", path.display()))?;
|
||||
Ok(raw.trim_start_matches('\u{feff}').to_string())
|
||||
}
|
||||
|
||||
fn print_json<T: serde::Serialize>(value: &T) -> Result<()> {
|
||||
println!("{}", serde_json::to_string_pretty(value)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_usage() {
|
||||
eprintln!(
|
||||
"usage: epiphany-agent-memory-store <migrate-json-dir|project-json-dir|project-persona|set-swarm-identity|admit-legacy-mind|status|refresh-soa|repair|validate|review-patch|apply-patch|review-lifecycle|apply-lifecycle|smoke> ..."
|
||||
);
|
||||
}
|
||||
|
||||
fn run_smoke(store: &Path) -> Result<serde_json::Value> {
|
||||
let errors = validate_agent_memory_store(store)?;
|
||||
if !errors.is_empty() {
|
||||
return Ok(serde_json::json!({
|
||||
"ok": false,
|
||||
"phase": "validate",
|
||||
"errors": errors,
|
||||
}));
|
||||
}
|
||||
let accepted_patch: AgentSelfPatch = serde_json::from_value(serde_json::json!({
|
||||
"agentId": "epiphany.modeling",
|
||||
"reason": "Modeling should remember accepted graph growth must be source-grounded and bounded.",
|
||||
"semanticMemories": [{
|
||||
"memoryId": "mem-body-smoke-source-grounding",
|
||||
"summary": "A modeling self-memory request is acceptable when it improves future graph/checkpoint judgment without smuggling project truth.",
|
||||
"salience": 0.74,
|
||||
"confidence": 0.86,
|
||||
}],
|
||||
}))?;
|
||||
let accepted = review_agent_self_patch_document("modeling", &accepted_patch, store);
|
||||
let wrong_role = review_agent_self_patch_document("verification", &accepted_patch, store);
|
||||
let forbidden_patch: AgentSelfPatch = serde_json::from_value(serde_json::json!({
|
||||
"agentId": "epiphany.modeling",
|
||||
"reason": "This tries to put project state in lane memory, which should be refused.",
|
||||
"graphs": {},
|
||||
"semanticMemories": [{
|
||||
"memoryId": "mem-body-bad-project-truth",
|
||||
"summary": "Bad patch.",
|
||||
"salience": 0.5,
|
||||
"confidence": 0.5,
|
||||
}],
|
||||
}))?;
|
||||
let forbidden = review_agent_self_patch_document("modeling", &forbidden_patch, store);
|
||||
|
||||
let temp_dir = scoped_temp_dir("epiphany-agent-memory-smoke")?;
|
||||
let temp_store = temp_dir.join("agents.msgpack");
|
||||
fs::copy(store, &temp_store).with_context(|| {
|
||||
format!(
|
||||
"failed to copy {} to {}",
|
||||
store.display(),
|
||||
temp_store.display()
|
||||
)
|
||||
})?;
|
||||
let applied = apply_agent_self_patch_document("modeling", accepted_patch, &temp_store)?;
|
||||
let soa_refresh = refresh_agent_state_soa(&temp_store)?;
|
||||
let soa_status = agent_memory_status(&temp_store)?;
|
||||
let temp_validation_errors = validate_agent_memory_store(&temp_store)?;
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
|
||||
let ok = accepted.status == "accepted"
|
||||
&& wrong_role.status == "rejected"
|
||||
&& forbidden.status == "rejected"
|
||||
&& applied.status == "accepted"
|
||||
&& soa_refresh["rowCount"] == 7
|
||||
&& soa_status["persistedAgentStateSoa"]["rowCount"] == 7
|
||||
&& temp_validation_errors.is_empty()
|
||||
&& project_persona_state_for_role(store, "Persona")?["schemaVersion"]
|
||||
== epiphany_core::PERSONA_STATE_SCHEMA_VERSION;
|
||||
Ok(serde_json::json!({
|
||||
"ok": ok,
|
||||
"accepted": accepted,
|
||||
"wrongRole": wrong_role,
|
||||
"forbidden": forbidden,
|
||||
"applied": applied,
|
||||
"soaRefresh": soa_refresh,
|
||||
"soaStatus": soa_status["persistedAgentStateSoa"].clone(),
|
||||
"tempValidationErrors": temp_validation_errors,
|
||||
}))
|
||||
}
|
||||
|
||||
fn scoped_temp_dir(prefix: &str) -> Result<PathBuf> {
|
||||
let path = env::temp_dir().join(format!("{prefix}-{}", Uuid::new_v4()));
|
||||
fs::create_dir(&path).with_context(|| format!("failed to claim {}", path.display()))?;
|
||||
Ok(path)
|
||||
}
|
||||
|
|
@ -1,526 +0,0 @@
|
|||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use chrono::Utc;
|
||||
use serde_json::Map;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::BTreeSet;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const TEXTLIKE_KEYS: &[&str] = &[
|
||||
"activeTranscript",
|
||||
"body",
|
||||
"content",
|
||||
"input",
|
||||
"inputTranscript",
|
||||
"items",
|
||||
"message",
|
||||
"note",
|
||||
"output",
|
||||
"prompt",
|
||||
"raw",
|
||||
"rawResult",
|
||||
"reasoning",
|
||||
"result",
|
||||
"summary",
|
||||
"text",
|
||||
"turns",
|
||||
];
|
||||
|
||||
const SAFE_KEYS: &[&str] = &[
|
||||
"action",
|
||||
"artifactPath",
|
||||
"backendJobId",
|
||||
"bindingId",
|
||||
"changedFields",
|
||||
"cwd",
|
||||
"ephemeral",
|
||||
"expectedRevision",
|
||||
"id",
|
||||
"jobId",
|
||||
"kind",
|
||||
"level",
|
||||
"maxRuntimeSeconds",
|
||||
"method",
|
||||
"mode",
|
||||
"path",
|
||||
"recommendedAction",
|
||||
"recommendedSceneAction",
|
||||
"revision",
|
||||
"roleId",
|
||||
"source",
|
||||
"stateStatus",
|
||||
"status",
|
||||
"targetRole",
|
||||
"threadId",
|
||||
"turnId",
|
||||
"type",
|
||||
"verdict",
|
||||
];
|
||||
|
||||
const COMMAND_VERBS: &[&str] = &[
|
||||
"rg",
|
||||
"git",
|
||||
"Get-Content",
|
||||
"Get-ChildItem",
|
||||
"Select-String",
|
||||
"Test-Path",
|
||||
"New-Item",
|
||||
"Set-Content",
|
||||
"python",
|
||||
"cargo",
|
||||
"dotnet",
|
||||
"npm",
|
||||
"node",
|
||||
"apply_patch",
|
||||
];
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = parse_args()?;
|
||||
let telemetry = build_telemetry(&args.transcript)?;
|
||||
if let Some(output) = args.output {
|
||||
if let Some(parent) = output.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("failed to create {}", parent.display()))?;
|
||||
}
|
||||
fs::write(
|
||||
&output,
|
||||
format!("{}\n", serde_json::to_string_pretty(&telemetry)?),
|
||||
)
|
||||
.with_context(|| format!("failed to write {}", output.display()))?;
|
||||
}
|
||||
println!("{}", serde_json::to_string_pretty(&telemetry)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct Args {
|
||||
transcript: PathBuf,
|
||||
output: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn parse_args() -> Result<Args> {
|
||||
let mut args = env::args().skip(1);
|
||||
let Some(transcript) = args.next() else {
|
||||
return Err(anyhow!(
|
||||
"usage: epiphany-agent-telemetry <transcript> [--output <path>]"
|
||||
));
|
||||
};
|
||||
let mut output = None;
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--output" => {
|
||||
output = Some(PathBuf::from(
|
||||
args.next()
|
||||
.ok_or_else(|| anyhow!("--output requires a path"))?,
|
||||
));
|
||||
}
|
||||
_ => return Err(anyhow!("unknown argument: {arg}")),
|
||||
}
|
||||
}
|
||||
Ok(Args {
|
||||
transcript: PathBuf::from(transcript),
|
||||
output,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_telemetry(transcript_path: &Path) -> Result<Value> {
|
||||
if !transcript_path.exists() {
|
||||
return Ok(json!({
|
||||
"transcriptPath": transcript_path,
|
||||
"generatedAt": Utc::now().to_rfc3339(),
|
||||
"status": "missing",
|
||||
"events": [],
|
||||
"counts": {},
|
||||
}));
|
||||
}
|
||||
|
||||
let raw = fs::read_to_string(transcript_path)
|
||||
.with_context(|| format!("failed to read {}", transcript_path.display()))?;
|
||||
let mut events = Vec::new();
|
||||
let mut method_counts: BTreeMap<String, u64> = BTreeMap::new();
|
||||
let mut direction_counts: BTreeMap<String, u64> = BTreeMap::new();
|
||||
let mut function_counts: BTreeMap<String, u64> = BTreeMap::new();
|
||||
let mut decode_errors = Vec::new();
|
||||
|
||||
for (index, line) in raw.lines().enumerate() {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let record: Value = match serde_json::from_str(line) {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
decode_errors.push(json!({
|
||||
"index": index,
|
||||
"error": error.to_string(),
|
||||
"chars": line.chars().count(),
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let event = match record {
|
||||
Value::Object(map) => telemetry_event(index, &map),
|
||||
other => json!({
|
||||
"index": index,
|
||||
"payload": sealed_summary("record", &other),
|
||||
}),
|
||||
};
|
||||
let direction = event
|
||||
.get("direction")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
*direction_counts.entry(direction).or_default() += 1;
|
||||
if let Some(method) = event.get("method").and_then(Value::as_str) {
|
||||
*method_counts.entry(method.to_string()).or_default() += 1;
|
||||
}
|
||||
if let Some(names) = event.get("functionNames").and_then(Value::as_array) {
|
||||
for name in names.iter().filter_map(Value::as_str) {
|
||||
*function_counts.entry(name.to_string()).or_default() += 1;
|
||||
}
|
||||
}
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
let mut request_shape: BTreeMap<String, BTreeMap<String, u64>> = BTreeMap::new();
|
||||
for event in &events {
|
||||
let Some(event_id) = event.get("id") else {
|
||||
continue;
|
||||
};
|
||||
let Some(direction) = event.get("direction").and_then(Value::as_str) else {
|
||||
continue;
|
||||
};
|
||||
if direction != "sent" && direction != "received" {
|
||||
continue;
|
||||
}
|
||||
let id = value_key(event_id);
|
||||
let entry = request_shape.entry(id).or_insert_with(|| {
|
||||
BTreeMap::from([("sent".to_string(), 0), ("received".to_string(), 0)])
|
||||
});
|
||||
*entry.entry(direction.to_string()).or_default() += 1;
|
||||
}
|
||||
|
||||
Ok(json!({
|
||||
"transcriptPath": transcript_path,
|
||||
"generatedAt": Utc::now().to_rfc3339(),
|
||||
"status": "ok",
|
||||
"policy": {
|
||||
"directThoughtsSealed": true,
|
||||
"note": "Telemetry summarizes function/API shape only; text, raw results, and transcript payloads are sealed.",
|
||||
},
|
||||
"counts": {
|
||||
"events": events.len(),
|
||||
"directions": direction_counts,
|
||||
"methods": method_counts,
|
||||
"functionNames": function_counts,
|
||||
"decodeErrors": decode_errors.len(),
|
||||
},
|
||||
"requestShape": request_shape,
|
||||
"decodeErrors": decode_errors,
|
||||
"events": events,
|
||||
}))
|
||||
}
|
||||
|
||||
fn telemetry_event(index: usize, record: &Map<String, Value>) -> Value {
|
||||
let (direction, payload) = if let Some(sent) = record.get("sent") {
|
||||
("sent", sent)
|
||||
} else if let Some(received) = record.get("received") {
|
||||
("received", received)
|
||||
} else {
|
||||
("unknown", &Value::Object(record.clone()))
|
||||
};
|
||||
|
||||
let mut event = Map::new();
|
||||
event.insert("index".to_string(), json!(index));
|
||||
event.insert("direction".to_string(), json!(direction));
|
||||
|
||||
let Some(payload) = payload.as_object() else {
|
||||
event.insert("payload".to_string(), sealed_summary("payload", payload));
|
||||
return Value::Object(event);
|
||||
};
|
||||
|
||||
for key in ["id", "method"] {
|
||||
if let Some(value) = payload.get(key) {
|
||||
event.insert(key.to_string(), value.clone());
|
||||
}
|
||||
}
|
||||
if let Some(error) = payload.get("error") {
|
||||
event.insert("error".to_string(), summarize_value(error, ""));
|
||||
}
|
||||
if let Some(params) = payload.get("params") {
|
||||
event.insert("params".to_string(), summarize_value(params, "params"));
|
||||
}
|
||||
if let Some(result) = payload.get("result") {
|
||||
event.insert(
|
||||
"result".to_string(),
|
||||
summarize_value(result, "responseResult"),
|
||||
);
|
||||
}
|
||||
|
||||
let command_telemetry = collect_command_telemetry(&Value::Object(payload.clone()));
|
||||
if !command_telemetry.is_empty() {
|
||||
event.insert(
|
||||
"commandTelemetry".to_string(),
|
||||
Value::Array(command_telemetry.iter().take(16).cloned().collect()),
|
||||
);
|
||||
event.insert(
|
||||
"commandTelemetryCount".to_string(),
|
||||
json!(command_telemetry.len()),
|
||||
);
|
||||
}
|
||||
|
||||
let names = collect_strings(
|
||||
&Value::Object(payload.clone()),
|
||||
&["toolName", "tool", "functionName", "name"],
|
||||
);
|
||||
if !names.is_empty() {
|
||||
event.insert(
|
||||
"functionNames".to_string(),
|
||||
Value::Array(names.iter().take(16).map(|s| json!(s)).collect()),
|
||||
);
|
||||
event.insert("functionNameCount".to_string(), json!(names.len()));
|
||||
}
|
||||
|
||||
let paths = collect_strings(
|
||||
&Value::Object(payload.clone()),
|
||||
&["path", "cwd", "artifactPath"],
|
||||
);
|
||||
if !paths.is_empty() {
|
||||
event.insert(
|
||||
"paths".to_string(),
|
||||
Value::Array(paths.iter().take(16).map(|s| json!(s)).collect()),
|
||||
);
|
||||
event.insert("pathCount".to_string(), json!(paths.len()));
|
||||
}
|
||||
|
||||
Value::Object(event)
|
||||
}
|
||||
|
||||
fn summarize_value(value: &Value, key: &str) -> Value {
|
||||
if let Some(scalar) = scalar_summary(key, value) {
|
||||
return scalar;
|
||||
}
|
||||
if contains(TEXTLIKE_KEYS, key) {
|
||||
return sealed_summary(key, value);
|
||||
}
|
||||
match value {
|
||||
Value::Array(items) => json!({
|
||||
"kind": "list",
|
||||
"count": items.len(),
|
||||
"items": items.iter().take(8).map(|item| summarize_value(item, "")).collect::<Vec<_>>(),
|
||||
"truncated": items.len() > 8,
|
||||
}),
|
||||
Value::Object(map) => {
|
||||
let mut result = Map::new();
|
||||
for (child_key, child_value) in map {
|
||||
let summarized =
|
||||
if contains(TEXTLIKE_KEYS, child_key) && !contains(SAFE_KEYS, child_key) {
|
||||
sealed_summary(child_key, child_value)
|
||||
} else if contains(SAFE_KEYS, child_key)
|
||||
|| child_value.is_object()
|
||||
|| child_value.is_array()
|
||||
{
|
||||
summarize_value(child_value, child_key)
|
||||
} else if let Some(text) = child_value.as_str() {
|
||||
json!({"sealed": true, "kind": "text", "chars": text.chars().count()})
|
||||
} else {
|
||||
child_value.clone()
|
||||
};
|
||||
result.insert(child_key.clone(), summarized);
|
||||
}
|
||||
Value::Object(result)
|
||||
}
|
||||
other => json!({"sealed": true, "kind": value_kind(other)}),
|
||||
}
|
||||
}
|
||||
|
||||
fn scalar_summary(key: &str, value: &Value) -> Option<Value> {
|
||||
match value {
|
||||
Value::Null | Value::Bool(_) | Value::Number(_) => Some(value.clone()),
|
||||
Value::String(text) if contains(SAFE_KEYS, key) => Some(json!(text)),
|
||||
Value::String(text) => {
|
||||
Some(json!({"sealed": true, "kind": "text", "chars": text.chars().count()}))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn sealed_summary(key: &str, value: &Value) -> Value {
|
||||
let mut summary = Map::new();
|
||||
summary.insert("sealed".to_string(), json!(true));
|
||||
summary.insert("key".to_string(), json!(key));
|
||||
summary.insert(
|
||||
"reason".to_string(),
|
||||
json!("direct agent or transcript content is excluded from telemetry"),
|
||||
);
|
||||
match value {
|
||||
Value::String(text) => {
|
||||
summary.insert("chars".to_string(), json!(text.chars().count()));
|
||||
}
|
||||
Value::Array(items) => {
|
||||
summary.insert("items".to_string(), json!(items.len()));
|
||||
}
|
||||
Value::Object(map) => {
|
||||
let mut keys: Vec<_> = map.keys().map(String::as_str).collect();
|
||||
keys.sort_unstable();
|
||||
summary.insert(
|
||||
"keys".to_string(),
|
||||
json!(keys.into_iter().take(24).collect::<Vec<_>>()),
|
||||
);
|
||||
summary.insert("keyCount".to_string(), json!(map.len()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Value::Object(summary)
|
||||
}
|
||||
|
||||
fn collect_strings(value: &Value, key_names: &[&str]) -> Vec<String> {
|
||||
let mut found = BTreeSet::new();
|
||||
collect_strings_into(value, key_names, &mut found);
|
||||
found.into_iter().collect()
|
||||
}
|
||||
|
||||
fn collect_strings_into(value: &Value, key_names: &[&str], found: &mut BTreeSet<String>) {
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
for (key, item) in map {
|
||||
if contains(key_names, key) {
|
||||
if let Some(text) = item.as_str() {
|
||||
found.insert(text.to_string());
|
||||
}
|
||||
} else if item.is_object() || item.is_array() {
|
||||
collect_strings_into(item, key_names, found);
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::Array(items) => {
|
||||
for item in items {
|
||||
collect_strings_into(item, key_names, found);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_command_telemetry(value: &Value) -> Vec<Value> {
|
||||
let mut found = Vec::new();
|
||||
collect_command_telemetry_into(value, &mut found);
|
||||
found
|
||||
}
|
||||
|
||||
fn collect_command_telemetry_into(value: &Value, found: &mut Vec<Value>) {
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
if map.get("type").and_then(Value::as_str) == Some("commandExecution")
|
||||
&& let Some(command) = map.get("command").and_then(Value::as_str)
|
||||
{
|
||||
let mut info = summarize_command_text(command);
|
||||
if let Some(object) = info.as_object_mut() {
|
||||
object.insert(
|
||||
"cwd".to_string(),
|
||||
map.get("cwd").cloned().unwrap_or(Value::Null),
|
||||
);
|
||||
object.insert(
|
||||
"status".to_string(),
|
||||
map.get("status").cloned().unwrap_or(Value::Null),
|
||||
);
|
||||
object.insert(
|
||||
"exitCode".to_string(),
|
||||
map.get("exitCode").cloned().unwrap_or(Value::Null),
|
||||
);
|
||||
object.insert(
|
||||
"durationMs".to_string(),
|
||||
map.get("durationMs").cloned().unwrap_or(Value::Null),
|
||||
);
|
||||
}
|
||||
found.push(info);
|
||||
}
|
||||
for item in map
|
||||
.values()
|
||||
.filter(|item| item.is_object() || item.is_array())
|
||||
{
|
||||
collect_command_telemetry_into(item, found);
|
||||
}
|
||||
}
|
||||
Value::Array(items) => {
|
||||
for item in items {
|
||||
collect_command_telemetry_into(item, found);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn summarize_command_text(command: &str) -> Value {
|
||||
let compact = command
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let verbs = COMMAND_VERBS
|
||||
.iter()
|
||||
.filter(|verb| contains_token(command, verb))
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
let has_write_verb = verbs.iter().any(|verb| {
|
||||
["New-Item", "Set-Content", "apply_patch"]
|
||||
.iter()
|
||||
.any(|write| verb.eq_ignore_ascii_case(write))
|
||||
});
|
||||
json!({
|
||||
"chars": command.chars().count(),
|
||||
"lines": std::cmp::max(command.lines().count(), 1),
|
||||
"preview": compact.chars().take(240).collect::<String>(),
|
||||
"truncated": compact.chars().count() > 240,
|
||||
"verbs": verbs,
|
||||
"hasWriteVerb": has_write_verb,
|
||||
})
|
||||
}
|
||||
|
||||
fn contains_token(haystack: &str, needle: &str) -> bool {
|
||||
let haystack = haystack.to_lowercase();
|
||||
let needle = needle.to_lowercase();
|
||||
let bytes = haystack.as_bytes();
|
||||
let mut start = 0;
|
||||
while let Some(offset) = haystack[start..].find(&needle) {
|
||||
let idx = start + offset;
|
||||
let end = idx + needle.len();
|
||||
let before_ok = idx == 0 || !is_word(bytes[idx - 1] as char);
|
||||
let after_ok = end >= bytes.len() || !is_word(bytes[end] as char);
|
||||
if before_ok && after_ok {
|
||||
return true;
|
||||
}
|
||||
start = end;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn value_key(value: &Value) -> String {
|
||||
value
|
||||
.as_str()
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| value.to_string())
|
||||
}
|
||||
|
||||
fn value_kind(value: &Value) -> &'static str {
|
||||
match value {
|
||||
Value::Null => "null",
|
||||
Value::Bool(_) => "bool",
|
||||
Value::Number(_) => "number",
|
||||
Value::String(_) => "string",
|
||||
Value::Array(_) => "list",
|
||||
Value::Object(_) => "dict",
|
||||
}
|
||||
}
|
||||
|
||||
fn contains(keys: &[&str], key: &str) -> bool {
|
||||
keys.iter().any(|candidate| candidate == &key)
|
||||
}
|
||||
|
||||
fn is_word(ch: char) -> bool {
|
||||
ch.is_ascii_alphanumeric() || ch == '_' || ch == '-'
|
||||
}
|
||||
|
|
@ -1,918 +0,0 @@
|
|||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use chrono::SecondsFormat;
|
||||
use epiphany_core::EpiphanyAgentMemoryEntry;
|
||||
use epiphany_core::GhostlightPerceivedStateOverlay;
|
||||
use epiphany_core::GhostlightTraitVector;
|
||||
use epiphany_core::HeartbeatAgentReaction;
|
||||
use epiphany_core::HeartbeatAgentThoughtAppraisal;
|
||||
use epiphany_core::HeartbeatAppraisalReview;
|
||||
use epiphany_core::HeartbeatCandidateImplications;
|
||||
use epiphany_core::HeartbeatEmotionalAppraisal;
|
||||
use epiphany_core::HeartbeatParticipantLocalContext;
|
||||
use epiphany_core::HeartbeatPersonalityProjection;
|
||||
use epiphany_core::derive_agent_utterance_state;
|
||||
use epiphany_core::load_agent_memory_entry_for_role;
|
||||
use epiphany_core::organ_state_profile_for_role;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::BTreeSet;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use uuid::Uuid;
|
||||
|
||||
const CHARACTER_TURN_SCHEMA_VERSION: &str = "epiphany.character_turn_packet.v0";
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut args = env::args().skip(1);
|
||||
let Some(command) = args.next() else {
|
||||
return usage();
|
||||
};
|
||||
let mut role = "Persona".to_string();
|
||||
let mut agent_store = PathBuf::from("state/agents.msgpack");
|
||||
let mut artifact_dir = PathBuf::from(".epiphany-character-loop");
|
||||
let mut stimulus: Option<String> = None;
|
||||
let mut source = "epiphany/character-loop".to_string();
|
||||
let mut mode = "public-surface".to_string();
|
||||
let mut mood = "attentive".to_string();
|
||||
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--role" => role = next_value(&mut args, "--role")?,
|
||||
"--agent-store" => agent_store = next_path(&mut args, "--agent-store")?,
|
||||
"--artifact-dir" => artifact_dir = next_path(&mut args, "--artifact-dir")?,
|
||||
"--stimulus" => stimulus = Some(next_value(&mut args, "--stimulus")?),
|
||||
"--source" => source = next_value(&mut args, "--source")?,
|
||||
"--mode" => mode = next_value(&mut args, "--mode")?,
|
||||
"--mood" => mood = next_value(&mut args, "--mood")?,
|
||||
_ => return Err(anyhow!("unknown argument {arg:?}")),
|
||||
}
|
||||
}
|
||||
|
||||
let result = match command.as_str() {
|
||||
"turn" => {
|
||||
let stimulus = read_required_text(stimulus, "stimulus")?;
|
||||
run_turn(
|
||||
&role,
|
||||
&agent_store,
|
||||
&artifact_dir,
|
||||
&stimulus,
|
||||
&source,
|
||||
&mode,
|
||||
&mood,
|
||||
)?
|
||||
}
|
||||
"smoke" => run_smoke()?,
|
||||
_ => return usage(),
|
||||
};
|
||||
|
||||
let ok = result["ok"].as_bool().unwrap_or(false);
|
||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
||||
if !ok {
|
||||
std::process::exit(1);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_turn(
|
||||
role: &str,
|
||||
agent_store: &Path,
|
||||
artifact_dir: &Path,
|
||||
stimulus: &str,
|
||||
source: &str,
|
||||
mode: &str,
|
||||
mood: &str,
|
||||
) -> Result<Value> {
|
||||
if role.trim().is_empty() {
|
||||
return Err(anyhow!("--role must not be empty"));
|
||||
}
|
||||
ensure_content(stimulus, "stimulus")?;
|
||||
let entry = load_agent_memory_entry_for_role(agent_store, role)?.ok_or_else(|| {
|
||||
anyhow!(
|
||||
"{role} agent memory is missing from {}",
|
||||
agent_store.display()
|
||||
)
|
||||
})?;
|
||||
let packet = character_turn_packet(&entry, stimulus, source, mode, mood);
|
||||
let path = artifact_dir.join(format!(
|
||||
"character-turn-{}-{}-{}.json",
|
||||
sanitize_file_stem(role),
|
||||
now_stamp(),
|
||||
short_id()
|
||||
));
|
||||
write_json(&path, &packet)?;
|
||||
Ok(serde_json::json!({
|
||||
"ok": true,
|
||||
"schemaVersion": CHARACTER_TURN_SCHEMA_VERSION,
|
||||
"roleId": role,
|
||||
"agentId": entry.agent.agent_id,
|
||||
"turnPath": path,
|
||||
"turn": packet,
|
||||
}))
|
||||
}
|
||||
|
||||
fn character_turn_packet(
|
||||
entry: &EpiphanyAgentMemoryEntry,
|
||||
stimulus: &str,
|
||||
source: &str,
|
||||
mode: &str,
|
||||
mood: &str,
|
||||
) -> Value {
|
||||
let organ_state_profile = organ_state_profile_for_role(&entry.role_id);
|
||||
let utterance_state = derive_agent_utterance_state(entry, None, Some(mood), source);
|
||||
let projection_seed = deterministic_projection_seed(entry, stimulus, source, mode, mood);
|
||||
let appraisal_seed = deterministic_appraisal_seed(entry, &projection_seed, stimulus, source);
|
||||
let reaction_seed = deterministic_reaction_seed(&entry.role_id, &appraisal_seed);
|
||||
serde_json::json!({
|
||||
"schemaVersion": CHARACTER_TURN_SCHEMA_VERSION,
|
||||
"protocol": {
|
||||
"bundle": "epiphany.character_loop",
|
||||
"referenceLineage": "Ghostlight-style character-local cognition, rebuilt as Epiphany-native protocol.",
|
||||
"mode": mode,
|
||||
"roleId": entry.role_id,
|
||||
"agentId": entry.agent.agent_id,
|
||||
"organStateProfile": organ_state_profile,
|
||||
"turnKind": "stimulus_response",
|
||||
"responseContract": "Respond from projected local state and visible stimulus; do not leak hidden agent streams or invent omniscient truth.",
|
||||
},
|
||||
"source": source,
|
||||
"createdAt": now_iso(),
|
||||
"stimulus": {
|
||||
"kind": "operator_or_aquarium_input",
|
||||
"content": stimulus,
|
||||
"status": "received",
|
||||
"mood": mood,
|
||||
},
|
||||
"utteranceState": utterance_state,
|
||||
"projectedLocalContext": {
|
||||
"identity": {
|
||||
"name": entry.agent.identity.name,
|
||||
"roles": entry.agent.identity.roles,
|
||||
"origin": entry.agent.identity.origin,
|
||||
"publicDescription": entry.agent.identity.public_description,
|
||||
},
|
||||
"world": {
|
||||
"worldId": entry.world.world_id,
|
||||
"setting": entry.world.setting,
|
||||
"time": entry.world.time.label,
|
||||
"canonContext": entry.world.canon_context,
|
||||
},
|
||||
"organStateProfile": organ_state_profile_for_role(&entry.role_id),
|
||||
"activeGoals": entry.agent.goals.iter().map(|goal| serde_json::json!({
|
||||
"goalId": goal.goal_id,
|
||||
"description": goal.description,
|
||||
"scope": goal.scope,
|
||||
"priority": goal.priority,
|
||||
"emotionalStake": goal.emotional_stake,
|
||||
"status": goal.status,
|
||||
})).collect::<Vec<_>>(),
|
||||
"values": entry.agent.canonical_state.values.iter().map(|value| serde_json::json!({
|
||||
"valueId": value.value_id,
|
||||
"label": value.label,
|
||||
"priority": value.priority,
|
||||
"unforgivableIfBetrayed": value.unforgivable_if_betrayed,
|
||||
})).collect::<Vec<_>>(),
|
||||
"perceivedStateOverlays": entry.agent.perceived_state_overlays.iter().take(6).map(perceived_overlay_projection).collect::<Vec<_>>(),
|
||||
"privateNoteCount": entry.agent.identity.private_notes.len(),
|
||||
},
|
||||
"projectionSeed": projection_seed,
|
||||
"appraisalSeed": appraisal_seed,
|
||||
"reactionSeed": reaction_seed,
|
||||
"allowedOutputs": [
|
||||
"bubble",
|
||||
"discordAquariumDraft",
|
||||
"roleFinding",
|
||||
"silence",
|
||||
"selfPatch"
|
||||
],
|
||||
"cognitionLanes": {
|
||||
"schema_version": "epiphany.cognition_lanes.v0",
|
||||
"analytic": {
|
||||
"description": "Literal, evidence-facing lane: what happened, what constraints apply, what action is justified.",
|
||||
"requiredThreadShape": {
|
||||
"topic": "string",
|
||||
"claim": "string",
|
||||
"evidenceRefs": "string[]",
|
||||
"confidence": "0..1",
|
||||
"counterweight": "string"
|
||||
}
|
||||
},
|
||||
"associative": {
|
||||
"description": "Pattern-facing lane: what this rhymes with, what older seam or emotional salience may matter, what should incubate.",
|
||||
"requiredThreadShape": {
|
||||
"topic": "string",
|
||||
"claim": "string",
|
||||
"sourceThemeIds": "string[]",
|
||||
"novelty": "0..1",
|
||||
"counterweight": "string"
|
||||
}
|
||||
},
|
||||
"bridge": {
|
||||
"schema_version": "epiphany.cognition_bridge.v0",
|
||||
"contract": "Synthesize or separate the lanes, name saturation and unresolved tension, then choose bubble, draft, finding, silence, or selfPatch. The bridge may steer attention; it may not mutate project truth."
|
||||
},
|
||||
"appraisal": {
|
||||
"schema_version": "epiphany.agent_thought_appraisal.v0",
|
||||
"contract": "Project the active thought cluster through this actor's own personality vectors before reacting. Interpretation, emotion, and reaction are participant-local; state mutation still requires reviewed selfPatch or project-state acceptance."
|
||||
}
|
||||
},
|
||||
"guardrails": [
|
||||
"Humans talk to Persona; other organs expose internals through Aquarium and typed artifacts rather than becoming direct chats.",
|
||||
"Use character-local projection: the actor receives its own organ or Persona state and visible stimulus, not omniscient hidden state.",
|
||||
"Run both cognition lanes before choosing an output: analytic keeps the promise honest, associative keeps the living signal from going flat.",
|
||||
"Appraise the thought cluster through this role's personality before reacting; do not use a global mood knob as a substitute for participant-local appraisal.",
|
||||
"If speech or action would be noise, return silence plus bounded rumination or selfPatch.",
|
||||
"Posting, state mutation, objective adoption, and semantic acceptance remain separate reviewed Epiphany surfaces."
|
||||
],
|
||||
"resultShape": {
|
||||
"observedStimulus": "string",
|
||||
"localInterpretation": "string",
|
||||
"candidateBubble": "string|null",
|
||||
"candidateDraft": "string|null",
|
||||
"analyticThread": "object",
|
||||
"associativeThread": "object",
|
||||
"bridgeSynthesis": "object",
|
||||
"appraisal": "object",
|
||||
"reaction": "object",
|
||||
"decision": "bubble|draft|finding|silence|blocked",
|
||||
"selfPatch": "optional bounded role memory mutation"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct TraitSignal {
|
||||
group: &'static str,
|
||||
name: String,
|
||||
activation: f64,
|
||||
plasticity: f64,
|
||||
weight: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct CharacterProjectionSeed {
|
||||
#[serde(rename = "schema_version")]
|
||||
schema_version: String,
|
||||
participant_agent_id: String,
|
||||
role_id: String,
|
||||
thought_cluster_ref: String,
|
||||
thought_tokens: Vec<String>,
|
||||
dominant_trait_matches: Vec<HeartbeatPersonalityProjection>,
|
||||
relationship_pressure: CharacterPressureSeed,
|
||||
perceived_overlay_pressure: CharacterPressureSeed,
|
||||
contract: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct CharacterPressureSeed {
|
||||
strength: f64,
|
||||
summary: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct CharacterReactionSeed {
|
||||
#[serde(rename = "schema_version")]
|
||||
schema_version: String,
|
||||
#[serde(flatten)]
|
||||
reaction: HeartbeatAgentReaction,
|
||||
contract: String,
|
||||
}
|
||||
|
||||
fn deterministic_projection_seed(
|
||||
entry: &EpiphanyAgentMemoryEntry,
|
||||
stimulus: &str,
|
||||
source: &str,
|
||||
mode: &str,
|
||||
mood: &str,
|
||||
) -> CharacterProjectionSeed {
|
||||
let thought_tokens = stimulus_token_set(stimulus, source, mode, mood);
|
||||
let traits = collect_trait_signals(entry);
|
||||
let mut scored = traits
|
||||
.iter()
|
||||
.map(|item| {
|
||||
let trait_tokens = summary_tokens(&format!("{} {}", item.group, item.name));
|
||||
let overlap = token_overlap(&trait_tokens, &thought_tokens);
|
||||
let projection = round3((item.weight * (0.55 + overlap * 1.8)).clamp(0.0, 1.0));
|
||||
HeartbeatPersonalityProjection {
|
||||
group: item.group.to_string(),
|
||||
name: item.name.clone(),
|
||||
activation: round3(item.activation),
|
||||
plasticity: round3(item.plasticity),
|
||||
token_overlap: round3(overlap),
|
||||
projection,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
scored.sort_by(|left, right| right.projection.total_cmp(&left.projection));
|
||||
scored.truncate(6);
|
||||
|
||||
let (relationship_pressure, relationship_summary) =
|
||||
strongest_relationship_pressure(entry, &thought_tokens);
|
||||
let (overlay_pressure, overlay_summary) = strongest_overlay_pressure(entry, &thought_tokens);
|
||||
|
||||
CharacterProjectionSeed {
|
||||
schema_version: "epiphany.personality_projection.v0".to_string(),
|
||||
participant_agent_id: entry.agent.agent_id.clone(),
|
||||
role_id: entry.role_id.clone(),
|
||||
thought_cluster_ref: format!("{}#stimulus", source),
|
||||
thought_tokens: thought_tokens.into_iter().collect::<Vec<_>>(),
|
||||
dominant_trait_matches: scored,
|
||||
relationship_pressure: CharacterPressureSeed {
|
||||
strength: round3(relationship_pressure),
|
||||
summary: relationship_summary,
|
||||
},
|
||||
perceived_overlay_pressure: CharacterPressureSeed {
|
||||
strength: round3(overlay_pressure),
|
||||
summary: overlay_summary,
|
||||
},
|
||||
contract:
|
||||
"Projection is participant-local and fallible. It is a seed for embodied response, not reviewed project truth."
|
||||
.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn deterministic_appraisal_seed(
|
||||
entry: &EpiphanyAgentMemoryEntry,
|
||||
projection_seed: &CharacterProjectionSeed,
|
||||
stimulus: &str,
|
||||
source: &str,
|
||||
) -> HeartbeatAgentThoughtAppraisal {
|
||||
let traits = collect_trait_signals(entry);
|
||||
let reactivity = average(traits.iter().take(8).map(|item| item.activation)).unwrap_or(0.5);
|
||||
let plasticity = average(traits.iter().take(8).map(|item| item.plasticity)).unwrap_or(0.5);
|
||||
let expressiveness = average(
|
||||
traits
|
||||
.iter()
|
||||
.filter(|item| item.group == "voice_style" || item.group == "presentation_strategy")
|
||||
.map(|item| item.activation),
|
||||
)
|
||||
.unwrap_or(reactivity);
|
||||
let guarded_baseline = average(
|
||||
traits
|
||||
.iter()
|
||||
.filter(|item| {
|
||||
let name = item.name.as_str();
|
||||
name.contains("guard")
|
||||
|| name.contains("shame")
|
||||
|| name.contains("risk")
|
||||
|| name.contains("caution")
|
||||
|| name.contains("contingent")
|
||||
|| name.contains("defens")
|
||||
|| name.contains("susp")
|
||||
})
|
||||
.map(|item| item.activation),
|
||||
)
|
||||
.unwrap_or((1.0 - expressiveness * 0.45).clamp(0.0, 1.0));
|
||||
let alignment = projection_seed
|
||||
.dominant_trait_matches
|
||||
.first()
|
||||
.map(|item| item.projection)
|
||||
.unwrap_or(reactivity);
|
||||
let relationship_pressure = projection_seed.relationship_pressure.strength;
|
||||
let overlay_pressure = projection_seed.perceived_overlay_pressure.strength;
|
||||
let thought_pressure = (0.18
|
||||
+ (summary_tokens(stimulus).len() as f64 / 18.0).min(0.42)
|
||||
+ relationship_pressure * 0.22
|
||||
+ overlay_pressure * 0.18)
|
||||
.clamp(0.0, 1.0);
|
||||
let arousal =
|
||||
round3((thought_pressure * (0.35 + reactivity * 0.45 + plasticity * 0.2)).clamp(0.0, 1.0));
|
||||
let guardedness = round3(
|
||||
(guarded_baseline * 0.58
|
||||
+ relationship_pressure * 0.18
|
||||
+ overlay_pressure * 0.14
|
||||
+ (1.0 - alignment) * 0.10)
|
||||
.clamp(0.0, 1.0),
|
||||
);
|
||||
let curiosity =
|
||||
round3(((1.0 - guardedness) * 0.22 + alignment * 0.46 + plasticity * 0.32).clamp(0.0, 1.0));
|
||||
let urgency =
|
||||
round3((arousal * 0.58 + guardedness * 0.22 + thought_pressure * 0.20).clamp(0.0, 1.0));
|
||||
let valence = round3((0.52 + curiosity * 0.22 - guardedness * 0.20).clamp(0.0, 1.0));
|
||||
let label = interpretation_label("draft", arousal, guardedness, curiosity);
|
||||
HeartbeatAgentThoughtAppraisal {
|
||||
schema_version: "epiphany.agent_thought_appraisal.v0".to_string(),
|
||||
appraisal_id: format!("Persona-appraisal-{}", short_id()),
|
||||
review_status: "generated_unreviewed".to_string(),
|
||||
participant_agent_id: entry.agent.agent_id.clone(),
|
||||
role_id: entry.role_id.clone(),
|
||||
current_character_state_ref: format!("state/agents.msgpack#{}", entry.role_id),
|
||||
thought_cluster_ref: format!("{}#stimulus", source),
|
||||
participant_local_context: HeartbeatParticipantLocalContext {
|
||||
display_name: entry.agent.identity.name.clone(),
|
||||
values: entry
|
||||
.agent
|
||||
.canonical_state
|
||||
.values
|
||||
.iter()
|
||||
.take(5)
|
||||
.map(|value| value.label.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
reactivity: round3(reactivity),
|
||||
plasticity: round3(plasticity),
|
||||
expressiveness: round3(expressiveness),
|
||||
guardedness: round3(guarded_baseline),
|
||||
},
|
||||
observable_thought_summary: stimulus.to_string(),
|
||||
personality_projection: projection_seed.dominant_trait_matches.clone(),
|
||||
interpretation: format!(
|
||||
"{} appraises the visible stimulus through its local personality, relationship memory, and perceived overlays; this is embodied response guidance, not omniscient truth.",
|
||||
entry.agent.identity.name
|
||||
),
|
||||
emotional_appraisal: HeartbeatEmotionalAppraisal {
|
||||
valence,
|
||||
arousal,
|
||||
urgency,
|
||||
curiosity,
|
||||
guardedness,
|
||||
thought_pressure: round3(thought_pressure),
|
||||
},
|
||||
interpretation_label: label.clone(),
|
||||
confidence_notes: "Deterministic Persona-local appraisal rebuilt from the same Ghostlight-style personality projection logic used by heartbeat. Useful for reaction shape, still reviewable and fallible.".to_string(),
|
||||
candidate_implications: HeartbeatCandidateImplications {
|
||||
reaction_mode: reaction_mode(&label, "draft").to_string(),
|
||||
reaction_intensity: round3(
|
||||
(urgency * 0.55 + arousal * 0.3 + curiosity * 0.15).clamp(0.0, 1.0),
|
||||
),
|
||||
should_speak: entry.role_id == "Persona" && guardedness < 0.78,
|
||||
should_incubate: guardedness >= 0.55,
|
||||
},
|
||||
review: HeartbeatAppraisalReview {
|
||||
accepted_for_mutation: false,
|
||||
rationale:
|
||||
"Appraisal may steer expression and reaction; state mutation still requires explicit review."
|
||||
.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn deterministic_reaction_seed(
|
||||
role_id: &str,
|
||||
appraisal_seed: &HeartbeatAgentThoughtAppraisal,
|
||||
) -> CharacterReactionSeed {
|
||||
let arousal = appraisal_seed.emotional_appraisal.arousal;
|
||||
let guardedness = appraisal_seed.emotional_appraisal.guardedness;
|
||||
let curiosity = appraisal_seed.emotional_appraisal.curiosity;
|
||||
let mode = appraisal_seed.candidate_implications.reaction_mode.as_str();
|
||||
let intensity = appraisal_seed.candidate_implications.reaction_intensity;
|
||||
CharacterReactionSeed {
|
||||
schema_version: "epiphany.agent_reaction.v0".to_string(),
|
||||
reaction: HeartbeatAgentReaction {
|
||||
reaction_id: format!("reaction-{}-{}", role_id, short_id()),
|
||||
role_id: role_id.to_string(),
|
||||
participant_agent_id: appraisal_seed.participant_agent_id.clone(),
|
||||
appraisal_id: appraisal_seed.appraisal_id.clone(),
|
||||
mode: mode.to_string(),
|
||||
mood_label: mood_label(arousal, guardedness, curiosity).to_string(),
|
||||
intensity: round3(intensity),
|
||||
bridge_decision: "draft".to_string(),
|
||||
surface: if role_id == "Persona" {
|
||||
"aquarium"
|
||||
} else {
|
||||
"internal"
|
||||
}
|
||||
.to_string(),
|
||||
recommended_use: reaction_recommended_use(role_id, mode).to_string(),
|
||||
},
|
||||
contract: "Reaction is derived from local appraisal and may color speech or silence; it still does not mutate durable state by itself.".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_trait_signals(entry: &EpiphanyAgentMemoryEntry) -> Vec<TraitSignal> {
|
||||
let mut traits = Vec::new();
|
||||
collect_trait_group(
|
||||
&mut traits,
|
||||
"underlying_organization",
|
||||
&entry.agent.canonical_state.underlying_organization,
|
||||
);
|
||||
collect_trait_group(
|
||||
&mut traits,
|
||||
"stable_dispositions",
|
||||
&entry.agent.canonical_state.stable_dispositions,
|
||||
);
|
||||
collect_trait_group(
|
||||
&mut traits,
|
||||
"behavioral_dimensions",
|
||||
&entry.agent.canonical_state.behavioral_dimensions,
|
||||
);
|
||||
collect_trait_group(
|
||||
&mut traits,
|
||||
"presentation_strategy",
|
||||
&entry.agent.canonical_state.presentation_strategy,
|
||||
);
|
||||
collect_trait_group(
|
||||
&mut traits,
|
||||
"voice_style",
|
||||
&entry.agent.canonical_state.voice_style,
|
||||
);
|
||||
collect_trait_group(
|
||||
&mut traits,
|
||||
"situational_state",
|
||||
&entry.agent.canonical_state.situational_state,
|
||||
);
|
||||
traits.sort_by(|left, right| right.weight.total_cmp(&left.weight));
|
||||
traits
|
||||
}
|
||||
|
||||
fn collect_trait_group(
|
||||
traits: &mut Vec<TraitSignal>,
|
||||
group: &'static str,
|
||||
source: &BTreeMap<String, GhostlightTraitVector>,
|
||||
) {
|
||||
for (name, vector) in source {
|
||||
let activation = vector.current_activation.clamp(0.0, 1.0);
|
||||
let plasticity = vector.plasticity.clamp(0.0, 1.0);
|
||||
traits.push(TraitSignal {
|
||||
group,
|
||||
name: name.clone(),
|
||||
activation,
|
||||
plasticity,
|
||||
weight: round3(activation * (0.65 + plasticity * 0.35)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn strongest_relationship_pressure(
|
||||
entry: &EpiphanyAgentMemoryEntry,
|
||||
thought_tokens: &BTreeSet<String>,
|
||||
) -> (f64, String) {
|
||||
entry
|
||||
.agent
|
||||
.memories
|
||||
.relationship_summaries
|
||||
.iter()
|
||||
.map(|memory| {
|
||||
let overlap = token_overlap(&summary_tokens(&memory.summary), thought_tokens);
|
||||
let strength = overlap * (memory.salience * memory.confidence);
|
||||
(strength, memory.summary.clone())
|
||||
})
|
||||
.max_by(|left, right| left.0.total_cmp(&right.0))
|
||||
.map(|(strength, summary)| (round3(strength.clamp(0.0, 1.0)), summary))
|
||||
.unwrap_or((
|
||||
0.0,
|
||||
"No strong relationship pressure is active.".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn strongest_overlay_pressure(
|
||||
entry: &EpiphanyAgentMemoryEntry,
|
||||
thought_tokens: &BTreeSet<String>,
|
||||
) -> (f64, String) {
|
||||
entry
|
||||
.agent
|
||||
.perceived_state_overlays
|
||||
.iter()
|
||||
.map(|overlay| {
|
||||
let tokens = overlay_tokens(overlay);
|
||||
let overlap = token_overlap(&tokens, thought_tokens);
|
||||
let summary = overlay_summary(overlay);
|
||||
(overlap, summary)
|
||||
})
|
||||
.max_by(|left, right| left.0.total_cmp(&right.0))
|
||||
.map(|(strength, summary)| (round3(strength.clamp(0.0, 1.0)), summary))
|
||||
.unwrap_or((
|
||||
0.0,
|
||||
"No perceived overlay is tugging on this stimulus.".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn stimulus_token_set(stimulus: &str, source: &str, mode: &str, mood: &str) -> BTreeSet<String> {
|
||||
let mut tokens = BTreeSet::new();
|
||||
tokens.extend(summary_tokens(stimulus));
|
||||
tokens.extend(summary_tokens(source));
|
||||
tokens.extend(summary_tokens(mode));
|
||||
tokens.extend(summary_tokens(mood));
|
||||
tokens
|
||||
}
|
||||
|
||||
fn perceived_overlay_projection(overlay: &GhostlightPerceivedStateOverlay) -> Value {
|
||||
serde_json::json!({
|
||||
"overlayId": overlay.overlay_id,
|
||||
"label": overlay.label,
|
||||
"summary": overlay.summary,
|
||||
"source": overlay.source,
|
||||
"salience": overlay.salience,
|
||||
"confidence": overlay.confidence,
|
||||
"linkedMemoryIds": overlay.linked_memory_ids,
|
||||
})
|
||||
}
|
||||
|
||||
fn overlay_tokens(overlay: &GhostlightPerceivedStateOverlay) -> BTreeSet<String> {
|
||||
let mut tokens = BTreeSet::new();
|
||||
tokens.extend(summary_tokens(&overlay.overlay_id));
|
||||
tokens.extend(summary_tokens(&overlay.label));
|
||||
tokens.extend(summary_tokens(&overlay.summary));
|
||||
tokens.extend(summary_tokens(&overlay.source));
|
||||
for memory_id in &overlay.linked_memory_ids {
|
||||
tokens.extend(summary_tokens(memory_id));
|
||||
}
|
||||
tokens
|
||||
}
|
||||
|
||||
fn overlay_summary(overlay: &GhostlightPerceivedStateOverlay) -> String {
|
||||
if !overlay.summary.trim().is_empty() {
|
||||
overlay.summary.clone()
|
||||
} else if !overlay.label.trim().is_empty() {
|
||||
overlay.label.clone()
|
||||
} else {
|
||||
"Perceived overlay relevance".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn summary_tokens(text: &str) -> BTreeSet<String> {
|
||||
text.split(|ch: char| !ch.is_ascii_alphanumeric() && ch != '_' && ch != '-')
|
||||
.filter_map(|item| {
|
||||
let token = item.trim().to_ascii_lowercase().replace('_', "-");
|
||||
(token.len() >= 3).then_some(token)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn token_overlap(left: &BTreeSet<String>, right: &BTreeSet<String>) -> f64 {
|
||||
if left.is_empty() || right.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let shared = left.intersection(right).count() as f64;
|
||||
let denom = left.len().min(right.len()) as f64;
|
||||
if denom <= f64::EPSILON {
|
||||
0.0
|
||||
} else {
|
||||
(shared / denom).clamp(0.0, 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn average<I>(values: I) -> Option<f64>
|
||||
where
|
||||
I: Iterator<Item = f64>,
|
||||
{
|
||||
let mut total = 0.0_f64;
|
||||
let mut count = 0_usize;
|
||||
for value in values {
|
||||
total += value;
|
||||
count += 1;
|
||||
}
|
||||
(count > 0).then_some(total / count as f64)
|
||||
}
|
||||
|
||||
fn interpretation_label(
|
||||
bridge_decision: &str,
|
||||
arousal: f64,
|
||||
guardedness: f64,
|
||||
curiosity: f64,
|
||||
) -> String {
|
||||
if guardedness > 0.72 && arousal > 0.35 {
|
||||
"protective_appraisal".to_string()
|
||||
} else if curiosity > 0.68 {
|
||||
"investigative_appraisal".to_string()
|
||||
} else if bridge_decision == "draft" {
|
||||
"expressive_appraisal".to_string()
|
||||
} else if arousal < 0.12 {
|
||||
"low_pressure_appraisal".to_string()
|
||||
} else {
|
||||
"incubating_appraisal".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn reaction_mode(label: &str, bridge_decision: &str) -> &'static str {
|
||||
match (label, bridge_decision) {
|
||||
("protective_appraisal", _) => "hold_and_verify",
|
||||
("investigative_appraisal", _) => "inspect",
|
||||
("expressive_appraisal", "draft") => "draft",
|
||||
("low_pressure_appraisal", _) => "sleep_ruminate",
|
||||
_ => "incubate",
|
||||
}
|
||||
}
|
||||
|
||||
fn mood_label(arousal: f64, guardedness: f64, curiosity: f64) -> &'static str {
|
||||
if guardedness > 0.72 && arousal > 0.35 {
|
||||
"wary"
|
||||
} else if curiosity > 0.68 && arousal > 0.25 {
|
||||
"keen"
|
||||
} else if arousal < 0.12 {
|
||||
"drowsy"
|
||||
} else if guardedness > curiosity {
|
||||
"watchful"
|
||||
} else {
|
||||
"interested"
|
||||
}
|
||||
}
|
||||
|
||||
fn reaction_recommended_use(role_id: &str, mode: &str) -> &'static str {
|
||||
match (role_id, mode) {
|
||||
("Persona", "draft") => {
|
||||
"Prepare a reviewed Aquarium-facing draft; do not post automatically."
|
||||
}
|
||||
(_, "hold_and_verify") => "Bias toward verifier/modeler review before expression.",
|
||||
(_, "inspect") => {
|
||||
"Bias the next heartbeat toward a bounded retrieval or modeling inspection."
|
||||
}
|
||||
(_, "sleep_ruminate") => "Let this organ sleep-ruminate unless real work arrives.",
|
||||
_ => "Keep the thought incubating and visible in Aquarium.",
|
||||
}
|
||||
}
|
||||
|
||||
fn round3(value: f64) -> f64 {
|
||||
(value.clamp(0.0, 1.0) * 1000.0).round() / 1000.0
|
||||
}
|
||||
|
||||
fn run_smoke() -> Result<Value> {
|
||||
let temp_dir = scoped_temp_dir("epiphany-character-loop-smoke")?;
|
||||
let artifact_dir = temp_dir.join("artifacts");
|
||||
let result = run_turn(
|
||||
"Persona",
|
||||
Path::new("state/agents.msgpack"),
|
||||
&artifact_dir,
|
||||
"Aquarium hover selected Persona and asked what the swarm is feeling.",
|
||||
"smoke/character-loop",
|
||||
"public-surface",
|
||||
"attentive",
|
||||
)?;
|
||||
let path = PathBuf::from(
|
||||
result["turnPath"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow!("smoke result missing turnPath"))?,
|
||||
);
|
||||
let packet: Value = serde_json::from_str(
|
||||
&fs::read_to_string(&path)
|
||||
.with_context(|| format!("failed to read smoke artifact {}", path.display()))?,
|
||||
)?;
|
||||
const EXPECTED_PACKET_FIELDS: [&str; 14] = [
|
||||
"allowedOutputs",
|
||||
"appraisalSeed",
|
||||
"cognitionLanes",
|
||||
"createdAt",
|
||||
"guardrails",
|
||||
"projectedLocalContext",
|
||||
"projectionSeed",
|
||||
"protocol",
|
||||
"reactionSeed",
|
||||
"resultShape",
|
||||
"schemaVersion",
|
||||
"source",
|
||||
"stimulus",
|
||||
"utteranceState",
|
||||
];
|
||||
let ok = packet.as_object().is_some_and(|payload| {
|
||||
payload.len() == EXPECTED_PACKET_FIELDS.len()
|
||||
&& EXPECTED_PACKET_FIELDS
|
||||
.iter()
|
||||
.all(|key| payload.contains_key(*key))
|
||||
}) && packet["schemaVersion"] == CHARACTER_TURN_SCHEMA_VERSION
|
||||
&& packet["protocol"]["bundle"] == "epiphany.character_loop"
|
||||
&& packet["protocol"]["roleId"] == "Persona"
|
||||
&& packet["protocol"]["organStateProfile"]["profileKind"] == "persona"
|
||||
&& packet["projectedLocalContext"]["organStateProfile"]["profileKind"] == "persona"
|
||||
&& packet["utteranceState"]["schemaVersion"] == "epiphany.agent_utterance_state.v0"
|
||||
&& packet["utteranceState"]["identity"]["name"]
|
||||
== packet["projectedLocalContext"]["identity"]["name"]
|
||||
&& packet["utteranceState"]["personalityVectors"]["voiceStyle"].is_object()
|
||||
&& packet["utteranceState"]["characterStateVector"]["compatibleHandoffSchema"]
|
||||
== "weksa.utterance_embedding_handoff.v0.1"
|
||||
&& packet["utteranceState"]["characterStateVector"]["values"]
|
||||
.as_array()
|
||||
.is_some_and(|values| values.len() == 64)
|
||||
&& packet["projectedLocalContext"]
|
||||
.get("semanticMemories")
|
||||
.is_none()
|
||||
&& packet["projectedLocalContext"]
|
||||
.get("episodicMemories")
|
||||
.is_none()
|
||||
&& packet["projectedLocalContext"]
|
||||
.get("relationshipMemories")
|
||||
.is_none()
|
||||
&& packet["projectionSeed"]["schema_version"] == "epiphany.personality_projection.v0"
|
||||
&& packet["appraisalSeed"]["schema_version"] == "epiphany.agent_thought_appraisal.v0"
|
||||
&& packet["reactionSeed"]["schema_version"] == "epiphany.agent_reaction.v0"
|
||||
&& packet["projectedLocalContext"]["identity"]["name"]
|
||||
.as_str()
|
||||
.is_some_and(|name| !name.trim().is_empty())
|
||||
&& packet["stimulus"]["content"]
|
||||
.as_str()
|
||||
.is_some_and(|text| text.contains("Aquarium"))
|
||||
&& packet["stimulus"]["status"] == "received"
|
||||
&& packet["utteranceState"]["activation"]["status"] == "unknown"
|
||||
&& packet["cognitionLanes"]["schema_version"] == "epiphany.cognition_lanes.v0"
|
||||
&& packet["cognitionLanes"]["bridge"]["schema_version"] == "epiphany.cognition_bridge.v0"
|
||||
&& packet["cognitionLanes"]["appraisal"]["schema_version"]
|
||||
== "epiphany.agent_thought_appraisal.v0";
|
||||
Ok(serde_json::json!({
|
||||
"ok": ok,
|
||||
"turnPath": path,
|
||||
"packet": {
|
||||
"schemaVersion": packet["schemaVersion"],
|
||||
"stimulusStatus": packet["stimulus"]["status"],
|
||||
"activationStatus": packet["utteranceState"]["activation"]["status"],
|
||||
"bundle": packet["protocol"]["bundle"],
|
||||
"roleId": packet["protocol"]["roleId"],
|
||||
"agentId": packet["protocol"]["agentId"],
|
||||
"organStateProfile": packet["protocol"]["organStateProfile"]["profileKind"],
|
||||
"projectionSchema": packet["projectionSeed"]["schema_version"],
|
||||
"appraisalSchema": packet["appraisalSeed"]["schema_version"],
|
||||
"reactionSchema": packet["reactionSeed"]["schema_version"],
|
||||
"utteranceStateSchema": packet["utteranceState"]["schemaVersion"],
|
||||
"identityName": packet["projectedLocalContext"]["identity"]["name"],
|
||||
"allowedOutputs": packet["allowedOutputs"],
|
||||
"cognitionLanes": packet["cognitionLanes"]["schema_version"],
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn read_required_text(content: Option<String>, label: &str) -> Result<String> {
|
||||
let Some(content) = content else {
|
||||
let mut stdin = String::new();
|
||||
std::io::stdin()
|
||||
.read_to_string(&mut stdin)
|
||||
.context("failed to read stdin")?;
|
||||
ensure_content(&stdin, label)?;
|
||||
return Ok(stdin);
|
||||
};
|
||||
let path = PathBuf::from(&content);
|
||||
if path.exists() {
|
||||
let text = fs::read_to_string(&path)
|
||||
.with_context(|| format!("failed to read content file {}", path.display()))?;
|
||||
ensure_content(&text, label)?;
|
||||
return Ok(text);
|
||||
}
|
||||
ensure_content(&content, label)?;
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
fn ensure_content(content: &str, label: &str) -> Result<()> {
|
||||
if content.trim().is_empty() {
|
||||
return Err(anyhow!("{label} is empty"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn next_value(args: &mut impl Iterator<Item = String>, flag: &str) -> Result<String> {
|
||||
args.next()
|
||||
.ok_or_else(|| anyhow!("missing value after {flag}"))
|
||||
}
|
||||
|
||||
fn next_path(args: &mut impl Iterator<Item = String>, flag: &str) -> Result<PathBuf> {
|
||||
Ok(PathBuf::from(next_value(args, flag)?))
|
||||
}
|
||||
|
||||
fn write_json(path: &Path, payload: &Value) -> Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("failed to create {}", parent.display()))?;
|
||||
}
|
||||
fs::write(
|
||||
path,
|
||||
format!("{}\n", serde_json::to_string_pretty(payload)?),
|
||||
)
|
||||
.with_context(|| format!("failed to write {}", path.display()))
|
||||
}
|
||||
|
||||
fn now_iso() -> String {
|
||||
chrono::Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true)
|
||||
}
|
||||
|
||||
fn now_stamp() -> String {
|
||||
chrono::Utc::now().format("%Y%m%dT%H%M%SZ").to_string()
|
||||
}
|
||||
|
||||
fn short_id() -> String {
|
||||
Uuid::new_v4()
|
||||
.to_string()
|
||||
.split('-')
|
||||
.next()
|
||||
.unwrap_or("id")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn sanitize_file_stem(value: &str) -> String {
|
||||
value
|
||||
.chars()
|
||||
.map(|ch| {
|
||||
if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
|
||||
ch
|
||||
} else {
|
||||
'-'
|
||||
}
|
||||
})
|
||||
.collect::<String>()
|
||||
.trim_matches('-')
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn scoped_temp_dir(prefix: &str) -> Result<PathBuf> {
|
||||
let dir = env::temp_dir().join(format!("{prefix}-{}", Uuid::new_v4()));
|
||||
fs::create_dir(&dir).with_context(|| format!("failed to claim {}", dir.display()))?;
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
fn usage() -> Result<()> {
|
||||
Err(anyhow!(
|
||||
"usage: epiphany-character-loop <turn|smoke> [--role <role>] [--agent-store <path>] [--artifact-dir <path>] [--stimulus <text-or-path>] [--source <source>] [--mode <mode>]"
|
||||
))
|
||||
}
|
||||
|
|
@ -1,299 +0,0 @@
|
|||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use chrono::Utc;
|
||||
use epiphany_core::EPIPHANY_CULTMESH_DAEMON_HEARTBEAT_EVENT_SCHEMA_VERSION;
|
||||
use epiphany_core::EPIPHANY_CULTMESH_DAEMON_STATUS_SCHEMA_VERSION;
|
||||
use epiphany_core::EpiphanyCultMeshDaemonHeartbeatEventEntry;
|
||||
use epiphany_core::EpiphanyCultMeshDaemonStatusEntry;
|
||||
use epiphany_core::load_epiphany_cultmesh_cluster_topology;
|
||||
use epiphany_core::write_epiphany_cultmesh_daemon_heartbeat_event;
|
||||
use epiphany_core::write_epiphany_cultmesh_daemon_status;
|
||||
use serde_json::json;
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = Args::parse()?;
|
||||
match args.command.as_str() {
|
||||
"heartbeat" | "status" | "once" => {
|
||||
let written = write_heartbeat(&args, 0)?;
|
||||
print_output(&args, "heartbeat", vec![written])
|
||||
}
|
||||
"serve" | "loop" | "daemon" => serve(args),
|
||||
other => anyhow::bail!("unknown command {other:?}; use heartbeat or serve"),
|
||||
}
|
||||
}
|
||||
|
||||
fn serve(args: Args) -> Result<()> {
|
||||
let mut written = Vec::new();
|
||||
let mut iteration = 0_u64;
|
||||
loop {
|
||||
written.push(write_heartbeat(&args, iteration)?);
|
||||
iteration = iteration.saturating_add(1);
|
||||
if args.max_iterations > 0 && iteration >= args.max_iterations {
|
||||
break;
|
||||
}
|
||||
thread::sleep(Duration::from_secs(args.interval_seconds));
|
||||
}
|
||||
print_output(&args, "serveComplete", written)
|
||||
}
|
||||
|
||||
fn write_heartbeat(
|
||||
args: &Args,
|
||||
iteration: u64,
|
||||
) -> Result<EpiphanyCultMeshDaemonHeartbeatEventEntry> {
|
||||
let topology = load_epiphany_cultmesh_cluster_topology(&args.store, args.runtime_id.clone())?;
|
||||
let cluster = topology
|
||||
.into_iter()
|
||||
.find(|cluster| cluster.daemon_id == args.daemon_id)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"local Verse has no persisted topology for {}; run explicit bootstrap before starting cluster daemons",
|
||||
args.daemon_id
|
||||
)
|
||||
})?;
|
||||
let heartbeat_at = Utc::now().to_rfc3339();
|
||||
let heartbeat = new_heartbeat_event(
|
||||
args,
|
||||
&cluster.daemon_id,
|
||||
&cluster.cluster_id,
|
||||
iteration,
|
||||
heartbeat_at,
|
||||
);
|
||||
let written = write_epiphany_cultmesh_daemon_heartbeat_event(
|
||||
&args.store,
|
||||
args.runtime_id.clone(),
|
||||
heartbeat,
|
||||
)?;
|
||||
|
||||
// Compatibility sight for readers not yet migrated to heartbeat events. It is
|
||||
// derived after the provider-owned event and cannot mint heartbeat authority.
|
||||
write_epiphany_cultmesh_daemon_status(
|
||||
&args.store,
|
||||
args.runtime_id.clone(),
|
||||
EpiphanyCultMeshDaemonStatusEntry {
|
||||
schema_version: EPIPHANY_CULTMESH_DAEMON_STATUS_SCHEMA_VERSION.to_string(),
|
||||
daemon_id: cluster.daemon_id,
|
||||
cluster_id: cluster.cluster_id,
|
||||
body_domain: cluster.body_domain,
|
||||
daemon_surface_id: cluster.daemon_surface_id,
|
||||
eve_surface_id: cluster.eve_surface_id,
|
||||
status: written.status.clone(),
|
||||
last_heartbeat_utc: written.heartbeat_at.clone(),
|
||||
supported_actions: vec![
|
||||
"inspectStatus".to_string(),
|
||||
"pokeDaemon".to_string(),
|
||||
"watchHeartbeat".to_string(),
|
||||
],
|
||||
operator_action: "none".to_string(),
|
||||
private_state_exposed: false,
|
||||
notes: vec![
|
||||
"Display-only compatibility projection derived from the provider heartbeat event."
|
||||
.to_string(),
|
||||
format!("Source heartbeat id: {}", written.heartbeat_id),
|
||||
],
|
||||
},
|
||||
)?;
|
||||
Ok(written)
|
||||
}
|
||||
|
||||
fn new_heartbeat_event(
|
||||
args: &Args,
|
||||
daemon_id: &str,
|
||||
cluster_id: &str,
|
||||
iteration: u64,
|
||||
heartbeat_at: String,
|
||||
) -> EpiphanyCultMeshDaemonHeartbeatEventEntry {
|
||||
EpiphanyCultMeshDaemonHeartbeatEventEntry {
|
||||
schema_version: EPIPHANY_CULTMESH_DAEMON_HEARTBEAT_EVENT_SCHEMA_VERSION.to_string(),
|
||||
heartbeat_id: Uuid::new_v4().to_string(),
|
||||
daemon_id: daemon_id.to_string(),
|
||||
cluster_id: cluster_id.to_string(),
|
||||
provider_incarnation: args.provider_incarnation.clone(),
|
||||
sequence: iteration.saturating_add(1),
|
||||
status: "ready".to_string(),
|
||||
heartbeat_at,
|
||||
private_state_exposed: false,
|
||||
startup_lifecycle_receipt_id: args.startup_lifecycle_receipt_id.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_output(
|
||||
args: &Args,
|
||||
status: &str,
|
||||
written: Vec<EpiphanyCultMeshDaemonHeartbeatEventEntry>,
|
||||
) -> Result<()> {
|
||||
let latest = written
|
||||
.last()
|
||||
.context("cluster daemon wrote no heartbeat status")?;
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"schemaVersion": "epiphany.cluster_daemon_run.v0",
|
||||
"status": status,
|
||||
"store": args.store,
|
||||
"runtimeId": args.runtime_id,
|
||||
"daemonId": latest.daemon_id,
|
||||
"clusterId": latest.cluster_id,
|
||||
"daemonStatus": latest.status,
|
||||
"heartbeatId": latest.heartbeat_id,
|
||||
"providerIncarnation": latest.provider_incarnation,
|
||||
"sequence": latest.sequence,
|
||||
"lastHeartbeatUtc": latest.heartbeat_at,
|
||||
"heartbeatCount": written.len(),
|
||||
"intervalSeconds": args.interval_seconds,
|
||||
"maxIterations": args.max_iterations,
|
||||
"privateStateExposed": false,
|
||||
}))?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct Args {
|
||||
command: String,
|
||||
store: PathBuf,
|
||||
runtime_id: String,
|
||||
daemon_id: String,
|
||||
interval_seconds: u64,
|
||||
max_iterations: u64,
|
||||
provider_incarnation: String,
|
||||
startup_lifecycle_receipt_id: String,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
fn parse() -> Result<Self> {
|
||||
let mut values = env::args().skip(1);
|
||||
let command = values.next().unwrap_or_else(|| "heartbeat".to_string());
|
||||
let mut store = PathBuf::from(".epiphany-run/cultmesh/local-verse.ccmp");
|
||||
let mut runtime_id = "epiphany-local".to_string();
|
||||
let mut daemon_id = None;
|
||||
let mut interval_seconds = 60_u64;
|
||||
let mut max_iterations = 1_u64;
|
||||
let mut provider_incarnation = None;
|
||||
let mut startup_lifecycle_receipt_id =
|
||||
env::var("EPIPHANY_STARTUP_LIFECYCLE_RECEIPT_ID").unwrap_or_default();
|
||||
|
||||
while let Some(arg) = values.next() {
|
||||
match arg.as_str() {
|
||||
"--store" => store = PathBuf::from(values.next().context("missing --store value")?),
|
||||
"--runtime-id" => {
|
||||
runtime_id = values.next().context("missing --runtime-id value")?
|
||||
}
|
||||
"--daemon-id" => {
|
||||
daemon_id = Some(values.next().context("missing --daemon-id value")?)
|
||||
}
|
||||
"--interval-seconds" => {
|
||||
interval_seconds = values
|
||||
.next()
|
||||
.context("missing --interval-seconds value")?
|
||||
.parse()?;
|
||||
}
|
||||
"--max-iterations" => {
|
||||
max_iterations = values
|
||||
.next()
|
||||
.context("missing --max-iterations value")?
|
||||
.parse()?;
|
||||
}
|
||||
"--provider-incarnation" => {
|
||||
provider_incarnation = Some(
|
||||
values
|
||||
.next()
|
||||
.context("missing --provider-incarnation value")?,
|
||||
)
|
||||
}
|
||||
"--startup-lifecycle-receipt-id" => {
|
||||
startup_lifecycle_receipt_id = values
|
||||
.next()
|
||||
.context("missing --startup-lifecycle-receipt-id value")?
|
||||
}
|
||||
other => anyhow::bail!("unknown argument {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
let daemon_id = daemon_id.context("cluster daemon requires --daemon-id")?;
|
||||
Ok(Self {
|
||||
command,
|
||||
store,
|
||||
runtime_id,
|
||||
daemon_id,
|
||||
interval_seconds,
|
||||
max_iterations,
|
||||
provider_incarnation: provider_incarnation
|
||||
.unwrap_or_else(|| Uuid::new_v4().to_string()),
|
||||
startup_lifecycle_receipt_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use epiphany_core::query_epiphany_local_verse_context;
|
||||
use epiphany_core::seed_epiphany_local_verse_context;
|
||||
|
||||
#[test]
|
||||
fn process_heartbeat_events_keep_one_incarnation_and_increment_sequence() {
|
||||
let args = Args {
|
||||
command: "serve".to_string(),
|
||||
store: PathBuf::from("unused.ccmp"),
|
||||
runtime_id: "runtime-test".to_string(),
|
||||
daemon_id: "daemon-test".to_string(),
|
||||
interval_seconds: 1,
|
||||
max_iterations: 2,
|
||||
provider_incarnation: "incarnation-test".to_string(),
|
||||
startup_lifecycle_receipt_id: "receipt-test".to_string(),
|
||||
};
|
||||
let first = new_heartbeat_event(
|
||||
&args,
|
||||
"daemon-test",
|
||||
"cluster-test",
|
||||
0,
|
||||
"2026-07-15T12:00:00Z".to_string(),
|
||||
);
|
||||
let second = new_heartbeat_event(
|
||||
&args,
|
||||
"daemon-test",
|
||||
"cluster-test",
|
||||
1,
|
||||
"2026-07-15T12:00:01Z".to_string(),
|
||||
);
|
||||
assert_eq!(first.provider_incarnation, second.provider_incarnation);
|
||||
assert_eq!(first.sequence, 1);
|
||||
assert_eq!(second.sequence, 2);
|
||||
assert_ne!(first.heartbeat_id, second.heartbeat_id);
|
||||
assert_eq!(first.startup_lifecycle_receipt_id, "receipt-test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heartbeat_writes_liveness_without_minting_provider_truth() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let store = temp.path().join("heartbeat-only.ccmp");
|
||||
seed_epiphany_local_verse_context(
|
||||
&store,
|
||||
"runtime-test",
|
||||
"2026-07-15T12:00:00Z",
|
||||
"repo:C:/fixture/Epiphany",
|
||||
)?;
|
||||
let args = Args {
|
||||
command: "heartbeat".to_string(),
|
||||
store: store.clone(),
|
||||
runtime_id: "runtime-test".to_string(),
|
||||
daemon_id: "epiphany-daemon-hands".to_string(),
|
||||
interval_seconds: 1,
|
||||
max_iterations: 1,
|
||||
provider_incarnation: "incarnation-test".to_string(),
|
||||
startup_lifecycle_receipt_id: "receipt-test".to_string(),
|
||||
};
|
||||
|
||||
write_heartbeat(&args, 0)?;
|
||||
let context = query_epiphany_local_verse_context(&store, "runtime-test")?;
|
||||
assert_eq!(context.daemon_statuses.len(), 1);
|
||||
assert!(context.odin_advertisements.is_empty());
|
||||
assert!(context.eve_surface_states.is_empty());
|
||||
assert!(context.daemon_tool_capabilities.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use epiphany_core::EPIPHANY_CULTMESH_INTERNAL_TIER;
|
||||
use epiphany_core::EPIPHANY_CULTMESH_INTERNAL_VERSE_ID;
|
||||
use epiphany_core::EPIPHANY_CULTMESH_STATUS_SCHEMA_VERSION;
|
||||
use epiphany_core::EpiphanyCultMeshStatusEntry;
|
||||
use epiphany_core::epiphany_cultmesh_operator_snapshot_from_status_json;
|
||||
use epiphany_core::load_epiphany_cultmesh_status;
|
||||
use epiphany_core::load_latest_epiphany_cultmesh_operator_snapshot;
|
||||
use epiphany_core::write_epiphany_cultmesh_continuity_contracts;
|
||||
use epiphany_core::write_epiphany_cultmesh_eyes_contracts;
|
||||
use epiphany_core::write_epiphany_cultmesh_global_room_policies;
|
||||
use epiphany_core::write_epiphany_cultmesh_hands_contracts;
|
||||
use epiphany_core::write_epiphany_cultmesh_mind_contracts;
|
||||
use epiphany_core::write_epiphany_cultmesh_operator_snapshot;
|
||||
use epiphany_core::write_epiphany_cultmesh_soul_contracts;
|
||||
use epiphany_core::write_epiphany_cultmesh_status;
|
||||
use epiphany_core::write_epiphany_cultmesh_substrate_gate_contracts;
|
||||
use epiphany_core::write_epiphany_cultmesh_verse_policies;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut store = PathBuf::from(".epiphany-smoke");
|
||||
store.push("cultmesh");
|
||||
std::fs::create_dir_all(&store)?;
|
||||
store.push("epiphany-local.ccmp");
|
||||
|
||||
let status = EpiphanyCultMeshStatusEntry {
|
||||
schema_version: EPIPHANY_CULTMESH_STATUS_SCHEMA_VERSION.to_string(),
|
||||
runtime_id: "epiphany-cultmesh-smoke".to_string(),
|
||||
verse_id: EPIPHANY_CULTMESH_INTERNAL_VERSE_ID.to_string(),
|
||||
verse_tier: EPIPHANY_CULTMESH_INTERNAL_TIER.to_string(),
|
||||
app_id: "epiphany".to_string(),
|
||||
note: "Epiphany wrote this through CultMesh, not direct CultNet plumbing.".to_string(),
|
||||
};
|
||||
|
||||
write_epiphany_cultmesh_status(&store, status.clone())?;
|
||||
write_epiphany_cultmesh_verse_policies(&store, "epiphany-cultmesh-smoke")?;
|
||||
write_epiphany_cultmesh_global_room_policies(&store, "epiphany-cultmesh-smoke")?;
|
||||
write_epiphany_cultmesh_mind_contracts(&store, "epiphany-cultmesh-smoke")?;
|
||||
write_epiphany_cultmesh_substrate_gate_contracts(&store, "epiphany-cultmesh-smoke")?;
|
||||
write_epiphany_cultmesh_eyes_contracts(&store, "epiphany-cultmesh-smoke")?;
|
||||
write_epiphany_cultmesh_hands_contracts(&store, "epiphany-cultmesh-smoke")?;
|
||||
write_epiphany_cultmesh_soul_contracts(&store, "epiphany-cultmesh-smoke")?;
|
||||
write_epiphany_cultmesh_continuity_contracts(&store, "epiphany-cultmesh-smoke")?;
|
||||
let operator_snapshot = epiphany_cultmesh_operator_snapshot_from_status_json(
|
||||
"epiphany-cultmesh-smoke",
|
||||
"cultmesh-smoke-status",
|
||||
"2026-05-27T00:00:00Z",
|
||||
"status",
|
||||
".epiphany-smoke/cultmesh/status.json",
|
||||
&serde_json::json!({
|
||||
"threadId": "thread-smoke",
|
||||
"scene": {
|
||||
"scene": {
|
||||
"stateStatus": "missing",
|
||||
"availableActions": ["crrc", "roles"]
|
||||
}
|
||||
},
|
||||
"pressure": {
|
||||
"pressure": {
|
||||
"level": "low"
|
||||
}
|
||||
},
|
||||
"reorient": {
|
||||
"decision": {
|
||||
"action": "regather",
|
||||
"nextAction": "Regather source context."
|
||||
}
|
||||
},
|
||||
"crrc": {
|
||||
"recommendation": {
|
||||
"action": "regatherManually"
|
||||
}
|
||||
},
|
||||
"coordinator": {
|
||||
"action": "wait"
|
||||
}
|
||||
}),
|
||||
)?;
|
||||
write_epiphany_cultmesh_operator_snapshot(&store, operator_snapshot.clone())?;
|
||||
let loaded = load_epiphany_cultmesh_status(&store, "epiphany-cultmesh-smoke")?;
|
||||
if loaded != Some(status) {
|
||||
anyhow::bail!("CultMesh smoke failed to round-trip Epiphany status document");
|
||||
}
|
||||
let loaded_operator_snapshot =
|
||||
load_latest_epiphany_cultmesh_operator_snapshot(&store, "epiphany-cultmesh-smoke")?;
|
||||
if loaded_operator_snapshot != Some(operator_snapshot) {
|
||||
anyhow::bail!("CultMesh smoke failed to round-trip Epiphany operator snapshot document");
|
||||
}
|
||||
|
||||
println!(
|
||||
"epiphany cultmesh smoke ok: {}",
|
||||
store.canonicalize()?.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -209,28 +209,6 @@ fn dispatch(args: Args) -> Result<()> {
|
|||
managed_service_reconcile(args)
|
||||
}
|
||||
"service-runbook" | "runbook-service" => service_runbook(args),
|
||||
"repo-work-service-audit"
|
||||
| "repo-work-service-readiness"
|
||||
| "repo-work-queue-runner-audit" => repo_work_service_audit(args),
|
||||
"cluster-service-runbook" | "cluster-daemon-runbook" => cluster_daemon_runbook(args),
|
||||
"cluster-service-install-plan"
|
||||
| "cluster-daemon-install-plan"
|
||||
| "cluster-service-install-execute" => refuse_false_windows_scm_authority(),
|
||||
"cluster-windows-service-audit"
|
||||
| "cluster-service-audit"
|
||||
| "cluster-service-readiness"
|
||||
| "cluster-windows-service-start"
|
||||
| "cluster-service-start"
|
||||
| "cluster-windows-service-stop"
|
||||
| "cluster-service-stop" => refuse_false_windows_scm_authority(),
|
||||
"cluster-windows-service-execution-readiness"
|
||||
| "cluster-service-execution-readiness"
|
||||
| "cluster-windows-service-execution-runbook"
|
||||
| "cluster-service-execution-runbook"
|
||||
| "cluster-windows-service-execution-audit"
|
||||
| "cluster-service-execution-audit" => refuse_false_windows_scm_authority(),
|
||||
"cluster-windows-service-execution-audit-smoke"
|
||||
| "cluster-service-execution-audit-smoke" => refuse_false_windows_scm_authority(),
|
||||
"service-install-plan" | "service-install-execute" => refuse_false_windows_scm_authority(),
|
||||
"windows-service-execution-readiness"
|
||||
| "service-execution-readiness"
|
||||
|
|
@ -256,7 +234,7 @@ fn dispatch(args: Args) -> Result<()> {
|
|||
"policy" | "write-policy" => write_policy(args),
|
||||
"semantic-recover" => semantic_recover(args),
|
||||
other => anyhow::bail!(
|
||||
"unknown command {other:?}; use reconcile, tick, serve, managed-service-serve, managed-service-task-plan/install/status/start/stop/uninstall, service-plan, service-launch, service-runbook, managed-service-policy, semantic-projector-service-policy, repo-work-service-audit, cluster-service-runbook, policy, or semantic-recover"
|
||||
"unknown command {other:?}; use reconcile, tick, serve, managed-service-serve, managed-service-task-plan/install/status/start/stop/uninstall, service-plan, service-launch, service-runbook, managed-service-policy, semantic-projector-service-policy, policy, or semantic-recover"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
@ -2014,25 +1992,6 @@ fn workspace_coverage_projector_service_policy(mut args: Args) -> Result<()> {
|
|||
}
|
||||
|
||||
fn packaged_role_command_path(args: &Args, role: &str) -> Result<PathBuf> {
|
||||
#[cfg(feature = "workspace-coverage-recovery-smoke")]
|
||||
if args.release_id.is_none()
|
||||
&& std::env::var_os("EPIPHANY_WORKSPACE_COVERAGE_SMOKE_DIAGNOSTICS").is_some()
|
||||
{
|
||||
let file_name = match role {
|
||||
"semantic-projector" if cfg!(windows) => "epiphany-memory-semantic-projector.exe",
|
||||
"semantic-projector" => "epiphany-memory-semantic-projector",
|
||||
"workspace-coverage-projector" if cfg!(windows) => {
|
||||
"epiphany-workspace-coverage-projector.exe"
|
||||
}
|
||||
"workspace-coverage-projector" => "epiphany-workspace-coverage-projector",
|
||||
_ => anyhow::bail!("unknown smoke-only packaged role {role}"),
|
||||
};
|
||||
let path = env::current_exe()?.with_file_name(file_name);
|
||||
if !path.is_file() {
|
||||
anyhow::bail!("smoke-only sibling is absent: {}", path.display());
|
||||
}
|
||||
return Ok(path);
|
||||
}
|
||||
let (release, _) = pinned_packaged_release(args, true)?;
|
||||
epiphany_packaged_release_binary_path(&release, role)
|
||||
}
|
||||
|
|
@ -3142,270 +3101,6 @@ fn service_runbook(args: Args) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn repo_work_service_audit(args: Args) -> Result<()> {
|
||||
require_supervisor_bootstrap(&args)?;
|
||||
|
||||
let brake = load_epiphany_cultmesh_swarm_brake(&args.store, args.runtime_id.clone())?;
|
||||
assert_swarm_brake_allows_service_lifecycle_entry(brake.as_ref())?;
|
||||
let started_at = Utc::now();
|
||||
let receipts =
|
||||
load_epiphany_cultmesh_daemon_service_lifecycle_receipts(&args.store, &args.runtime_id)?;
|
||||
let service_receipts = receipts
|
||||
.iter()
|
||||
.filter(|receipt| receipt.service_id == args.service_id)
|
||||
.collect::<Vec<_>>();
|
||||
let plan = latest_service_lifecycle_receipt(&service_receipts, "install-plan");
|
||||
let runbook = latest_service_lifecycle_receipt(&service_receipts, "runbook");
|
||||
let launch = latest_service_lifecycle_receipt(&service_receipts, "launch");
|
||||
|
||||
let mut missing_checks = Vec::new();
|
||||
let mut failed_checks = Vec::new();
|
||||
|
||||
let plan_status = receipt_status_or_missing(plan);
|
||||
if plan.is_none() {
|
||||
missing_checks.push("install-plan".to_string());
|
||||
} else if plan_status != "planned" {
|
||||
failed_checks.push(format!("install-plan status {plan_status}"));
|
||||
}
|
||||
|
||||
let runbook_status = receipt_status_or_missing(runbook);
|
||||
if runbook.is_none() {
|
||||
missing_checks.push("runbook".to_string());
|
||||
} else if runbook_status != "written" {
|
||||
failed_checks.push(format!("runbook status {runbook_status}"));
|
||||
}
|
||||
let runbook_ref = runbook
|
||||
.map(|receipt| receipt.operator_artifact_ref.as_str())
|
||||
.unwrap_or("none");
|
||||
let runbook_artifact_status = service_artifact_status(runbook_ref);
|
||||
let runbook_sha256 = local_file_sha256(runbook_ref).unwrap_or_else(|| "none".to_string());
|
||||
if runbook.is_some() && runbook_artifact_status != "present" {
|
||||
failed_checks.push(format!("runbook artifact {runbook_artifact_status}"));
|
||||
}
|
||||
|
||||
let launch_status = receipt_status_or_missing(launch);
|
||||
let launch_exit_code = launch.and_then(|receipt| receipt.exit_code);
|
||||
if launch.is_none() {
|
||||
missing_checks.push("launch".to_string());
|
||||
} else {
|
||||
if launch_status != "completed" {
|
||||
failed_checks.push(format!("launch status {launch_status}"));
|
||||
}
|
||||
if launch_exit_code != Some(0) {
|
||||
failed_checks.push(format!(
|
||||
"launch exit code {}",
|
||||
launch_exit_code
|
||||
.map(|code| code.to_string())
|
||||
.unwrap_or_else(|| "none".to_string())
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let private_state_exposed = service_receipts
|
||||
.iter()
|
||||
.any(|receipt| receipt.private_state_exposed);
|
||||
if private_state_exposed {
|
||||
failed_checks.push("private state exposed".to_string());
|
||||
}
|
||||
|
||||
let status = if missing_checks.is_empty() && failed_checks.is_empty() {
|
||||
"complete"
|
||||
} else {
|
||||
"incomplete"
|
||||
};
|
||||
let next_safe_move = if missing_checks.iter().any(|check| check == "install-plan")
|
||||
|| failed_checks
|
||||
.iter()
|
||||
.any(|check| check.starts_with("install-plan "))
|
||||
{
|
||||
"tools/epiphany_local_run.ps1 -Mode repo-work-service-plan"
|
||||
} else if missing_checks.iter().any(|check| check == "runbook")
|
||||
|| failed_checks
|
||||
.iter()
|
||||
.any(|check| check.starts_with("runbook "))
|
||||
{
|
||||
"tools/epiphany_local_run.ps1 -Mode repo-work-service-runbook"
|
||||
} else if missing_checks.iter().any(|check| check == "launch")
|
||||
|| failed_checks
|
||||
.iter()
|
||||
.any(|check| check.starts_with("launch "))
|
||||
{
|
||||
"tools/epiphany_local_run.ps1 -Mode repo-work-service-launch"
|
||||
} else {
|
||||
"continue repo-swarm MVP planner/interpreter hardening"
|
||||
};
|
||||
|
||||
let receipt = service_lifecycle_receipt(
|
||||
&args,
|
||||
"repo-work-service-audit",
|
||||
status,
|
||||
"repo-work-service-audit".to_string(),
|
||||
vec![
|
||||
format!("plan={plan_status}"),
|
||||
format!("runbook={runbook_status}"),
|
||||
format!("runbookArtifact={runbook_artifact_status}"),
|
||||
format!("launch={launch_status}"),
|
||||
format!(
|
||||
"launchExitCode={}",
|
||||
launch_exit_code
|
||||
.map(|code| code.to_string())
|
||||
.unwrap_or_else(|| "none".to_string())
|
||||
),
|
||||
format!("missing={}", missing_checks.len()),
|
||||
format!("failed={}", failed_checks.len()),
|
||||
],
|
||||
None,
|
||||
None,
|
||||
started_at,
|
||||
Some(Utc::now()),
|
||||
Some(format!(
|
||||
"service://{}/repo-work-service-audit",
|
||||
sanitize_id(&args.service_id)
|
||||
)),
|
||||
);
|
||||
let written = write_epiphany_cultmesh_daemon_service_lifecycle_receipt(
|
||||
&args.store,
|
||||
args.runtime_id.clone(),
|
||||
receipt,
|
||||
)?;
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"schemaVersion": "epiphany.repo_work_service_audit.v0",
|
||||
"status": status,
|
||||
"store": args.store,
|
||||
"runtimeId": args.runtime_id,
|
||||
"serviceId": written.service_id,
|
||||
"schedulerId": written.scheduler_id,
|
||||
"receiptId": written.receipt_id,
|
||||
"planStatus": plan_status,
|
||||
"runbookStatus": runbook_status,
|
||||
"runbookArtifactStatus": runbook_artifact_status,
|
||||
"runbookArtifactRef": runbook_ref,
|
||||
"runbookSha256": runbook_sha256,
|
||||
"launchStatus": launch_status,
|
||||
"launchExitCode": launch_exit_code,
|
||||
"missingChecks": missing_checks,
|
||||
"failedChecks": failed_checks,
|
||||
"lifecycleOwner": "Idunn",
|
||||
"hostedBody": "repo-work",
|
||||
"mutatesServiceManager": false,
|
||||
"requiresElevatedAuthority": false,
|
||||
"privateStateExposed": private_state_exposed,
|
||||
"nextSafeMove": next_safe_move,
|
||||
}))?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn latest_service_lifecycle_receipt<'a>(
|
||||
receipts: &'a [&EpiphanyCultMeshDaemonServiceLifecycleReceiptEntry],
|
||||
action: &str,
|
||||
) -> Option<&'a EpiphanyCultMeshDaemonServiceLifecycleReceiptEntry> {
|
||||
receipts
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|receipt| receipt.action == action)
|
||||
.max_by_key(|receipt| {
|
||||
receipt
|
||||
.completed_at_utc
|
||||
.as_deref()
|
||||
.unwrap_or(&receipt.started_at_utc)
|
||||
})
|
||||
}
|
||||
|
||||
fn receipt_status_or_missing(
|
||||
receipt: Option<&EpiphanyCultMeshDaemonServiceLifecycleReceiptEntry>,
|
||||
) -> String {
|
||||
receipt
|
||||
.map(|receipt| receipt.status.clone())
|
||||
.unwrap_or_else(|| "missing".to_string())
|
||||
}
|
||||
|
||||
fn service_artifact_status(artifact_ref: &str) -> &'static str {
|
||||
if artifact_ref.trim().is_empty() || artifact_ref == "none" {
|
||||
return "none";
|
||||
}
|
||||
let path = Path::new(artifact_ref);
|
||||
if path.is_file() {
|
||||
return "present";
|
||||
}
|
||||
if artifact_ref.contains("://") {
|
||||
return "external-ref";
|
||||
}
|
||||
"missing"
|
||||
}
|
||||
|
||||
fn cluster_daemon_runbook(args: Args) -> Result<()> {
|
||||
require_supervisor_bootstrap(&args)?;
|
||||
|
||||
let context = query_epiphany_local_verse_context(&args.store, args.runtime_id.clone())?;
|
||||
assert_swarm_brake_allows_service_lifecycle(&context)?;
|
||||
let started_at = Utc::now();
|
||||
let command_path = cluster_daemon_command_path(&args)?;
|
||||
let daemon_rows = cluster_daemon_runbook_rows(&args, &context)?;
|
||||
let runbook_path = service_runbook_path(&args);
|
||||
if let Some(parent) = runbook_path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("failed to create {}", parent.display()))?;
|
||||
}
|
||||
fs::write(
|
||||
&runbook_path,
|
||||
cluster_daemon_runbook_content(&args, &command_path, &daemon_rows),
|
||||
)
|
||||
.with_context(|| format!("failed to write {}", runbook_path.display()))?;
|
||||
let artifact_ref = runbook_path.display().to_string();
|
||||
let service_args = daemon_rows
|
||||
.iter()
|
||||
.flat_map(|row| {
|
||||
let mut values = vec![format!("daemon={}", row.daemon_id)];
|
||||
values.extend(row.args.clone());
|
||||
values
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let receipt = service_lifecycle_receipt(
|
||||
&args,
|
||||
"cluster-runbook",
|
||||
"written",
|
||||
command_path.display().to_string(),
|
||||
service_args,
|
||||
None,
|
||||
None,
|
||||
started_at,
|
||||
Some(Utc::now()),
|
||||
Some(artifact_ref.clone()),
|
||||
);
|
||||
let written = write_epiphany_cultmesh_daemon_service_lifecycle_receipt(
|
||||
&args.store,
|
||||
args.runtime_id.clone(),
|
||||
receipt,
|
||||
)?;
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"status": "written",
|
||||
"store": args.store,
|
||||
"runtimeId": args.runtime_id,
|
||||
"serviceId": written.service_id,
|
||||
"receiptId": written.receipt_id,
|
||||
"daemonSelector": written.daemon_selector,
|
||||
"daemonCount": daemon_rows.len(),
|
||||
"runbookPath": artifact_ref,
|
||||
"command": written.command,
|
||||
"daemons": daemon_rows.iter().map(|row| json!({
|
||||
"daemonId": row.daemon_id,
|
||||
"clusterId": row.cluster_id,
|
||||
"displayName": row.display_name,
|
||||
"observedStatus": row.observed_status,
|
||||
"args": row.args,
|
||||
"privateStateExposed": false,
|
||||
})).collect::<Vec<_>>(),
|
||||
"privateStateExposed": written.private_state_exposed,
|
||||
}))?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn local_file_sha256(path: &str) -> Option<String> {
|
||||
if path.trim().is_empty() || path == "none" {
|
||||
return None;
|
||||
|
|
@ -3951,20 +3646,6 @@ fn service_command_path(args: &Args) -> Result<PathBuf> {
|
|||
env::current_exe().context("failed to resolve current supervisor executable")
|
||||
}
|
||||
|
||||
fn cluster_daemon_command_path(args: &Args) -> Result<PathBuf> {
|
||||
if let Some(command) = args.service_command.as_ref() {
|
||||
return Ok(command.clone());
|
||||
}
|
||||
let daemon_binary = if cfg!(windows) {
|
||||
"epiphany-cluster-daemon.exe"
|
||||
} else {
|
||||
"epiphany-cluster-daemon"
|
||||
};
|
||||
env::current_exe()
|
||||
.map(|path| path.with_file_name(daemon_binary))
|
||||
.context("failed to resolve cluster daemon executable")
|
||||
}
|
||||
|
||||
fn service_serve_args(args: &Args) -> Vec<String> {
|
||||
if !args.service_args.is_empty() {
|
||||
return args.service_args.clone();
|
||||
|
|
@ -4000,29 +3681,6 @@ fn service_serve_args(args: &Args) -> Vec<String> {
|
|||
service_args
|
||||
}
|
||||
|
||||
fn cluster_daemon_serve_args(args: &Args, daemon_id: &str) -> Vec<String> {
|
||||
let mut service_args = vec![
|
||||
"serve".to_string(),
|
||||
"--store".to_string(),
|
||||
args.store.display().to_string(),
|
||||
"--runtime-id".to_string(),
|
||||
args.runtime_id.clone(),
|
||||
"--daemon-id".to_string(),
|
||||
daemon_id.to_string(),
|
||||
"--interval-seconds".to_string(),
|
||||
args.loop_interval_seconds.to_string(),
|
||||
"--max-iterations".to_string(),
|
||||
args.max_iterations.to_string(),
|
||||
"--note".to_string(),
|
||||
"Cluster daemon service runbook heartbeat.".to_string(),
|
||||
];
|
||||
if let Some(reason) = &args.reason {
|
||||
service_args.push("--note".to_string());
|
||||
service_args.push(reason.clone());
|
||||
}
|
||||
service_args
|
||||
}
|
||||
|
||||
fn service_runbook_path(args: &Args) -> PathBuf {
|
||||
if let Some(path) = args.runbook_path.as_ref() {
|
||||
return path.clone();
|
||||
|
|
@ -4032,44 +3690,6 @@ fn service_runbook_path(args: &Args) -> PathBuf {
|
|||
.join(format!("{}.ps1", sanitize_id(&args.service_id)))
|
||||
}
|
||||
|
||||
fn cluster_daemon_runbook_rows(
|
||||
args: &Args,
|
||||
context: &EpiphanyLocalVerseContext,
|
||||
) -> Result<Vec<ClusterDaemonRunbookRow>> {
|
||||
let requested_daemon = args.daemon_id.as_str();
|
||||
let mut rows = Vec::new();
|
||||
for cluster in &context.cluster_topology {
|
||||
if requested_daemon != "*" && cluster.daemon_id != requested_daemon {
|
||||
continue;
|
||||
}
|
||||
let observed_status = context
|
||||
.daemon_statuses
|
||||
.iter()
|
||||
.find(|status| status.daemon_id == cluster.daemon_id)
|
||||
.map(|status| status.status.clone())
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"local Verse has topology for {} but no daemon status row",
|
||||
cluster.daemon_id
|
||||
)
|
||||
})?;
|
||||
rows.push(ClusterDaemonRunbookRow {
|
||||
daemon_id: cluster.daemon_id.clone(),
|
||||
cluster_id: cluster.cluster_id.clone(),
|
||||
display_name: cluster.display_name.clone(),
|
||||
observed_status,
|
||||
args: cluster_daemon_serve_args(args, &cluster.daemon_id),
|
||||
});
|
||||
}
|
||||
if rows.is_empty() {
|
||||
anyhow::bail!(
|
||||
"local Verse has no cluster daemon topology rows matching {:?}",
|
||||
requested_daemon
|
||||
);
|
||||
}
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
fn service_runbook_content(
|
||||
args: &Args,
|
||||
command_path: &std::path::Path,
|
||||
|
|
@ -4105,60 +3725,10 @@ fn service_runbook_content(
|
|||
)
|
||||
}
|
||||
|
||||
fn cluster_daemon_runbook_content(
|
||||
args: &Args,
|
||||
command_path: &std::path::Path,
|
||||
daemon_rows: &[ClusterDaemonRunbookRow],
|
||||
) -> String {
|
||||
let cwd = args
|
||||
.cwd
|
||||
.as_ref()
|
||||
.map(|path| path.display().to_string())
|
||||
.unwrap_or_else(|| ".".to_string());
|
||||
let mut lines = vec![
|
||||
"# Epiphany cluster daemon service runbook".to_string(),
|
||||
"# Generated by epiphany-daemon-supervisor cluster-service-runbook.".to_string(),
|
||||
"# The typed lifecycle receipt is the witness; this script is an operator artifact."
|
||||
.to_string(),
|
||||
"$ErrorActionPreference = 'Stop'".to_string(),
|
||||
format!(
|
||||
"$command = {}",
|
||||
quote_powershell(&command_path.display().to_string())
|
||||
),
|
||||
format!("$workingDirectory = {}", quote_powershell(&cwd)),
|
||||
"$processes = @()".to_string(),
|
||||
];
|
||||
for row in daemon_rows {
|
||||
let quoted_args = row
|
||||
.args
|
||||
.iter()
|
||||
.map(|arg| quote_powershell(arg))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
lines.push(format!("# {} / {}", row.display_name, row.daemon_id));
|
||||
lines.push(format!("$arguments = @({quoted_args})"));
|
||||
lines.push("$processes += Start-Process -FilePath $command -ArgumentList $arguments -WorkingDirectory $workingDirectory -WindowStyle Hidden -PassThru".to_string());
|
||||
}
|
||||
lines.push(format!(
|
||||
"\"started cluster-daemon-count=$($processes.Count) service={} scheduler={}\"",
|
||||
args.service_id, args.scheduler_id
|
||||
));
|
||||
lines.push(String::new());
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
fn quote_powershell(value: &str) -> String {
|
||||
format!("'{}'", value.replace('\'', "''"))
|
||||
}
|
||||
|
||||
struct ClusterDaemonRunbookRow {
|
||||
daemon_id: String,
|
||||
cluster_id: String,
|
||||
display_name: String,
|
||||
observed_status: String,
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
fn service_lifecycle_receipt(
|
||||
args: &Args,
|
||||
action: &str,
|
||||
|
|
@ -4314,10 +3884,6 @@ fn assert_swarm_brake_allows_scheduler_tick(context: &EpiphanyLocalVerseContext)
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn assert_swarm_brake_allows_service_lifecycle(context: &EpiphanyLocalVerseContext) -> Result<()> {
|
||||
assert_swarm_brake_allows_service_lifecycle_entry(context.swarm_brake.as_ref())
|
||||
}
|
||||
|
||||
fn assert_swarm_brake_allows_service_lifecycle_entry(
|
||||
brake: Option<&EpiphanyCultMeshSwarmBrakeEntry>,
|
||||
) -> Result<()> {
|
||||
|
|
@ -4664,7 +4230,6 @@ mod semantic_projector_authority_tests {
|
|||
"managed_service_task_install",
|
||||
"managed_service_task_operation",
|
||||
"service_runbook",
|
||||
"repo_work_service_audit",
|
||||
] {
|
||||
let marker = format!("fn {function}(");
|
||||
let start = source.find(&marker).unwrap();
|
||||
|
|
@ -5047,10 +4612,6 @@ mod semantic_projector_authority_tests {
|
|||
"service-start",
|
||||
"service-stop",
|
||||
"service-execution-audit",
|
||||
"cluster-service-install-execute",
|
||||
"cluster-service-start",
|
||||
"cluster-service-stop",
|
||||
"cluster-service-execution-audit",
|
||||
] {
|
||||
let dispatch = source
|
||||
.find(&format!("\"{command}\""))
|
||||
|
|
@ -5664,30 +5225,6 @@ impl Args {
|
|||
| "service-desired-state-reconcile"
|
||||
| "service-runbook"
|
||||
| "runbook-service"
|
||||
| "repo-work-service-audit"
|
||||
| "repo-work-service-readiness"
|
||||
| "repo-work-queue-runner-audit"
|
||||
| "cluster-service-runbook"
|
||||
| "cluster-daemon-runbook"
|
||||
| "cluster-windows-service-install"
|
||||
| "cluster-service-install-plan"
|
||||
| "cluster-service-install-execute"
|
||||
| "cluster-daemon-install-plan"
|
||||
| "cluster-windows-service-audit"
|
||||
| "cluster-service-audit"
|
||||
| "cluster-service-readiness"
|
||||
| "cluster-windows-service-start"
|
||||
| "cluster-service-start"
|
||||
| "cluster-windows-service-stop"
|
||||
| "cluster-service-stop"
|
||||
| "cluster-windows-service-execution-readiness"
|
||||
| "cluster-service-execution-readiness"
|
||||
| "cluster-windows-service-execution-runbook"
|
||||
| "cluster-service-execution-runbook"
|
||||
| "cluster-windows-service-execution-audit"
|
||||
| "cluster-service-execution-audit"
|
||||
| "cluster-windows-service-execution-audit-smoke"
|
||||
| "cluster-service-execution-audit-smoke"
|
||||
| "windows-service-install"
|
||||
| "service-install-windows"
|
||||
| "service-install-plan"
|
||||
|
|
|
|||
|
|
@ -1,479 +0,0 @@
|
|||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use chrono::Utc;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = Args::parse()?;
|
||||
let result = run_smoke(args)?;
|
||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Args {
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
fn parse() -> Result<Self> {
|
||||
let mut root = env::current_dir().context("failed to resolve current directory")?;
|
||||
let mut args = env::args().skip(1).peekable();
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--root" => root = take_path(&mut args, "--root")?,
|
||||
other => return Err(anyhow!("unexpected argument {other:?}")),
|
||||
}
|
||||
}
|
||||
Ok(Self { root })
|
||||
}
|
||||
}
|
||||
|
||||
fn run_smoke(args: Args) -> Result<Value> {
|
||||
let root = args
|
||||
.root
|
||||
.canonicalize()
|
||||
.with_context(|| format!("failed to resolve {}", args.root.display()))?;
|
||||
let smoke_root = root.join(".epiphany-smoke");
|
||||
let manifest = root.join("epiphany-core").join("Cargo.toml");
|
||||
if !manifest.exists() {
|
||||
return Err(anyhow!(
|
||||
"could not find epiphany-core manifest at {}",
|
||||
manifest.display()
|
||||
));
|
||||
}
|
||||
fs::create_dir_all(&smoke_root)
|
||||
.with_context(|| format!("failed to create {}", smoke_root.display()))?;
|
||||
let stamp = Utc::now().format("%Y%m%d-%H%M%S").to_string();
|
||||
let smoke_dir = smoke_root.join(format!("daemon-survival-rehearsal-{stamp}"));
|
||||
fs::create_dir(&smoke_dir)
|
||||
.with_context(|| format!("failed to claim fresh smoke dir {}", smoke_dir.display()))?;
|
||||
|
||||
let local_verse = smoke_dir.join("local-verse.ccmp");
|
||||
let runtime_id = "epiphany-local";
|
||||
let daemon_id = "epiphany-daemon-self";
|
||||
let scheduler_id = "epiphany-daemon-survival-rehearsal";
|
||||
let restart_command = if cfg!(windows) {
|
||||
"powershell.exe"
|
||||
} else {
|
||||
"sh"
|
||||
};
|
||||
let restart_args = if cfg!(windows) {
|
||||
vec!["-NoProfile", "-Command", "exit 0"]
|
||||
} else {
|
||||
vec!["-c", "exit 0"]
|
||||
};
|
||||
|
||||
cargo_json(
|
||||
&manifest,
|
||||
"epiphany-verse-query",
|
||||
&[
|
||||
"seed",
|
||||
"--store",
|
||||
path_str(&local_verse)?,
|
||||
"--runtime-id",
|
||||
runtime_id,
|
||||
],
|
||||
&root,
|
||||
)?;
|
||||
cargo_json(
|
||||
&manifest,
|
||||
"epiphany-cluster-daemon",
|
||||
&[
|
||||
"heartbeat",
|
||||
"--store",
|
||||
path_str(&local_verse)?,
|
||||
"--runtime-id",
|
||||
runtime_id,
|
||||
"--daemon-id",
|
||||
daemon_id,
|
||||
],
|
||||
&root,
|
||||
)?;
|
||||
|
||||
let mut policy_args = vec![
|
||||
"policy",
|
||||
"--store",
|
||||
path_str(&local_verse)?,
|
||||
"--runtime-id",
|
||||
runtime_id,
|
||||
"--daemon-id",
|
||||
daemon_id,
|
||||
"--scheduler-id",
|
||||
scheduler_id,
|
||||
"--restart-command",
|
||||
restart_command,
|
||||
"--cwd",
|
||||
path_str(&root)?,
|
||||
"--cooldown-seconds",
|
||||
"0",
|
||||
"--reconcile-interval-seconds",
|
||||
"0",
|
||||
"--heartbeat-stale-seconds",
|
||||
"2",
|
||||
]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
for arg in &restart_args {
|
||||
policy_args.push("--restart-arg".to_string());
|
||||
policy_args.push((*arg).to_string());
|
||||
}
|
||||
let policy = cargo_json(
|
||||
&manifest,
|
||||
"epiphany-daemon-supervisor",
|
||||
&string_refs(&policy_args),
|
||||
&root,
|
||||
)?;
|
||||
let provider_before = cargo_json(
|
||||
&manifest,
|
||||
"epiphany-verse-query",
|
||||
&[
|
||||
"swarm-status",
|
||||
"--store",
|
||||
path_str(&local_verse)?,
|
||||
"--runtime-id",
|
||||
runtime_id,
|
||||
],
|
||||
&root,
|
||||
)?;
|
||||
|
||||
let serve_args = vec![
|
||||
"serve",
|
||||
"--store",
|
||||
path_str(&local_verse)?,
|
||||
"--runtime-id",
|
||||
runtime_id,
|
||||
"--daemon-id",
|
||||
daemon_id,
|
||||
"--scheduler-id",
|
||||
scheduler_id,
|
||||
"--loop-interval-seconds",
|
||||
"0",
|
||||
"--max-iterations",
|
||||
"2",
|
||||
]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
std::thread::sleep(std::time::Duration::from_secs(3));
|
||||
let serve = cargo_json(
|
||||
&manifest,
|
||||
"epiphany-daemon-supervisor",
|
||||
&string_refs(&serve_args),
|
||||
&root,
|
||||
)?;
|
||||
|
||||
require_eq(&policy, &["status"], "written")?;
|
||||
require_eq(&policy, &["daemonId"], daemon_id)?;
|
||||
require_bool(&policy, &["privateStateExposed"], false)?;
|
||||
require_eq(&serve, &["status"], "serveComplete")?;
|
||||
require_u64(&serve, &["iterations"], 2)?;
|
||||
require_bool(&serve, &["privateStateExposed"], false)?;
|
||||
let outputs = serve
|
||||
.get("outputs")
|
||||
.and_then(Value::as_array)
|
||||
.ok_or_else(|| anyhow!("serve output missing outputs array"))?;
|
||||
if outputs.len() != 2 {
|
||||
return Err(anyhow!("expected 2 serve outputs, got {}", outputs.len()));
|
||||
}
|
||||
let mut intent_ids = Vec::new();
|
||||
let mut receipt_ids = Vec::new();
|
||||
for (idx, output) in outputs.iter().enumerate() {
|
||||
require_eq(output, &["status"], "tickComplete")?;
|
||||
require_eq(output, &["schedulerId"], scheduler_id)?;
|
||||
require_u64(output, &["iteration"], (idx + 1) as u64)?;
|
||||
require_u64(output, &["outcomeCount"], 1)?;
|
||||
require_u64(output, &["restartedCount"], 1)?;
|
||||
require_u64(output, &["refusedCount"], 0)?;
|
||||
require_bool(output, &["privateStateExposed"], false)?;
|
||||
let outcomes = output
|
||||
.get("outcomes")
|
||||
.and_then(Value::as_array)
|
||||
.ok_or_else(|| anyhow!("tick output missing outcomes array"))?;
|
||||
let outcome = outcomes
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("tick output missing first outcome"))?;
|
||||
require_eq(outcome, &["status"], "awaiting-provider-heartbeat")?;
|
||||
require_eq(outcome, &["resultingStatus"], "awaiting-provider-heartbeat")?;
|
||||
require_bool(outcome, &["observedHeartbeatStale"], true)?;
|
||||
require_eq(outcome, &["daemonId"], daemon_id)?;
|
||||
require_bool(outcome, &["privateStateExposed"], false)?;
|
||||
intent_ids.push(
|
||||
outcome["intentId"]
|
||||
.as_str()
|
||||
.context("restart outcome missing intentId")?
|
||||
.to_string(),
|
||||
);
|
||||
receipt_ids.push(
|
||||
outcome["receiptId"]
|
||||
.as_str()
|
||||
.context("restart outcome missing receiptId")?
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if intent_ids[0] == intent_ids[1] || receipt_ids[0] == receipt_ids[1] {
|
||||
return Err(anyhow!(
|
||||
"two supervisor attempts reused immutable intent/receipt identity"
|
||||
));
|
||||
}
|
||||
|
||||
let provider_after = cargo_json(
|
||||
&manifest,
|
||||
"epiphany-verse-query",
|
||||
&[
|
||||
"swarm-status",
|
||||
"--store",
|
||||
path_str(&local_verse)?,
|
||||
"--runtime-id",
|
||||
runtime_id,
|
||||
],
|
||||
&root,
|
||||
)?;
|
||||
let provider_row = |report: &Value| -> Result<Value> {
|
||||
rows(report)
|
||||
.iter()
|
||||
.find(|row| row.get("daemonId").and_then(Value::as_str) == Some(daemon_id))
|
||||
.map(|row| (*row).clone())
|
||||
.ok_or_else(|| anyhow!("swarm status omitted provider daemon {daemon_id}"))
|
||||
};
|
||||
let before_row = provider_row(&provider_before)?;
|
||||
let after_row = provider_row(&provider_after)?;
|
||||
for field in ["status", "operatorAction", "lastHeartbeatUtc"] {
|
||||
if before_row[field] != after_row[field] {
|
||||
return Err(anyhow!(
|
||||
"supervisor changed provider-owned {field}: before={} after={}",
|
||||
before_row[field],
|
||||
after_row[field]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
cargo_json(
|
||||
&manifest,
|
||||
"epiphany-cluster-daemon",
|
||||
&[
|
||||
"heartbeat",
|
||||
"--store",
|
||||
path_str(&local_verse)?,
|
||||
"--runtime-id",
|
||||
runtime_id,
|
||||
"--daemon-id",
|
||||
daemon_id,
|
||||
],
|
||||
&root,
|
||||
)?;
|
||||
let recovered = cargo_json(
|
||||
&manifest,
|
||||
"epiphany-daemon-supervisor",
|
||||
&[
|
||||
"reconcile",
|
||||
"--store",
|
||||
path_str(&local_verse)?,
|
||||
"--runtime-id",
|
||||
runtime_id,
|
||||
"--daemon-id",
|
||||
daemon_id,
|
||||
],
|
||||
&root,
|
||||
)?;
|
||||
require_eq(&recovered, &["status"], "noop")?;
|
||||
require_bool(&recovered, &["providerHeartbeatProvedRecovery"], true)?;
|
||||
|
||||
let receipt_directory = cargo_json(
|
||||
&manifest,
|
||||
"epiphany-verse-query",
|
||||
&[
|
||||
"receipt-directory",
|
||||
"--store",
|
||||
path_str(&local_verse)?,
|
||||
"--runtime-id",
|
||||
runtime_id,
|
||||
],
|
||||
&root,
|
||||
)?;
|
||||
require_bool(&receipt_directory, &["privateStateExposed"], false)?;
|
||||
let scheduler_row = rows(&receipt_directory)
|
||||
.iter()
|
||||
.find(|row| row.get("family").and_then(Value::as_str) == Some("scheduler"))
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow!("receipt directory did not expose scheduler row"))?;
|
||||
require_eq(&scheduler_row, &["status"], "tickComplete")?;
|
||||
require_eq(&scheduler_row, &["route"], daemon_id)?;
|
||||
require_bool(&scheduler_row, &["privateStateExposed"], false)?;
|
||||
let poke_row = rows(&receipt_directory)
|
||||
.iter()
|
||||
.find(|row| row.get("family").and_then(Value::as_str) == Some("daemon-poke"))
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow!("receipt directory did not expose daemon poke row"))?;
|
||||
require_eq(&poke_row, &["status"], "resolved")?;
|
||||
let policy_directory = cargo_json(
|
||||
&manifest,
|
||||
"epiphany-verse-query",
|
||||
&[
|
||||
"restart-policy-directory",
|
||||
"--store",
|
||||
path_str(&local_verse)?,
|
||||
"--runtime-id",
|
||||
runtime_id,
|
||||
],
|
||||
&root,
|
||||
)?;
|
||||
let recovered_policy = rows(&policy_directory)
|
||||
.iter()
|
||||
.find(|row| row.get("daemonId").and_then(Value::as_str) == Some(daemon_id))
|
||||
.copied()
|
||||
.ok_or_else(|| anyhow!("restart policy directory omitted {daemon_id}"))?;
|
||||
require_u64(recovered_policy, &["failureCount"], 0)?;
|
||||
|
||||
let summary = json!({
|
||||
"schemaVersion": "epiphany.daemon_survival_rehearsal_smoke.v0",
|
||||
"status": "ok",
|
||||
"smokeDir": smoke_dir,
|
||||
"localVerseStore": local_verse,
|
||||
"runtimeId": runtime_id,
|
||||
"daemonId": daemon_id,
|
||||
"schedulerId": scheduler_id,
|
||||
"policyStatus": policy["status"],
|
||||
"policyId": policy["policyId"],
|
||||
"serveStatus": serve["status"],
|
||||
"serveIterations": serve["iterations"],
|
||||
"schedulerReceiptId": scheduler_row["latestId"],
|
||||
"schedulerReceiptStatus": scheduler_row["status"],
|
||||
"pokeReceiptStatus": poke_row["status"],
|
||||
"providerEnvelopeUnchanged": true,
|
||||
"providerHeartbeatRequiredForRecovery": true,
|
||||
"providerHeartbeatResolvedReceipt": true,
|
||||
"providerHeartbeatResetFailureCount": true,
|
||||
"distinctAttemptIntentIds": intent_ids,
|
||||
"distinctAttemptReceiptIds": receipt_ids,
|
||||
"restartCommand": restart_command,
|
||||
"restartArgs": restart_args,
|
||||
"boundedProofMode": true,
|
||||
"serviceManagerMutated": false,
|
||||
"requiresElevatedAuthority": false,
|
||||
"unattendedDaemonSurvivalRehearsed": true,
|
||||
"privateStateExposed": false,
|
||||
});
|
||||
write_json(&smoke_dir.join("summary.json"), &summary)?;
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
fn cargo_json(manifest: &Path, bin: &str, args: &[&str], cwd: &Path) -> Result<Value> {
|
||||
let output = Command::new("cargo")
|
||||
.arg("run")
|
||||
.arg("--quiet")
|
||||
.arg("--manifest-path")
|
||||
.arg(manifest)
|
||||
.arg("--bin")
|
||||
.arg(bin)
|
||||
.arg("--")
|
||||
.args(args)
|
||||
.current_dir(cwd)
|
||||
.output()
|
||||
.with_context(|| format!("failed to run cargo bin {bin}"))?;
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"{bin} failed with status {:?}\nstdout:\n{}\nstderr:\n{}",
|
||||
output.status.code(),
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
serde_json::from_slice(&output.stdout).with_context(|| {
|
||||
format!(
|
||||
"{bin} did not emit JSON on stdout:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn string_refs(args: &[String]) -> Vec<&str> {
|
||||
args.iter().map(String::as_str).collect()
|
||||
}
|
||||
|
||||
fn rows(value: &Value) -> Vec<&Value> {
|
||||
value
|
||||
.get("rows")
|
||||
.and_then(Value::as_array)
|
||||
.map(|rows| rows.iter().collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn write_json(path: &Path, value: &Value) -> Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("failed to create {}", parent.display()))?;
|
||||
}
|
||||
fs::write(path, serde_json::to_vec_pretty(value)?)
|
||||
.with_context(|| format!("failed to write {}", path.display()))
|
||||
}
|
||||
|
||||
fn require_eq(value: &Value, path: &[&str], expected: &str) -> Result<()> {
|
||||
let actual = path
|
||||
.iter()
|
||||
.try_fold(value, |current, key| current.get(*key))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("<missing>");
|
||||
if actual == expected {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"expected {} to be {expected:?}, got {actual:?}",
|
||||
path.join(".")
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn require_bool(value: &Value, path: &[&str], expected: bool) -> Result<()> {
|
||||
let actual = path
|
||||
.iter()
|
||||
.try_fold(value, |current, key| current.get(*key))
|
||||
.and_then(Value::as_bool);
|
||||
if actual == Some(expected) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"expected {} to be {expected}, got {:?}",
|
||||
path.join("."),
|
||||
actual
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn require_u64(value: &Value, path: &[&str], expected: u64) -> Result<()> {
|
||||
let actual = path
|
||||
.iter()
|
||||
.try_fold(value, |current, key| current.get(*key))
|
||||
.and_then(Value::as_u64);
|
||||
if actual == Some(expected) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"expected {} to be {expected}, got {:?}",
|
||||
path.join("."),
|
||||
actual
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn take_path(
|
||||
args: &mut std::iter::Peekable<impl Iterator<Item = String>>,
|
||||
flag: &str,
|
||||
) -> Result<PathBuf> {
|
||||
args.next()
|
||||
.map(PathBuf::from)
|
||||
.with_context(|| format!("missing value for {flag}"))
|
||||
}
|
||||
|
||||
fn path_str(path: &Path) -> Result<&str> {
|
||||
path.to_str()
|
||||
.ok_or_else(|| anyhow!("path is not valid UTF-8: {}", path.display()))
|
||||
}
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use epiphany_core::HANDS_ACTION_INTENT_SCHEMA_VERSION;
|
||||
use epiphany_core::HandsActionIntent;
|
||||
use epiphany_core::RuntimeSpineInitOptions;
|
||||
use epiphany_core::hands_action_review_for_intent;
|
||||
use epiphany_core::hands_command_receipt_for_review;
|
||||
use epiphany_core::hands_commit_receipt_for_review;
|
||||
use epiphany_core::hands_patch_receipt_for_review;
|
||||
use epiphany_core::initialize_runtime_spine;
|
||||
use epiphany_core::put_hands_action_intent;
|
||||
use epiphany_core::put_hands_action_review;
|
||||
use epiphany_core::put_hands_command_receipt;
|
||||
use epiphany_core::put_hands_commit_receipt;
|
||||
use epiphany_core::put_hands_patch_receipt;
|
||||
use epiphany_core::runtime_hands_action_intent;
|
||||
use epiphany_core::runtime_hands_action_review;
|
||||
use epiphany_core::runtime_hands_command_receipt;
|
||||
use epiphany_core::runtime_hands_commit_receipt;
|
||||
use epiphany_core::runtime_hands_patch_receipt;
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let store = PathBuf::from(".epiphany-smoke")
|
||||
.join("runtime-spine")
|
||||
.join("hands-action-smoke.msgpack");
|
||||
|
||||
initialize_runtime_spine(
|
||||
&store,
|
||||
RuntimeSpineInitOptions {
|
||||
runtime_id: "epiphany-hands-action-smoke".to_string(),
|
||||
display_name: "Epiphany Hands Action Smoke".to_string(),
|
||||
created_at: "2026-06-02T00:00:00Z".to_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
let intent = HandsActionIntent {
|
||||
schema_version: HANDS_ACTION_INTENT_SCHEMA_VERSION.to_string(),
|
||||
intent_id: "hands-intent-smoke".to_string(),
|
||||
runtime_job_id: "job-implementation-smoke".to_string(),
|
||||
binding_id: "implementation-worker".to_string(),
|
||||
role: "epiphany-hands".to_string(),
|
||||
authority_scope: "epiphany.role.implementation".to_string(),
|
||||
requested_action: "patch".to_string(),
|
||||
requested_paths: vec!["tools/epiphany_local_run.ps1".to_string()],
|
||||
substrate_gate_grant_receipt_id: "substrate-grant-smoke".to_string(),
|
||||
requested_at: "2026-06-02T00:00:10Z".to_string(),
|
||||
contract: "Smoke intent proves Hands action starts as typed runtime-spine state."
|
||||
.to_string(),
|
||||
frontier_route_id: String::new(),
|
||||
plan_candidate_sha256: String::new(),
|
||||
plan_action: String::new(),
|
||||
};
|
||||
put_hands_action_intent(&store, &intent)?;
|
||||
|
||||
let review = hands_action_review_for_intent(
|
||||
"hands-review-smoke".to_string(),
|
||||
&intent,
|
||||
"approved".to_string(),
|
||||
vec![
|
||||
"patch".to_string(),
|
||||
"command".to_string(),
|
||||
"commit".to_string(),
|
||||
"pr".to_string(),
|
||||
],
|
||||
vec![
|
||||
"Substrate Gate grant id is named; this smoke does not execute the patch.".to_string(),
|
||||
],
|
||||
"2026-06-02T00:00:20Z".to_string(),
|
||||
);
|
||||
put_hands_action_review(&store, &review)?;
|
||||
|
||||
let patch = hands_patch_receipt_for_review(
|
||||
"hands-patch-smoke".to_string(),
|
||||
&intent,
|
||||
&review,
|
||||
vec!["tools/epiphany_local_run.ps1".to_string()],
|
||||
"Recorded a typed patch receipt for the bounded Hands action path.".to_string(),
|
||||
"2026-06-02T00:00:30Z".to_string(),
|
||||
);
|
||||
put_hands_patch_receipt(&store, &patch)?;
|
||||
|
||||
let command = hands_command_receipt_for_review(
|
||||
"hands-command-smoke".to_string(),
|
||||
&intent,
|
||||
&review,
|
||||
"cargo test --manifest-path .\\epiphany-core\\Cargo.toml hands_gateway".to_string(),
|
||||
"0".to_string(),
|
||||
".epiphany-smoke/runtime-spine/hands-action-smoke.stdout.log".to_string(),
|
||||
".epiphany-smoke/runtime-spine/hands-action-smoke.stderr.log".to_string(),
|
||||
"Recorded a typed command receipt for the bounded Hands action path.".to_string(),
|
||||
"2026-06-02T00:00:40Z".to_string(),
|
||||
);
|
||||
put_hands_command_receipt(&store, &command)?;
|
||||
|
||||
let commit = hands_commit_receipt_for_review(
|
||||
"hands-commit-smoke".to_string(),
|
||||
&intent,
|
||||
&review,
|
||||
"dry-run-no-commit".to_string(),
|
||||
"main".to_string(),
|
||||
vec!["tools/epiphany_local_run.ps1".to_string()],
|
||||
"Recorded a typed commit receipt shape for the bounded Hands action path.".to_string(),
|
||||
"2026-06-02T00:00:50Z".to_string(),
|
||||
);
|
||||
put_hands_commit_receipt(&store, &commit)?;
|
||||
|
||||
let stored_intent =
|
||||
runtime_hands_action_intent(&store, "hands-intent-smoke")?.expect("stored Hands intent");
|
||||
let stored_review =
|
||||
runtime_hands_action_review(&store, "hands-review-smoke")?.expect("stored Hands review");
|
||||
let stored_patch =
|
||||
runtime_hands_patch_receipt(&store, "hands-patch-smoke")?.expect("stored Hands patch");
|
||||
let stored_command = runtime_hands_command_receipt(&store, "hands-command-smoke")?
|
||||
.expect("stored Hands command");
|
||||
let stored_commit =
|
||||
runtime_hands_commit_receipt(&store, "hands-commit-smoke")?.expect("stored Hands commit");
|
||||
|
||||
if stored_review.intent_id != stored_intent.intent_id {
|
||||
anyhow::bail!("Hands review lost its intent edge");
|
||||
}
|
||||
if stored_patch.intent_id != stored_intent.intent_id
|
||||
|| stored_patch.review_id != stored_review.review_id
|
||||
{
|
||||
anyhow::bail!("Hands patch receipt lost its intent/review edge");
|
||||
}
|
||||
if stored_command.intent_id != stored_intent.intent_id
|
||||
|| stored_command.review_id != stored_review.review_id
|
||||
{
|
||||
anyhow::bail!("Hands command receipt lost its intent/review edge");
|
||||
}
|
||||
if stored_commit.intent_id != stored_intent.intent_id
|
||||
|| stored_commit.review_id != stored_review.review_id
|
||||
{
|
||||
anyhow::bail!("Hands commit receipt lost its intent/review edge");
|
||||
}
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"status": "ok",
|
||||
"store": store,
|
||||
"intentId": stored_intent.intent_id,
|
||||
"reviewId": stored_review.review_id,
|
||||
"patchReceiptId": stored_patch.receipt_id,
|
||||
"commandReceiptId": stored_command.receipt_id,
|
||||
"commitReceiptId": stored_commit.receipt_id,
|
||||
"changedPaths": stored_patch.changed_paths,
|
||||
}))?
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,324 +0,0 @@
|
|||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = Args::parse()?;
|
||||
let result = run_smoke(&args)?;
|
||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Args {
|
||||
artifact_root: PathBuf,
|
||||
coordinator_exe: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
fn parse() -> Result<Self> {
|
||||
let root = env::current_dir().context("failed to resolve current dir")?;
|
||||
let mut parsed = Self {
|
||||
artifact_root: root.join(".epiphany-smoke").join("mvp-coordinator"),
|
||||
coordinator_exe: None,
|
||||
};
|
||||
let mut args = env::args().skip(1);
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--app-server" => {
|
||||
let _ = take_path(&mut args, "--app-server")?;
|
||||
}
|
||||
"--artifact-root" => {
|
||||
parsed.artifact_root = take_path(&mut args, "--artifact-root")?;
|
||||
}
|
||||
"--coordinator-exe" => {
|
||||
parsed.coordinator_exe = Some(take_path(&mut args, "--coordinator-exe")?);
|
||||
}
|
||||
other => return Err(anyhow!("unknown argument: {other}")),
|
||||
}
|
||||
}
|
||||
Ok(parsed)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_smoke(args: &Args) -> Result<Value> {
|
||||
let root = env::current_dir().context("failed to resolve current dir")?;
|
||||
let artifact_root = absolute_path(&args.artifact_root)?;
|
||||
reset_artifact_root(&root, &artifact_root)?;
|
||||
let coordinator = match &args.coordinator_exe {
|
||||
Some(path) => absolute_path(path)?,
|
||||
None => sibling_exe("epiphany-mvp-coordinator")?,
|
||||
};
|
||||
ensure_coordinator_built(&root, &coordinator)?;
|
||||
|
||||
let cold = run_coordinator(
|
||||
&root,
|
||||
&coordinator,
|
||||
&artifact_root,
|
||||
RunOptions {
|
||||
name: "cold",
|
||||
mode: "plan",
|
||||
max_steps: 1,
|
||||
},
|
||||
)?;
|
||||
require(
|
||||
cold.pointer("/finalAction/action").and_then(Value::as_str) == Some("prepareCheckpoint"),
|
||||
"cold start should stop at prepareCheckpoint",
|
||||
)?;
|
||||
require_artifacts(&cold)?;
|
||||
require_operator_safe(&cold, "$")?;
|
||||
|
||||
let legacy_flags = [
|
||||
"--bootstrap-smoke-state",
|
||||
"--bootstrap-local-state",
|
||||
"--bootstrap-objective",
|
||||
"--simulate-high-pressure",
|
||||
"--simulate-continue-implementation",
|
||||
"--simulate-source-drift",
|
||||
"--dry-compact",
|
||||
];
|
||||
for flag in legacy_flags {
|
||||
let rejected = Command::new(&coordinator)
|
||||
.current_dir(&root)
|
||||
.arg(flag)
|
||||
.output()
|
||||
.with_context(|| format!("failed to check rejected coordinator flag {flag}"))?;
|
||||
require(
|
||||
!rejected.status.success()
|
||||
&& String::from_utf8_lossy(&rejected.stderr).contains("unknown argument"),
|
||||
&format!("production coordinator accepted fixture-only flag {flag}"),
|
||||
)?;
|
||||
}
|
||||
|
||||
let rejected = Command::new(&coordinator)
|
||||
.current_dir(&root)
|
||||
.arg("--artifact-dir")
|
||||
.arg(artifact_root.join("rejected-private-store-completion"))
|
||||
.arg("--test-complete-backend")
|
||||
.output()
|
||||
.context("failed to run rejected completion check")?;
|
||||
let rejected_stderr = String::from_utf8_lossy(&rejected.stderr);
|
||||
require(
|
||||
!rejected.status.success() && rejected_stderr.contains("CultNet job-result API"),
|
||||
"native coordinator should reject direct backend-completion mutation",
|
||||
)?;
|
||||
|
||||
let result = json!({
|
||||
"artifactRoot": artifact_root,
|
||||
"coldAction": cold["finalAction"]["action"],
|
||||
"rejectedFixtureFlags": legacy_flags,
|
||||
"directBackendCompletionRejected": true,
|
||||
"note": "The smoke owns only its .epiphany-smoke fixture body. Production coordinator state and Hands authority come from typed runtime evidence.",
|
||||
});
|
||||
write_json(
|
||||
&artifact_root.join("coordinator-smoke-summary.json"),
|
||||
&result,
|
||||
)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
struct RunOptions {
|
||||
name: &'static str,
|
||||
mode: &'static str,
|
||||
max_steps: usize,
|
||||
}
|
||||
|
||||
fn run_coordinator(
|
||||
root: &Path,
|
||||
coordinator: &Path,
|
||||
artifact_root: &Path,
|
||||
options: RunOptions,
|
||||
) -> Result<Value> {
|
||||
let artifact_dir = artifact_root.join(options.name);
|
||||
let mut command = Command::new(coordinator);
|
||||
command
|
||||
.current_dir(root)
|
||||
.arg("--artifact-dir")
|
||||
.arg(&artifact_dir)
|
||||
.arg("--runtime-store")
|
||||
.arg(artifact_dir.join("runtime-spine.msgpack"))
|
||||
.arg("--codex-home")
|
||||
.arg(artifact_dir.join("codex-home"))
|
||||
.arg("--cwd")
|
||||
.arg(root)
|
||||
.arg("--mode")
|
||||
.arg(options.mode)
|
||||
.arg("--max-steps")
|
||||
.arg(options.max_steps.to_string())
|
||||
.arg("--poll-seconds")
|
||||
.arg("0.1")
|
||||
.arg("--timeout-seconds")
|
||||
.arg("3");
|
||||
let output = command
|
||||
.output()
|
||||
.context("failed to run native coordinator")?;
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"native coordinator failed: {}{}",
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
));
|
||||
}
|
||||
serde_json::from_slice(&output.stdout).context("failed to decode coordinator summary")
|
||||
}
|
||||
|
||||
fn require_artifacts(summary: &Value) -> Result<()> {
|
||||
let artifact_dir = PathBuf::from(
|
||||
summary
|
||||
.get("artifactDir")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| anyhow!("summary missing artifactDir"))?,
|
||||
);
|
||||
for name in [
|
||||
"coordinator-summary.json",
|
||||
"coordinator-steps.jsonl",
|
||||
"coordinator-final-status.json",
|
||||
"coordinator-final-status.txt",
|
||||
"coordinator-final-action.txt",
|
||||
"agent-function-telemetry.json",
|
||||
"runtime-spine-status.json",
|
||||
] {
|
||||
require(
|
||||
artifact_dir.join(name).exists(),
|
||||
&format!("missing coordinator artifact {name}"),
|
||||
)?;
|
||||
}
|
||||
require(
|
||||
artifact_dir.join("runtime-spine.msgpack").exists(),
|
||||
"missing native runtime spine store",
|
||||
)?;
|
||||
let runtime_status: Value = serde_json::from_str(&fs::read_to_string(
|
||||
artifact_dir.join("runtime-spine-status.json"),
|
||||
)?)?;
|
||||
require(
|
||||
runtime_status["present"].as_bool() == Some(true),
|
||||
"native runtime spine should be present",
|
||||
)?;
|
||||
require(
|
||||
runtime_status["sessions"].as_u64().unwrap_or(0) >= 1,
|
||||
"native runtime spine should record a session",
|
||||
)?;
|
||||
let telemetry: Value = serde_json::from_str(&fs::read_to_string(
|
||||
artifact_dir.join("agent-function-telemetry.json"),
|
||||
)?)?;
|
||||
require(
|
||||
telemetry["appServerCalls"].as_u64() == Some(0)
|
||||
&& telemetry["transport"].as_str() == Some("cultcache"),
|
||||
"native coordinator telemetry must prove zero app-server calls",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn require_operator_safe(value: &Value, path: &str) -> Result<()> {
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
if let Some(raw_result) = map.get("rawResult") {
|
||||
require(
|
||||
raw_result.get("sealed").and_then(Value::as_bool) == Some(true),
|
||||
&format!("{path}.rawResult should be sealed in operator-facing artifacts"),
|
||||
)?;
|
||||
}
|
||||
for (key, item) in map {
|
||||
require_operator_safe(item, &format!("{path}.{key}"))?;
|
||||
}
|
||||
}
|
||||
Value::Array(items) => {
|
||||
for (index, item) in items.iter().enumerate() {
|
||||
require_operator_safe(item, &format!("{path}[{index}]"))?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reset_artifact_root(root: &Path, path: &Path) -> Result<()> {
|
||||
let dogfood_root = root.join(".epiphany-smoke").canonicalize().or_else(|_| {
|
||||
let dogfood_root = root.join(".epiphany-smoke");
|
||||
fs::create_dir_all(&dogfood_root)?;
|
||||
dogfood_root.canonicalize()
|
||||
})?;
|
||||
let parent = path.parent().unwrap_or_else(|| Path::new("."));
|
||||
fs::create_dir_all(parent)?;
|
||||
let resolved = if path.exists() {
|
||||
path.canonicalize()?
|
||||
} else {
|
||||
parent.canonicalize()?.join(path.file_name().unwrap())
|
||||
};
|
||||
if resolved == dogfood_root || !resolved.starts_with(&dogfood_root) {
|
||||
return Err(anyhow!(
|
||||
"refusing to delete non-dogfood artifact root: {}",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
if path.exists() {
|
||||
fs::remove_dir_all(path)?;
|
||||
}
|
||||
fs::create_dir_all(path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sibling_exe(name: &str) -> Result<PathBuf> {
|
||||
let mut exe = env::current_exe().context("failed to resolve current executable")?;
|
||||
exe.set_file_name(format!("{name}{}", env::consts::EXE_SUFFIX));
|
||||
Ok(exe)
|
||||
}
|
||||
|
||||
fn ensure_coordinator_built(root: &Path, coordinator: &Path) -> Result<()> {
|
||||
ensure_epiphany_bin_built(root, coordinator, "epiphany-mvp-coordinator")
|
||||
}
|
||||
|
||||
fn ensure_epiphany_bin_built(root: &Path, binary: &Path, bin_name: &str) -> Result<()> {
|
||||
if binary.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
let status = Command::new("cargo")
|
||||
.current_dir(root)
|
||||
.arg("build")
|
||||
.arg("--manifest-path")
|
||||
.arg(root.join("epiphany-core").join("Cargo.toml"))
|
||||
.arg("--bin")
|
||||
.arg(bin_name)
|
||||
.status()
|
||||
.with_context(|| format!("failed to build {bin_name}"))?;
|
||||
require(
|
||||
status.success() && binary.exists(),
|
||||
&format!("native binary was not built: {}", binary.display()),
|
||||
)
|
||||
}
|
||||
|
||||
fn absolute_path(path: &Path) -> Result<PathBuf> {
|
||||
if path.is_absolute() {
|
||||
Ok(path.to_path_buf())
|
||||
} else {
|
||||
Ok(env::current_dir()?.join(path))
|
||||
}
|
||||
}
|
||||
|
||||
fn write_json(path: &Path, value: &Value) -> Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::write(path, format!("{}\n", serde_json::to_string_pretty(value)?))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn require(condition: bool, message: &str) -> Result<()> {
|
||||
if condition {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("{message}"))
|
||||
}
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
|
|
@ -1,486 +0,0 @@
|
|||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use chrono::Utc;
|
||||
use epiphany_core::EPIPHANY_CULTMESH_INTERNAL_VERSE_ID;
|
||||
use epiphany_core::EPIPHANY_CULTMESH_OPERATOR_RUN_INTENT_SCHEMA_VERSION;
|
||||
use epiphany_core::EPIPHANY_CULTMESH_OPERATOR_RUN_RECEIPT_SCHEMA_VERSION;
|
||||
use epiphany_core::EpiphanyCultMeshOperatorRunIntentEntry;
|
||||
use epiphany_core::EpiphanyCultMeshOperatorRunReceiptEntry;
|
||||
use epiphany_core::epiphany_cultmesh_coordinator_run_receipt_from_summary_json;
|
||||
use epiphany_core::epiphany_cultmesh_hands_action_gate_from_summary_json;
|
||||
use epiphany_core::epiphany_cultmesh_role_review_event_from_summary_json;
|
||||
use epiphany_core::load_epiphany_cultmesh_operator_run_intent;
|
||||
use epiphany_core::load_epiphany_cultmesh_operator_run_receipt;
|
||||
use epiphany_core::load_latest_epiphany_cultmesh_operator_run_intent;
|
||||
use epiphany_core::load_latest_epiphany_cultmesh_operator_run_receipt;
|
||||
use epiphany_core::write_epiphany_cultmesh_coordinator_run_receipt;
|
||||
use epiphany_core::write_epiphany_cultmesh_hands_action_gate;
|
||||
use epiphany_core::write_epiphany_cultmesh_operator_run_intent;
|
||||
use epiphany_core::write_epiphany_cultmesh_operator_run_receipt;
|
||||
use epiphany_core::write_epiphany_cultmesh_role_review_event;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = Args::parse()?;
|
||||
match args.command.as_str() {
|
||||
"intent" => {
|
||||
let intent = EpiphanyCultMeshOperatorRunIntentEntry {
|
||||
schema_version: EPIPHANY_CULTMESH_OPERATOR_RUN_INTENT_SCHEMA_VERSION.to_string(),
|
||||
runtime_id: args.runtime_id.clone(),
|
||||
verse_id: EPIPHANY_CULTMESH_INTERNAL_VERSE_ID.to_string(),
|
||||
run_id: args.run_id.clone(),
|
||||
requested_at_utc: Utc::now().to_rfc3339(),
|
||||
mode: args.mode.clone(),
|
||||
root: args.root.clone(),
|
||||
workspace: args.workspace.clone(),
|
||||
thread_id: args.thread_id.clone(),
|
||||
codex_home: args.codex_home.clone(),
|
||||
target_dir: args.target_dir.clone(),
|
||||
max_steps: args.max_steps,
|
||||
timeout_seconds: args.timeout_seconds,
|
||||
auto_review: args.auto_review,
|
||||
no_ephemeral: args.no_ephemeral,
|
||||
artifact_root: args.artifact_root.clone(),
|
||||
dogfood_root: args.dogfood_root.clone(),
|
||||
};
|
||||
let written = write_epiphany_cultmesh_operator_run_intent(&args.store, intent)?;
|
||||
print_json(json!({"status": "written", "store": args.store, "intent": written}))?;
|
||||
}
|
||||
"receipt" => {
|
||||
let intent = load_epiphany_cultmesh_operator_run_intent(
|
||||
&args.store,
|
||||
&args.runtime_id,
|
||||
&args.run_id,
|
||||
)?
|
||||
.context("operator-run receipt requires a persisted intent for its run id")?;
|
||||
if intent.mode != args.mode {
|
||||
anyhow::bail!(
|
||||
"operator-run receipt mode does not match its intent: run={} requested mode={} intent mode={}",
|
||||
args.run_id,
|
||||
args.mode,
|
||||
intent.mode
|
||||
);
|
||||
}
|
||||
let result_path = canonical_file(&args.result_path, "--result-path")?;
|
||||
let artifact_root = canonical_directory(&args.artifact_root, "--artifact-root")?;
|
||||
if !result_path.starts_with(&artifact_root) {
|
||||
anyhow::bail!(
|
||||
"operator-run result {} is outside artifact root {}",
|
||||
result_path.display(),
|
||||
artifact_root.display()
|
||||
);
|
||||
}
|
||||
let result_source = fs::read_to_string(&result_path).with_context(|| {
|
||||
format!("failed to read result artifact {}", result_path.display())
|
||||
})?;
|
||||
let _: Value = serde_json::from_str(result_source.trim_start_matches('\u{feff}'))
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"operator-run result is not valid JSON: {}",
|
||||
result_path.display()
|
||||
)
|
||||
})?;
|
||||
let requested_at = chrono::DateTime::parse_from_rfc3339(&intent.requested_at_utc)
|
||||
.context("operator-run intent has invalid requested_at_utc")?
|
||||
.with_timezone(&Utc);
|
||||
let modified_at: chrono::DateTime<Utc> = fs::metadata(&result_path)?
|
||||
.modified()
|
||||
.context("operator-run result has no modification time")?
|
||||
.into();
|
||||
if modified_at < requested_at {
|
||||
anyhow::bail!(
|
||||
"operator-run result predates its intent: result={} intent={}",
|
||||
modified_at.to_rfc3339(),
|
||||
requested_at.to_rfc3339()
|
||||
);
|
||||
}
|
||||
let mut artifact_refs = Vec::new();
|
||||
push_non_empty(&mut artifact_refs, &args.result_path);
|
||||
push_non_empty(&mut artifact_refs, &args.artifact_root);
|
||||
push_non_empty(&mut artifact_refs, &args.dogfood_root);
|
||||
let receipt = EpiphanyCultMeshOperatorRunReceiptEntry {
|
||||
schema_version: EPIPHANY_CULTMESH_OPERATOR_RUN_RECEIPT_SCHEMA_VERSION.to_string(),
|
||||
runtime_id: args.runtime_id.clone(),
|
||||
verse_id: EPIPHANY_CULTMESH_INTERNAL_VERSE_ID.to_string(),
|
||||
run_id: args.run_id.clone(),
|
||||
completed_at_utc: Utc::now().to_rfc3339(),
|
||||
mode: args.mode.clone(),
|
||||
status: "completed".to_string(),
|
||||
result_path: args.result_path.clone(),
|
||||
artifact_root: args.artifact_root.clone(),
|
||||
dogfood_root: args.dogfood_root.clone(),
|
||||
operator_snapshot_store: args.operator_snapshot_store.clone(),
|
||||
operator_snapshot_id: args.operator_snapshot_id.clone(),
|
||||
artifact_refs,
|
||||
notes: vec![
|
||||
"Receipt records local operator-run completion; referenced artifacts remain evidence, not owner state.".to_string(),
|
||||
"Codex app-server output is compatibility projection until the run path is native end to end.".to_string(),
|
||||
],
|
||||
};
|
||||
let written = write_epiphany_cultmesh_operator_run_receipt(&args.store, receipt)?;
|
||||
print_json(json!({"status": "written", "store": args.store, "receipt": written}))?;
|
||||
}
|
||||
"latest" => {
|
||||
let intent =
|
||||
load_latest_epiphany_cultmesh_operator_run_intent(&args.store, &args.runtime_id)?;
|
||||
let receipt = if let Some(intent) = intent.as_ref() {
|
||||
load_epiphany_cultmesh_operator_run_receipt(
|
||||
&args.store,
|
||||
&args.runtime_id,
|
||||
&intent.run_id,
|
||||
)?
|
||||
} else {
|
||||
load_latest_epiphany_cultmesh_operator_run_receipt(&args.store, &args.runtime_id)?
|
||||
};
|
||||
print_json(json!({
|
||||
"status": operator_run_status(intent.as_ref(), receipt.as_ref()),
|
||||
"store": args.store,
|
||||
"intent": intent,
|
||||
"receipt": receipt,
|
||||
}))?;
|
||||
}
|
||||
"coordinator-receipt" => {
|
||||
let summary_path = args
|
||||
.coordinator_summary
|
||||
.clone()
|
||||
.context("coordinator-receipt requires --coordinator-summary")?;
|
||||
let summary_source = fs::read_to_string(&summary_path)
|
||||
.with_context(|| format!("failed to read {}", summary_path.display()))?;
|
||||
let summary_json: Value =
|
||||
serde_json::from_str(summary_source.trim_start_matches('\u{feff}'))
|
||||
.with_context(|| format!("failed to parse {}", summary_path.display()))?;
|
||||
let receipt_id = if args.coordinator_receipt_id.trim().is_empty() {
|
||||
format!("coordinator-cultmesh-{}", args.run_id)
|
||||
} else {
|
||||
args.coordinator_receipt_id.clone()
|
||||
};
|
||||
let written = write_epiphany_cultmesh_coordinator_run_receipt(
|
||||
&args.store,
|
||||
epiphany_cultmesh_coordinator_run_receipt_from_summary_json(
|
||||
args.runtime_id.clone(),
|
||||
receipt_id,
|
||||
Utc::now().to_rfc3339(),
|
||||
args.artifact_root.clone(),
|
||||
&summary_json,
|
||||
)?,
|
||||
)?;
|
||||
let hands_action_gate = epiphany_cultmesh_hands_action_gate_from_summary_json(
|
||||
args.runtime_id.clone(),
|
||||
Utc::now().to_rfc3339(),
|
||||
summary_path.display().to_string(),
|
||||
&summary_json,
|
||||
)?
|
||||
.map(|gate| write_epiphany_cultmesh_hands_action_gate(&args.store, gate))
|
||||
.transpose()?;
|
||||
let role_review_event = epiphany_cultmesh_role_review_event_from_summary_json(
|
||||
args.runtime_id.clone(),
|
||||
Utc::now().to_rfc3339(),
|
||||
summary_path.display().to_string(),
|
||||
&summary_json,
|
||||
)?
|
||||
.map(|event| write_epiphany_cultmesh_role_review_event(&args.store, event))
|
||||
.transpose()?;
|
||||
print_json(json!({
|
||||
"status": "written",
|
||||
"store": args.store,
|
||||
"coordinatorReceipt": written,
|
||||
"handsActionGate": hands_action_gate,
|
||||
"roleReviewEvent": role_review_event,
|
||||
}))?;
|
||||
}
|
||||
"smoke" => {
|
||||
let intent = EpiphanyCultMeshOperatorRunIntentEntry {
|
||||
schema_version: EPIPHANY_CULTMESH_OPERATOR_RUN_INTENT_SCHEMA_VERSION.to_string(),
|
||||
runtime_id: args.runtime_id.clone(),
|
||||
verse_id: EPIPHANY_CULTMESH_INTERNAL_VERSE_ID.to_string(),
|
||||
run_id: args.run_id.clone(),
|
||||
requested_at_utc: "2026-05-27T00:00:00Z".to_string(),
|
||||
mode: "status".to_string(),
|
||||
root: "E:\\Projects\\EpiphanyAgent".to_string(),
|
||||
workspace: "E:\\Projects\\EpiphanyAgent".to_string(),
|
||||
thread_id: String::new(),
|
||||
codex_home: "C:\\Users\\Meta\\.codex".to_string(),
|
||||
target_dir: "C:\\Users\\Meta\\.cargo-target-codex".to_string(),
|
||||
max_steps: 4,
|
||||
timeout_seconds: 240,
|
||||
auto_review: false,
|
||||
no_ephemeral: false,
|
||||
artifact_root: ".epiphany-run/local-smoke".to_string(),
|
||||
dogfood_root: ".epiphany-dogfood/local-smoke".to_string(),
|
||||
};
|
||||
let receipt = EpiphanyCultMeshOperatorRunReceiptEntry {
|
||||
schema_version: EPIPHANY_CULTMESH_OPERATOR_RUN_RECEIPT_SCHEMA_VERSION.to_string(),
|
||||
runtime_id: args.runtime_id.clone(),
|
||||
verse_id: EPIPHANY_CULTMESH_INTERNAL_VERSE_ID.to_string(),
|
||||
run_id: args.run_id.clone(),
|
||||
completed_at_utc: "2026-05-27T00:00:01Z".to_string(),
|
||||
mode: "status".to_string(),
|
||||
status: "completed".to_string(),
|
||||
result_path: ".epiphany-run/local-smoke/status.json".to_string(),
|
||||
artifact_root: ".epiphany-run/local-smoke".to_string(),
|
||||
dogfood_root: ".epiphany-dogfood/local-smoke".to_string(),
|
||||
operator_snapshot_store: ".epiphany-run/cultmesh/operator-snapshots.ccmp"
|
||||
.to_string(),
|
||||
operator_snapshot_id: "local-smoke-status".to_string(),
|
||||
artifact_refs: vec![".epiphany-run/local-smoke/status.json".to_string()],
|
||||
notes: vec!["smoke receipt".to_string()],
|
||||
};
|
||||
write_epiphany_cultmesh_operator_run_intent(&args.store, intent.clone())?;
|
||||
write_epiphany_cultmesh_operator_run_receipt(&args.store, receipt.clone())?;
|
||||
let latest_intent =
|
||||
load_latest_epiphany_cultmesh_operator_run_intent(&args.store, &args.runtime_id)?;
|
||||
let latest_receipt =
|
||||
load_latest_epiphany_cultmesh_operator_run_receipt(&args.store, &args.runtime_id)?;
|
||||
if latest_intent != Some(intent) || latest_receipt != Some(receipt) {
|
||||
anyhow::bail!("operator run intent/receipt did not round-trip through CultMesh");
|
||||
}
|
||||
print_json(json!({
|
||||
"status": "ok",
|
||||
"store": args.store,
|
||||
"runtimeId": args.runtime_id,
|
||||
"runId": args.run_id,
|
||||
}))?;
|
||||
}
|
||||
other => anyhow::bail!("unknown command {other:?}; use intent, receipt, latest, or smoke"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn operator_run_status(
|
||||
intent: Option<&EpiphanyCultMeshOperatorRunIntentEntry>,
|
||||
receipt: Option<&EpiphanyCultMeshOperatorRunReceiptEntry>,
|
||||
) -> &'static str {
|
||||
match (intent, receipt) {
|
||||
(None, None) => "missing",
|
||||
(Some(_), None) => "requested",
|
||||
(None, Some(_)) => "orphaned-receipt",
|
||||
(Some(intent), Some(receipt))
|
||||
if intent.run_id == receipt.run_id && receipt.status == "completed" =>
|
||||
{
|
||||
"completed"
|
||||
}
|
||||
(Some(_), Some(_)) => "attention",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn intent(run_id: &str) -> EpiphanyCultMeshOperatorRunIntentEntry {
|
||||
EpiphanyCultMeshOperatorRunIntentEntry {
|
||||
schema_version: EPIPHANY_CULTMESH_OPERATOR_RUN_INTENT_SCHEMA_VERSION.to_string(),
|
||||
runtime_id: "epiphany-test".to_string(),
|
||||
verse_id: EPIPHANY_CULTMESH_INTERNAL_VERSE_ID.to_string(),
|
||||
run_id: run_id.to_string(),
|
||||
requested_at_utc: "2026-07-13T00:00:00Z".to_string(),
|
||||
mode: "status".to_string(),
|
||||
root: "root".to_string(),
|
||||
workspace: "workspace".to_string(),
|
||||
thread_id: String::new(),
|
||||
codex_home: String::new(),
|
||||
target_dir: String::new(),
|
||||
max_steps: 1,
|
||||
timeout_seconds: 1,
|
||||
auto_review: false,
|
||||
no_ephemeral: false,
|
||||
artifact_root: "artifacts".to_string(),
|
||||
dogfood_root: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn receipt(run_id: &str, status: &str) -> EpiphanyCultMeshOperatorRunReceiptEntry {
|
||||
EpiphanyCultMeshOperatorRunReceiptEntry {
|
||||
schema_version: EPIPHANY_CULTMESH_OPERATOR_RUN_RECEIPT_SCHEMA_VERSION.to_string(),
|
||||
runtime_id: "epiphany-test".to_string(),
|
||||
verse_id: EPIPHANY_CULTMESH_INTERNAL_VERSE_ID.to_string(),
|
||||
run_id: run_id.to_string(),
|
||||
completed_at_utc: "2026-07-13T00:00:01Z".to_string(),
|
||||
mode: "status".to_string(),
|
||||
status: status.to_string(),
|
||||
result_path: "result.json".to_string(),
|
||||
artifact_root: "artifacts".to_string(),
|
||||
dogfood_root: String::new(),
|
||||
operator_snapshot_store: String::new(),
|
||||
operator_snapshot_id: String::new(),
|
||||
artifact_refs: Vec::new(),
|
||||
notes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intent_presence_is_requested_not_ready() {
|
||||
let intent = intent("run-new");
|
||||
assert_eq!(operator_run_status(Some(&intent), None), "requested");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_matching_completed_receipt_closes_latest_intent() {
|
||||
let intent = intent("run-new");
|
||||
let old_receipt = receipt("run-old", "completed");
|
||||
let matching_receipt = receipt("run-new", "completed");
|
||||
assert_eq!(
|
||||
operator_run_status(Some(&intent), Some(&old_receipt)),
|
||||
"attention"
|
||||
);
|
||||
assert_eq!(
|
||||
operator_run_status(Some(&intent), Some(&matching_receipt)),
|
||||
"completed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
struct Args {
|
||||
command: String,
|
||||
store: PathBuf,
|
||||
runtime_id: String,
|
||||
run_id: String,
|
||||
mode: String,
|
||||
root: String,
|
||||
workspace: String,
|
||||
thread_id: String,
|
||||
codex_home: String,
|
||||
target_dir: String,
|
||||
max_steps: u32,
|
||||
timeout_seconds: u32,
|
||||
auto_review: bool,
|
||||
no_ephemeral: bool,
|
||||
artifact_root: String,
|
||||
dogfood_root: String,
|
||||
result_path: String,
|
||||
operator_snapshot_store: String,
|
||||
operator_snapshot_id: String,
|
||||
coordinator_summary: Option<PathBuf>,
|
||||
coordinator_receipt_id: String,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
fn parse() -> Result<Self> {
|
||||
let mut values = env::args().skip(1);
|
||||
let command = values.next().unwrap_or_else(|| "latest".to_string());
|
||||
let mut args = Args {
|
||||
command,
|
||||
store: PathBuf::from(".epiphany-run/cultmesh/operator-runs.ccmp"),
|
||||
runtime_id: "epiphany-local".to_string(),
|
||||
run_id: format!("run-{}", Utc::now().timestamp()),
|
||||
mode: "status".to_string(),
|
||||
root: String::new(),
|
||||
workspace: String::new(),
|
||||
thread_id: String::new(),
|
||||
codex_home: String::new(),
|
||||
target_dir: String::new(),
|
||||
max_steps: 4,
|
||||
timeout_seconds: 240,
|
||||
auto_review: false,
|
||||
no_ephemeral: false,
|
||||
artifact_root: String::new(),
|
||||
dogfood_root: String::new(),
|
||||
result_path: String::new(),
|
||||
operator_snapshot_store: String::new(),
|
||||
operator_snapshot_id: String::new(),
|
||||
coordinator_summary: None,
|
||||
coordinator_receipt_id: String::new(),
|
||||
};
|
||||
|
||||
while let Some(arg) = values.next() {
|
||||
match arg.as_str() {
|
||||
"--store" => args.store = PathBuf::from(next(&mut values, "--store")?),
|
||||
"--runtime-id" => args.runtime_id = next(&mut values, "--runtime-id")?,
|
||||
"--run-id" => args.run_id = next(&mut values, "--run-id")?,
|
||||
"--mode" => args.mode = next(&mut values, "--mode")?,
|
||||
"--root" => args.root = next(&mut values, "--root")?,
|
||||
"--workspace" => args.workspace = next(&mut values, "--workspace")?,
|
||||
"--thread-id" => args.thread_id = next(&mut values, "--thread-id")?,
|
||||
"--codex-home" => args.codex_home = next(&mut values, "--codex-home")?,
|
||||
"--target-dir" => args.target_dir = next(&mut values, "--target-dir")?,
|
||||
"--max-steps" => args.max_steps = parse_u32(&mut values, "--max-steps")?,
|
||||
"--timeout-seconds" => {
|
||||
args.timeout_seconds = parse_u32(&mut values, "--timeout-seconds")?
|
||||
}
|
||||
"--auto-review" => args.auto_review = parse_bool(&mut values, "--auto-review")?,
|
||||
"--no-ephemeral" => args.no_ephemeral = parse_bool(&mut values, "--no-ephemeral")?,
|
||||
"--artifact-root" => args.artifact_root = next(&mut values, "--artifact-root")?,
|
||||
"--dogfood-root" => args.dogfood_root = next(&mut values, "--dogfood-root")?,
|
||||
"--result-path" => args.result_path = next(&mut values, "--result-path")?,
|
||||
"--operator-snapshot-store" => {
|
||||
args.operator_snapshot_store = next(&mut values, "--operator-snapshot-store")?
|
||||
}
|
||||
"--operator-snapshot-id" => {
|
||||
args.operator_snapshot_id = next(&mut values, "--operator-snapshot-id")?
|
||||
}
|
||||
"--coordinator-summary" => {
|
||||
args.coordinator_summary =
|
||||
Some(PathBuf::from(next(&mut values, "--coordinator-summary")?))
|
||||
}
|
||||
"--coordinator-receipt-id" => {
|
||||
args.coordinator_receipt_id = next(&mut values, "--coordinator-receipt-id")?
|
||||
}
|
||||
_ => anyhow::bail!("unknown argument {arg:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(parent) = args.store.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
Ok(args)
|
||||
}
|
||||
}
|
||||
|
||||
fn next(values: &mut impl Iterator<Item = String>, name: &str) -> Result<String> {
|
||||
values
|
||||
.next()
|
||||
.with_context(|| format!("missing {name} value"))
|
||||
}
|
||||
|
||||
fn parse_u32(values: &mut impl Iterator<Item = String>, name: &str) -> Result<u32> {
|
||||
next(values, name)?
|
||||
.parse::<u32>()
|
||||
.with_context(|| format!("{name} must be a non-negative integer"))
|
||||
}
|
||||
|
||||
fn parse_bool(values: &mut impl Iterator<Item = String>, name: &str) -> Result<bool> {
|
||||
match next(values, name)?.as_str() {
|
||||
"true" | "1" | "yes" => Ok(true),
|
||||
"false" | "0" | "no" => Ok(false),
|
||||
_ => anyhow::bail!("{name} must be true/false"),
|
||||
}
|
||||
}
|
||||
|
||||
fn push_non_empty(items: &mut Vec<String>, value: &str) {
|
||||
if !value.trim().is_empty() {
|
||||
items.push(value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
fn canonical_file(value: &str, name: &str) -> Result<PathBuf> {
|
||||
if value.trim().is_empty() {
|
||||
anyhow::bail!("operator-run receipt requires {name}");
|
||||
}
|
||||
let path =
|
||||
fs::canonicalize(value).with_context(|| format!("{name} does not exist: {value}"))?;
|
||||
if !path.is_file() {
|
||||
anyhow::bail!("{name} is not a file: {}", path.display());
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn canonical_directory(value: &str, name: &str) -> Result<PathBuf> {
|
||||
if value.trim().is_empty() {
|
||||
anyhow::bail!("operator-run receipt requires {name}");
|
||||
}
|
||||
let path =
|
||||
fs::canonicalize(value).with_context(|| format!("{name} does not exist: {value}"))?;
|
||||
if !path.is_dir() {
|
||||
anyhow::bail!("{name} is not a directory: {}", path.display());
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn print_json(value: Value) -> Result<()> {
|
||||
println!("{}", serde_json::to_string_pretty(&value)?);
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,167 +0,0 @@
|
|||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use chrono::Utc;
|
||||
use epiphany_core::epiphany_cultmesh_operator_snapshot_from_status_json;
|
||||
use epiphany_core::load_latest_epiphany_cultmesh_operator_snapshot;
|
||||
use epiphany_core::write_epiphany_cultmesh_operator_snapshot;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = Args::parse()?;
|
||||
match args.command.as_str() {
|
||||
"from-status" => {
|
||||
let input = args.input.context("from-status requires --input")?;
|
||||
let source = fs::read_to_string(&input)
|
||||
.with_context(|| format!("failed to read {}", input.display()))?;
|
||||
let status_json: Value = serde_json::from_str(source.trim_start_matches('\u{feff}'))
|
||||
.with_context(|| format!("failed to parse {}", input.display()))?;
|
||||
let snapshot = epiphany_cultmesh_operator_snapshot_from_status_json(
|
||||
args.runtime_id.clone(),
|
||||
args.snapshot_id.clone(),
|
||||
Utc::now().to_rfc3339(),
|
||||
args.source_mode.clone(),
|
||||
input.to_string_lossy(),
|
||||
&status_json,
|
||||
)?;
|
||||
let written = write_epiphany_cultmesh_operator_snapshot(&args.store, snapshot)?;
|
||||
print_json(json!({
|
||||
"status": "written",
|
||||
"store": args.store,
|
||||
"snapshot": written,
|
||||
"toolInvocationIntent": null,
|
||||
"toolInvocationReceipt": null,
|
||||
"toolInvocationAuthority": "runtime-spine-only",
|
||||
}))?;
|
||||
}
|
||||
"latest" => {
|
||||
let latest =
|
||||
load_latest_epiphany_cultmesh_operator_snapshot(&args.store, &args.runtime_id)?;
|
||||
print_json(json!({
|
||||
"status": if latest.is_some() { "loaded" } else { "missing" },
|
||||
"store": args.store,
|
||||
"snapshot": latest,
|
||||
}))?;
|
||||
}
|
||||
"smoke" => {
|
||||
let status_json = json!({
|
||||
"threadId": "thread-smoke",
|
||||
"scene": {
|
||||
"scene": {
|
||||
"stateStatus": "missing",
|
||||
"availableActions": ["crrc", "roles"]
|
||||
}
|
||||
},
|
||||
"pressure": {
|
||||
"pressure": {
|
||||
"level": "low"
|
||||
}
|
||||
},
|
||||
"reorient": {
|
||||
"decision": {
|
||||
"action": "regather",
|
||||
"nextAction": "Regather source context."
|
||||
}
|
||||
},
|
||||
"crrc": {
|
||||
"recommendation": {
|
||||
"action": "regatherManually"
|
||||
}
|
||||
},
|
||||
"coordinator": {
|
||||
"action": "wait"
|
||||
},
|
||||
"rawResult": {
|
||||
"sealed": true
|
||||
}
|
||||
});
|
||||
let snapshot = epiphany_cultmesh_operator_snapshot_from_status_json(
|
||||
args.runtime_id.clone(),
|
||||
args.snapshot_id.clone(),
|
||||
"2026-05-27T00:00:00Z",
|
||||
"status",
|
||||
".epiphany-smoke/operator-status.json",
|
||||
&status_json,
|
||||
)?;
|
||||
write_epiphany_cultmesh_operator_snapshot(&args.store, snapshot.clone())?;
|
||||
let latest =
|
||||
load_latest_epiphany_cultmesh_operator_snapshot(&args.store, &args.runtime_id)?;
|
||||
if latest != Some(snapshot) {
|
||||
anyhow::bail!("operator snapshot did not round-trip through CultMesh");
|
||||
}
|
||||
print_json(json!({
|
||||
"status": "ok",
|
||||
"store": args.store,
|
||||
"runtimeId": args.runtime_id,
|
||||
"snapshotId": args.snapshot_id,
|
||||
}))?;
|
||||
}
|
||||
other => anyhow::bail!("unknown command {other:?}; use from-status, latest, or smoke"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct Args {
|
||||
command: String,
|
||||
store: PathBuf,
|
||||
runtime_id: String,
|
||||
snapshot_id: String,
|
||||
source_mode: String,
|
||||
input: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
fn parse() -> Result<Self> {
|
||||
let mut values = env::args().skip(1);
|
||||
let command = values.next().unwrap_or_else(|| "latest".to_string());
|
||||
let mut store = PathBuf::from(".epiphany-run/cultmesh/operator-snapshots.ccmp");
|
||||
let mut runtime_id = "epiphany-local".to_string();
|
||||
let mut snapshot_id = format!("snapshot-{}", Utc::now().timestamp());
|
||||
let mut source_mode = "status".to_string();
|
||||
let mut input = None;
|
||||
|
||||
while let Some(arg) = values.next() {
|
||||
match arg.as_str() {
|
||||
"--store" => {
|
||||
store = PathBuf::from(values.next().context("missing --store value")?);
|
||||
}
|
||||
"--runtime-id" => {
|
||||
runtime_id = values.next().context("missing --runtime-id value")?;
|
||||
}
|
||||
"--snapshot-id" => {
|
||||
snapshot_id = values.next().context("missing --snapshot-id value")?;
|
||||
}
|
||||
"--source-mode" => {
|
||||
source_mode = values.next().context("missing --source-mode value")?;
|
||||
}
|
||||
"--input" => {
|
||||
input = Some(PathBuf::from(
|
||||
values.next().context("missing --input value")?,
|
||||
));
|
||||
}
|
||||
_ => anyhow::bail!("unknown argument {arg:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(parent) = store.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
command,
|
||||
store,
|
||||
runtime_id,
|
||||
snapshot_id,
|
||||
source_mode,
|
||||
input,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn print_json(value: Value) -> Result<()> {
|
||||
println!("{}", serde_json::to_string_pretty(&value)?);
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
use std::{env, path::PathBuf};
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use epiphany_core::{
|
||||
enroll_persona_discord_request_identity, export_persona_discord_request_anchor_candidate,
|
||||
insert_persona_discord_delivery_request, open_persona_discord_request_identity,
|
||||
sign_persona_discord_delivery_request,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut values = std::collections::BTreeMap::new();
|
||||
let mut args = env::args().skip(1);
|
||||
while let Some(key) = args.next() {
|
||||
values.insert(
|
||||
key.clone(),
|
||||
args.next()
|
||||
.ok_or_else(|| anyhow!("{key} requires a value"))?,
|
||||
);
|
||||
}
|
||||
let value = |key: &str| {
|
||||
values
|
||||
.get(key)
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow!("{key} is required"))
|
||||
};
|
||||
let store = PathBuf::from(value("--request-store")?);
|
||||
let identity = PathBuf::from(value("--identity-store")?);
|
||||
let anchor = PathBuf::from(value("--request-anchor")?);
|
||||
enroll_persona_discord_request_identity(&identity)?;
|
||||
export_persona_discord_request_anchor_candidate(&identity, &anchor, "epiphany-yggdrasil")?;
|
||||
let signer = open_persona_discord_request_identity(&identity)?;
|
||||
let issued = chrono::Utc::now();
|
||||
let request = sign_persona_discord_delivery_request(
|
||||
&signer,
|
||||
"rust-crossing-smoke-1".into(),
|
||||
"effect-rust-crossing-smoke-1".into(),
|
||||
"epiphany-yggdrasil".into(),
|
||||
"epiphany.Persona".into(),
|
||||
"123".into(),
|
||||
"".into(),
|
||||
"Rust-authored crossing smoke".into(),
|
||||
issued.to_rfc3339(),
|
||||
(issued + chrono::Duration::seconds(120)).to_rfc3339(),
|
||||
)?;
|
||||
insert_persona_discord_delivery_request(&store, &request)?;
|
||||
println!("{}", request.request_id);
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
use std::{env, net::SocketAddr, path::PathBuf, time::Duration};
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use epiphany_core::{
|
||||
enroll_persona_discord_request_identity, exchange_persona_discord_delivery_rudp,
|
||||
export_persona_discord_request_anchor_candidate, insert_persona_discord_delivery_request,
|
||||
load_persona_discord_receipt_anchor, open_persona_discord_request_identity,
|
||||
sign_persona_discord_delivery_request,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let values = args()?;
|
||||
let request_store = path(&values, "--request-store")?;
|
||||
let receipt_store = path(&values, "--receipt-store")?;
|
||||
let identity_store = path(&values, "--identity-store")?;
|
||||
let request_anchor = path(&values, "--request-anchor")?;
|
||||
let receipt_anchor = path(&values, "--receipt-anchor")?;
|
||||
let endpoint: SocketAddr = value(&values, "--endpoint")?.parse()?;
|
||||
enroll_persona_discord_request_identity(&identity_store)?;
|
||||
export_persona_discord_request_anchor_candidate(
|
||||
&identity_store,
|
||||
&request_anchor,
|
||||
"epiphany-starfire",
|
||||
)?;
|
||||
let signer = open_persona_discord_request_identity(&identity_store)?;
|
||||
let issued = chrono::Utc::now();
|
||||
let request = sign_persona_discord_delivery_request(
|
||||
&signer,
|
||||
"persona-discord:rudp-cross-language:0".into(),
|
||||
"effect-rudp-cross-language".into(),
|
||||
"epiphany-starfire".into(),
|
||||
"epiphany.Persona".into(),
|
||||
"123".into(),
|
||||
"456".into(),
|
||||
"Cross-language Persona nerve smoke.".into(),
|
||||
issued.to_rfc3339(),
|
||||
(issued + chrono::Duration::seconds(90)).to_rfc3339(),
|
||||
)?;
|
||||
insert_persona_discord_delivery_request(&request_store, &request)?;
|
||||
let anchor = load_persona_discord_receipt_anchor(&receipt_anchor)?;
|
||||
let receipt = exchange_persona_discord_delivery_rudp(
|
||||
endpoint,
|
||||
"epiphany-starfire",
|
||||
&request_store,
|
||||
&receipt_store,
|
||||
&request,
|
||||
&anchor,
|
||||
Duration::from_secs(5),
|
||||
)?;
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string(&serde_json::json!({
|
||||
"requestId": receipt.request_id,
|
||||
"status": receipt.status,
|
||||
"messageId": receipt.message_id,
|
||||
"providerIdentityId": receipt.provider_identity_id,
|
||||
}))?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn args() -> Result<std::collections::BTreeMap<String, String>> {
|
||||
let mut values = std::collections::BTreeMap::new();
|
||||
let mut args = env::args().skip(1);
|
||||
while let Some(key) = args.next() {
|
||||
values.insert(
|
||||
key,
|
||||
args.next()
|
||||
.ok_or_else(|| anyhow!("argument requires value"))?,
|
||||
);
|
||||
}
|
||||
Ok(values)
|
||||
}
|
||||
fn value(values: &std::collections::BTreeMap<String, String>, key: &str) -> Result<String> {
|
||||
values
|
||||
.get(key)
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow!("{key} is required"))
|
||||
}
|
||||
fn path(values: &std::collections::BTreeMap<String, String>, key: &str) -> Result<PathBuf> {
|
||||
Ok(value(values, key)?.into())
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,184 +0,0 @@
|
|||
use anyhow::{Context, Result, bail};
|
||||
use epiphany_core::*;
|
||||
use epiphany_state_model::{
|
||||
EpiphanyMemoryDomain, EpiphanyMemoryLifecycle, EpiphanyMemoryNode, EpiphanyMemoryNodeKind,
|
||||
EpiphanyMemoryProfile,
|
||||
};
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
const AT: &str = "2026-07-18T00:00:00Z";
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = arguments()?;
|
||||
let feedback_store = path(&args, "--persona-feedback-store")?;
|
||||
let runtime_store = path(&args, "--runtime-store")?;
|
||||
let forbidden = [
|
||||
"--mind-store",
|
||||
"--release-store",
|
||||
"--local-verse-store",
|
||||
"--public-store",
|
||||
]
|
||||
.map(|flag| path(&args, flag))
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let before = forbidden
|
||||
.iter()
|
||||
.map(|p| Ok((p.clone(), bytes(p)?)))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
seed_fixture_runtime(&runtime_store)?;
|
||||
let admitted = admitted_persona_feedback(&feedback_store, "epiphany-yggdrasil")?;
|
||||
let classes = admitted
|
||||
.iter()
|
||||
.map(|f| (f.source_visibility.as_str(), f.data_classification.as_str()))
|
||||
.collect::<BTreeSet<_>>();
|
||||
let expected = BTreeSet::from([
|
||||
("public", "public_feedback"),
|
||||
("organization", "organization_feedback"),
|
||||
("private", "private_feedback"),
|
||||
]);
|
||||
if classes != expected {
|
||||
bail!("fixture requires exact public, organization, and private feedback classes");
|
||||
}
|
||||
|
||||
bind_runtime_repository_domain(&runtime_store, "GameCult/Epiphany", "2026-07-18T00:00:00Z")?;
|
||||
let materialized = materialize_resident_self_domain_obligations(
|
||||
&runtime_store,
|
||||
&feedback_store,
|
||||
"epiphany-yggdrasil",
|
||||
"GameCult/Epiphany",
|
||||
runtime_store.parent().unwrap().to_str().unwrap(),
|
||||
1_752_796_800_000,
|
||||
)?;
|
||||
if materialized < admitted.len() {
|
||||
bail!("resident Self did not materialize every admitted feedback obligation");
|
||||
}
|
||||
for feedback in admitted {
|
||||
let request = commit_imagination_consideration_request(
|
||||
&runtime_store,
|
||||
&feedback_store,
|
||||
&feedback.feedback_id,
|
||||
"GameCult/Epiphany",
|
||||
"epiphany",
|
||||
"resident-feedback-consideration-v0",
|
||||
AT,
|
||||
)?
|
||||
.ok_or_else(|| anyhow::anyhow!("fixture thread did not admit consideration"))?;
|
||||
if request.source_visibility != feedback.source_visibility
|
||||
|| request.data_classification != feedback.data_classification
|
||||
|| request.source_room_id != feedback.source_room_id
|
||||
{
|
||||
bail!("consideration lost classified source provenance");
|
||||
}
|
||||
let job_id =
|
||||
launch_current_imagination_consideration_work(&runtime_store, &request.request_id, AT)?;
|
||||
let candidate = ImaginationConsiderationCandidate {
|
||||
schema_version: IMAGINATION_CONSIDERATION_CANDIDATE_SCHEMA_VERSION.into(),
|
||||
candidate_id: imagination_consideration_candidate_id_for_launch(
|
||||
&request.request_id,
|
||||
&job_id,
|
||||
),
|
||||
request_id: request.request_id.clone(),
|
||||
feedback_id: request.feedback_id.clone(),
|
||||
feedback_packet_sha256: request.feedback_packet_sha256.clone(),
|
||||
source_room_id: request.source_room_id.clone(),
|
||||
source_visibility: request.source_visibility.clone(),
|
||||
data_classification: request.data_classification.clone(),
|
||||
model_projection_digest: request.model_projection_digest.clone(),
|
||||
model_source_documents: request.model_source_documents.clone(),
|
||||
disposition: ImaginationConsiderationDisposition::Suggest,
|
||||
title: "Fixture proposal".into(),
|
||||
summary: "Reviewable option only".into(),
|
||||
rationale: "Compared exact admitted feedback with the current Modeling map.".into(),
|
||||
option_drafts: vec![ImaginationOptionDraft {
|
||||
title: "Review".into(),
|
||||
summary: "Ask Modeling to review; do not mutate state.".into(),
|
||||
}],
|
||||
uncertainties: Vec::new(),
|
||||
evidence_refs: request.quoted_evidence.source_discussion_refs.clone(),
|
||||
recommended_review_route: ImaginationConsiderationReviewRoute::ModelingReview,
|
||||
proposed_at: AT.into(),
|
||||
contract: IMAGINATION_CONSIDERATION_CANDIDATE_CONTRACT.into(),
|
||||
};
|
||||
validate_imagination_consideration_candidate(&request, &candidate)?;
|
||||
}
|
||||
for (path, original) in before {
|
||||
if bytes(&path)? != original {
|
||||
bail!("fixture mutated forbidden store {}", path.display());
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"{{\"schemaVersion\":\"epiphany.persona_feedback_causal_smoke.v0\",\"status\":\"passed\",\"privateStateExposed\":false}}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn seed_fixture_runtime(store: &Path) -> Result<()> {
|
||||
initialize_runtime_spine(
|
||||
store,
|
||||
RuntimeSpineInitOptions {
|
||||
runtime_id: "epiphany-yggdrasil".into(),
|
||||
display_name: "fixture".into(),
|
||||
created_at: AT.into(),
|
||||
},
|
||||
)?;
|
||||
let seed = EpiphanyRepoModelSeed::new(
|
||||
"fixture-model-seed",
|
||||
"fixture-model",
|
||||
"epiphany-yggdrasil",
|
||||
"fixture-workspace",
|
||||
"fixture-body-binding",
|
||||
EpiphanyRepoModelSeedDocuments {
|
||||
domains: vec![EpiphanyMemoryDomain {
|
||||
id: "repo".into(),
|
||||
profile: EpiphanyMemoryProfile::RepoArchitecture,
|
||||
title: "Repository".into(),
|
||||
lifecycle: EpiphanyMemoryLifecycle::Accepted,
|
||||
..Default::default()
|
||||
}],
|
||||
nodes: vec![EpiphanyMemoryNode {
|
||||
id: "fixture-claim".into(),
|
||||
domain_id: "repo".into(),
|
||||
profile: EpiphanyMemoryProfile::RepoArchitecture,
|
||||
kind: EpiphanyMemoryNodeKind::RuntimeContract,
|
||||
title: "Feedback path".into(),
|
||||
claim: "Persona feedback is pressure, never authority.".into(),
|
||||
question: "Which proposal fits the Body?".into(),
|
||||
action_implication: "Review only.".into(),
|
||||
lifecycle: EpiphanyMemoryLifecycle::Accepted,
|
||||
..Default::default()
|
||||
}],
|
||||
edges: Vec::new(),
|
||||
summaries: Vec::new(),
|
||||
frontier: Vec::new(),
|
||||
lifecycle_receipts: Vec::new(),
|
||||
},
|
||||
)?;
|
||||
initialize_keyed_repo_model(store, &seed, AT)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn arguments() -> Result<BTreeMap<String, String>> {
|
||||
let mut map = BTreeMap::new();
|
||||
let mut args = env::args().skip(1);
|
||||
while let Some(k) = args.next() {
|
||||
map.insert(k, args.next().context("missing argument value")?);
|
||||
}
|
||||
Ok(map)
|
||||
}
|
||||
fn path(args: &BTreeMap<String, String>, key: &str) -> Result<PathBuf> {
|
||||
Ok(PathBuf::from(
|
||||
args.get(key).with_context(|| format!("missing {key}"))?,
|
||||
))
|
||||
}
|
||||
fn bytes(path: &Path) -> Result<Vec<u8>> {
|
||||
Ok(if path.exists() {
|
||||
fs::read(path)?
|
||||
} else {
|
||||
Vec::new()
|
||||
})
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
use epiphany_core::{
|
||||
EpiphanyMemoryContextPacket, EpiphanyMemoryFreshnessStatus, EpiphanyMemoryNode,
|
||||
EpiphanyMemorySummary, PersonaIdentity, PersonaInterpreterInput, PersonaProjectorInput,
|
||||
PersonaTurnInput, build_persona_interpreter_prompt, build_persona_projector_prompt,
|
||||
build_persona_turn_prompt, render_persona_semantic_memory_recall,
|
||||
semantic_memory_recall_from_heartbeat_action,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
fn main() {
|
||||
let packet = EpiphanyMemoryContextPacket {
|
||||
id: "memctx-persona-smoke".to_string(),
|
||||
query_id: "persona-smoke-current-turn".to_string(),
|
||||
summaries: vec![EpiphanyMemorySummary {
|
||||
id: "summary-persona-contracts".to_string(),
|
||||
target: "role:Persona".to_string(),
|
||||
claim: "Persona remembers that public speech is a reviewed mouth edge, not raw side effect authority."
|
||||
.to_string(),
|
||||
action_implication:
|
||||
"Shape the public voice, then let Mind and the mouth edge route effects."
|
||||
.to_string(),
|
||||
freshness: EpiphanyMemoryFreshnessStatus::Ready,
|
||||
confidence: 84,
|
||||
..Default::default()
|
||||
}],
|
||||
nodes: vec![EpiphanyMemoryNode {
|
||||
id: "node-persona-shared-mind".to_string(),
|
||||
title: "Shared Mind semantic projection".to_string(),
|
||||
claim: "Persona recall resolves canonical Mind graph documents after semantic ranking; Qdrant payload text never speaks into the prompt."
|
||||
.to_string(),
|
||||
action_implication:
|
||||
"Use the canonical packet as a hint and leave durable state with Mind."
|
||||
.to_string(),
|
||||
..Default::default()
|
||||
}],
|
||||
warnings: vec![
|
||||
"semantic projection ranked canonical Mind candidates; payload text was ignored"
|
||||
.to_string(),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
let rendered = render_persona_semantic_memory_recall(&packet);
|
||||
assert_contains(&rendered, "Shared Mind semantic projection");
|
||||
assert_contains(&rendered, "payload text was ignored");
|
||||
|
||||
let heartbeat_action = json!({
|
||||
"action_type": "persona_turn",
|
||||
"persona_memory_recall": {
|
||||
"privateStateExposed": false,
|
||||
"renderedRecall": rendered,
|
||||
}
|
||||
});
|
||||
let recall = semantic_memory_recall_from_heartbeat_action(&heartbeat_action);
|
||||
assert_contains(&recall, "canonical Mind graph documents");
|
||||
|
||||
let identity = PersonaIdentity {
|
||||
identity_id: "epiphany".to_string(),
|
||||
display_name: "Epiphany".to_string(),
|
||||
repo_name: "EpiphanyAgent".to_string(),
|
||||
public_description: "Repo Persona for typed agent substrate.".to_string(),
|
||||
jurisdiction: vec!["typed state and review-gated agency".to_string()],
|
||||
};
|
||||
let projector_prompt = build_persona_projector_prompt(&PersonaProjectorInput {
|
||||
identity: identity.clone(),
|
||||
semantic_memory_recall: recall.clone(),
|
||||
..Default::default()
|
||||
});
|
||||
let persona_prompt = build_persona_turn_prompt(&PersonaTurnInput {
|
||||
identity: identity.clone(),
|
||||
projected_state: "The public mouth remains receipt-bound.".to_string(),
|
||||
});
|
||||
let persona_output = "I can speak, but consequence still needs a receipt.";
|
||||
let interpreter_prompt = build_persona_interpreter_prompt(&PersonaInterpreterInput {
|
||||
identity,
|
||||
persona_prompt: "The public mouth remains receipt-bound.".to_string(),
|
||||
persona_output: persona_output.to_string(),
|
||||
semantic_memory_recall: recall.clone(),
|
||||
dynamic_semantic_memory_recall: recall,
|
||||
pending_mentions: Vec::new(),
|
||||
allowed_channel_ids: vec!["aquarium".to_string()],
|
||||
});
|
||||
assert_contains(&projector_prompt, "Semantic memory recall");
|
||||
assert_not_contains(&persona_prompt, "canonical Mind graph documents");
|
||||
assert_contains(&interpreter_prompt, "Dynamic self-memory recall");
|
||||
|
||||
println!(
|
||||
"status=ok recallSource=shared-mind-memory-graph qdrantPayloadAuthority=false canonicalReload=true heartbeatRecall=prompt-wired privateStateExposed=false"
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_contains(haystack: &str, needle: &str) {
|
||||
assert!(
|
||||
haystack.contains(needle),
|
||||
"expected prompt to contain `{needle}`"
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_not_contains(haystack: &str, needle: &str) {
|
||||
assert!(
|
||||
!haystack.contains(needle),
|
||||
"expected prompt not to contain `{needle}`"
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,320 +0,0 @@
|
|||
use anyhow::{Context, Result, bail};
|
||||
use epiphany_core::*;
|
||||
use epiphany_model_adapter::EpiphanyModelRequest;
|
||||
use epiphany_tool_adapter::{
|
||||
EPIPHANY_TOOL_RUNTIME_ADAPTER_ID, EpiphanyToolInvocationIntent, EpiphanyToolInvocationReceipt,
|
||||
tool_invocation_intent_key, tool_invocation_receipt_key,
|
||||
};
|
||||
use serde_json::json;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const WORKER_JOB_ID: &str = "public-source-no-grant-worker";
|
||||
const MODEL_REQUEST_ID: &str = "public-source-no-grant-model-request";
|
||||
const MODEL_JOB_ID: &str = "public-source-no-grant-model-job";
|
||||
const MODEL_SESSION_ID: &str = "public-source-no-grant-model-session";
|
||||
const HOSTILE_INTENT_ID: &str = "public-source-no-grant-intent";
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let (command, fixture_root) = parse_command()?;
|
||||
let proof = match command.as_str() {
|
||||
"admission-no-grant" => {
|
||||
let store = create_fixture_root(&fixture_root)?;
|
||||
prove_admission_no_grant_refusal(&store)?
|
||||
}
|
||||
"prepare-execution-no-grant" => {
|
||||
let store = create_fixture_root(&fixture_root)?;
|
||||
prepare_execution_no_grant(&fixture_root, &store)?
|
||||
}
|
||||
"verify-execution-no-grant" => verify_execution_no_grant(&fixture_root)?,
|
||||
_ => bail!(usage()),
|
||||
};
|
||||
println!("{}", serde_json::to_string_pretty(&proof)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_command() -> Result<(String, PathBuf)> {
|
||||
let mut args = env::args().skip(1);
|
||||
let command = args.next().context("missing command")?;
|
||||
if args.next().as_deref() != Some("--fixture-root") {
|
||||
bail!(usage());
|
||||
}
|
||||
let root = args.next().context("missing --fixture-root value")?;
|
||||
if args.next().is_some() {
|
||||
bail!("unexpected trailing arguments");
|
||||
}
|
||||
let root = PathBuf::from(root);
|
||||
if !root.is_absolute() {
|
||||
bail!("--fixture-root must be absolute");
|
||||
}
|
||||
Ok((command, root))
|
||||
}
|
||||
|
||||
fn usage() -> &'static str {
|
||||
"usage: epiphany-public-source-gate-probe <admission-no-grant|prepare-execution-no-grant|verify-execution-no-grant> --fixture-root PATH"
|
||||
}
|
||||
|
||||
fn create_fixture_root(fixture_root: &Path) -> Result<PathBuf> {
|
||||
if fixture_root.exists() {
|
||||
bail!(
|
||||
"public-source gate probe refuses existing fixture root {}",
|
||||
fixture_root.display()
|
||||
);
|
||||
}
|
||||
fs::create_dir_all(fixture_root)
|
||||
.with_context(|| format!("creating fixture root {}", fixture_root.display()))?;
|
||||
Ok(fixture_root.join("runtime.cc"))
|
||||
}
|
||||
|
||||
fn prepare_authority_family(store: &Path) -> Result<EpiphanyToolInvocationIntent> {
|
||||
initialize_runtime_spine(
|
||||
store,
|
||||
RuntimeSpineInitOptions {
|
||||
runtime_id: "public-source-no-grant-probe".into(),
|
||||
display_name: "Public source no-grant probe".into(),
|
||||
created_at: "2026-08-13T00:00:00Z".into(),
|
||||
},
|
||||
)?;
|
||||
let launch_document = EpiphanyWorkerLaunchDocument::Role(EpiphanyRoleWorkerLaunchDocument {
|
||||
thread_id: "public-source-no-grant-thread".into(),
|
||||
role_id: "research".into(),
|
||||
objective: Some("Prove the exact public-source grant boundary.".into()),
|
||||
dynamic_prompt_context: None,
|
||||
repository_body_observation_basis: None,
|
||||
proposal_modeling_context: None,
|
||||
frontier_verdict_modeling_context: None,
|
||||
frontier_planning_context: None,
|
||||
frontier_research_context: None,
|
||||
frontier_verification_context: None,
|
||||
frontier_plan_mind_context: None,
|
||||
imagination_consideration_context: None,
|
||||
admitted_model_direction_consideration_context: None,
|
||||
active_subgoal_id: None,
|
||||
active_subgoals: Vec::new(),
|
||||
active_graph_node_ids: Vec::new(),
|
||||
investigation_checkpoint: None,
|
||||
scratch: None,
|
||||
invariants: Vec::new(),
|
||||
graphs: None,
|
||||
recent_evidence: Vec::new(),
|
||||
recent_observations: Vec::new(),
|
||||
graph_frontier: None,
|
||||
graph_checkpoint: None,
|
||||
planning: None,
|
||||
churn: None,
|
||||
});
|
||||
let authority_scope = "epiphany.eyes.public_source_gate_probe";
|
||||
open_runtime_spine_heartbeat_job(
|
||||
store,
|
||||
RuntimeSpineHeartbeatJobOptions {
|
||||
runtime_id: "public-source-no-grant-probe".into(),
|
||||
display_name: "Public source no-grant probe".into(),
|
||||
session_id: EPIPHANY_RUNTIME_ROOT_SESSION_ID.into(),
|
||||
objective: "Prove the exact public-source grant boundary.".into(),
|
||||
coordinator_note: "isolated gate probe".into(),
|
||||
job_id: WORKER_JOB_ID.into(),
|
||||
role: EPIPHANY_RESEARCH_OWNER_ROLE.into(),
|
||||
binding_id: EPIPHANY_RESEARCH_ROLE_BINDING_ID.into(),
|
||||
authority_scope: authority_scope.into(),
|
||||
instruction: "Attempt one immutable public source lookup.".into(),
|
||||
output_contract_id: ROLE_WORKER_OUTPUT_CONTRACT_ID.into(),
|
||||
organ_launch_contract: default_launch_organ_contract(
|
||||
authority_scope,
|
||||
"role",
|
||||
ROLE_WORKER_OUTPUT_CONTRACT_ID,
|
||||
),
|
||||
launch_document,
|
||||
proposal_modeling_request_id: None,
|
||||
frontier_planning_request_id: None,
|
||||
frontier_plan_mind_request_id: None,
|
||||
imagination_consideration_request_id: None,
|
||||
admitted_model_direction_consideration_request_id: None,
|
||||
repo_frontier_modeling_request_id: None,
|
||||
repo_frontier_research_request_id: None,
|
||||
repo_frontier_verification_request_id: None,
|
||||
created_at: "2026-08-13T00:00:01Z".into(),
|
||||
},
|
||||
)?;
|
||||
runtime_spine_cache(store)?.put(
|
||||
&format!("substrate-grant-{WORKER_JOB_ID}"),
|
||||
&substrate_gate_repo_access_grant_for_worker(
|
||||
format!("substrate-grant-{WORKER_JOB_ID}"),
|
||||
WORKER_JOB_ID.into(),
|
||||
EPIPHANY_RESEARCH_ROLE_BINDING_ID.into(),
|
||||
EPIPHANY_RESEARCH_OWNER_ROLE.into(),
|
||||
authority_scope.into(),
|
||||
true,
|
||||
"2026-08-13T00:00:01Z".into(),
|
||||
),
|
||||
)?;
|
||||
|
||||
let mut model_request = EpiphanyModelRequest::new(
|
||||
MODEL_REQUEST_ID,
|
||||
"public-source-no-grant-conversation",
|
||||
"openai-codex",
|
||||
"gpt-probe",
|
||||
"Attempt one immutable public source read.",
|
||||
);
|
||||
model_request.source_worker_job_id = Some(WORKER_JOB_ID.into());
|
||||
open_runtime_model_execution(
|
||||
store,
|
||||
RuntimeSpineSessionOptions {
|
||||
session_id: MODEL_SESSION_ID.into(),
|
||||
objective: "Prove refusal before public-source execution.".into(),
|
||||
created_at: "2026-08-13T00:00:02Z".into(),
|
||||
coordinator_note: "isolated packaged gate probe".into(),
|
||||
},
|
||||
RuntimeSpineJobOptions {
|
||||
job_id: MODEL_JOB_ID.into(),
|
||||
session_id: MODEL_SESSION_ID.into(),
|
||||
role: "openai-model-adapter".into(),
|
||||
created_at: "2026-08-13T00:00:02Z".into(),
|
||||
summary: "Bound hostile public-source model turn.".into(),
|
||||
artifact_refs: Vec::new(),
|
||||
},
|
||||
&model_request,
|
||||
"2026-08-13T00:00:02Z",
|
||||
)?;
|
||||
|
||||
Ok(EpiphanyToolInvocationIntent::new(
|
||||
HOSTILE_INTENT_ID,
|
||||
EPIPHANY_TOOL_RUNTIME_ADAPTER_ID,
|
||||
"epiphany_public",
|
||||
"github_file",
|
||||
r#"{"owner":"GameCult","repository":"Epiphany","revision":"0123456789abcdef0123456789abcdef01234567","path":"README.md"}"#,
|
||||
"gate-probe",
|
||||
"Prove missing publicSourceRead refuses before adapter execution.",
|
||||
"2026-08-13T00:00:03Z",
|
||||
)
|
||||
.with_model_call("public-source-no-grant-call", MODEL_REQUEST_ID))
|
||||
}
|
||||
|
||||
fn remove_public_source_grant(store: &Path) -> Result<()> {
|
||||
let grant_id = format!("substrate-grant-{WORKER_JOB_ID}");
|
||||
let mut cache = runtime_spine_cache(store)?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
let mut grant = cache
|
||||
.get::<SubstrateGateRepoAccessGrantReceipt>(&grant_id)?
|
||||
.context("Research launch omitted its exact Substrate Gate grant")?;
|
||||
grant
|
||||
.granted_operations
|
||||
.retain(|operation| operation != "publicSourceRead");
|
||||
cache.put(&grant_id, &grant)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prove_admission_no_grant_refusal(store: &Path) -> Result<serde_json::Value> {
|
||||
let hostile = prepare_authority_family(store)?;
|
||||
remove_public_source_grant(store)?;
|
||||
let before = fs::read(store)?;
|
||||
let error = put_runtime_tool_execution_intent(
|
||||
store,
|
||||
MODEL_SESSION_ID,
|
||||
MODEL_JOB_ID,
|
||||
&hostile,
|
||||
"2026-08-13T00:00:03Z",
|
||||
)
|
||||
.expect_err("hostile public-source intent unexpectedly acquired authority");
|
||||
let after = fs::read(store)?;
|
||||
if before != after {
|
||||
bail!("no-grant refusal changed the canonical runtime store");
|
||||
}
|
||||
|
||||
let mut reloaded = runtime_spine_cache(store)?;
|
||||
reloaded.pull_all_backing_stores()?;
|
||||
let binding_absent = reloaded
|
||||
.get::<EpiphanyRuntimeToolExecutionBinding>(HOSTILE_INTENT_ID)?
|
||||
.is_none();
|
||||
let intent_absent = reloaded
|
||||
.get::<EpiphanyToolInvocationIntent>(&tool_invocation_intent_key(HOSTILE_INTENT_ID))?
|
||||
.is_none();
|
||||
let receipt_absent = reloaded
|
||||
.get::<EpiphanyToolInvocationReceipt>(&tool_invocation_receipt_key(HOSTILE_INTENT_ID))?
|
||||
.is_none();
|
||||
if !(binding_absent && intent_absent && receipt_absent) {
|
||||
bail!("no-grant refusal persisted forbidden execution authority");
|
||||
}
|
||||
|
||||
Ok(json!({
|
||||
"schemaVersion": "epiphany.public_source_admission_no_grant_gate_proof.v0",
|
||||
"status": "passed",
|
||||
"fixtureStore": store,
|
||||
"requiredOperation": "publicSourceRead",
|
||||
"admissionOwner": "put_runtime_tool_execution_intent",
|
||||
"refusedBeforeAdapterExecution": true,
|
||||
"storeByteIdentical": true,
|
||||
"bindingAbsent": binding_absent,
|
||||
"intentAbsent": intent_absent,
|
||||
"receiptAbsent": receipt_absent,
|
||||
"error": format!("{error:#}"),
|
||||
"privateStateExposed": false
|
||||
}))
|
||||
}
|
||||
|
||||
fn prepare_execution_no_grant(fixture_root: &Path, store: &Path) -> Result<serde_json::Value> {
|
||||
let hostile = prepare_authority_family(store)?;
|
||||
put_runtime_tool_execution_intent(
|
||||
store,
|
||||
MODEL_SESSION_ID,
|
||||
MODEL_JOB_ID,
|
||||
&hostile,
|
||||
"2026-08-13T00:00:03Z",
|
||||
)?;
|
||||
remove_public_source_grant(store)?;
|
||||
let digest = sha256(&fs::read(store)?);
|
||||
fs::write(fixture_root.join("before.sha256"), format!("{digest}\n"))?;
|
||||
Ok(json!({
|
||||
"schemaVersion": "epiphany.public_source_execution_no_grant_gate_fixture.v0",
|
||||
"status": "prepared",
|
||||
"fixtureStore": store,
|
||||
"intentId": HOSTILE_INTENT_ID,
|
||||
"requiredOperation": "publicSourceRead",
|
||||
"storeSha256": digest,
|
||||
"expectedPackagedCommand": ["epiphany-tool-mcp-runtime", "run", "--store", store, "--intent-id", HOSTILE_INTENT_ID],
|
||||
"privateStateExposed": false
|
||||
}))
|
||||
}
|
||||
|
||||
fn verify_execution_no_grant(fixture_root: &Path) -> Result<serde_json::Value> {
|
||||
let store = fixture_root.join("runtime.cc");
|
||||
let expected_digest = fs::read_to_string(fixture_root.join("before.sha256"))?
|
||||
.trim()
|
||||
.to_string();
|
||||
let actual_digest = sha256(&fs::read(&store)?);
|
||||
if actual_digest != expected_digest {
|
||||
bail!("packaged no-grant refusal changed the canonical runtime store");
|
||||
}
|
||||
let mut cache = runtime_spine_cache(&store)?;
|
||||
cache.pull_all_backing_stores()?;
|
||||
let binding_present = cache
|
||||
.get::<EpiphanyRuntimeToolExecutionBinding>(HOSTILE_INTENT_ID)?
|
||||
.is_some();
|
||||
let intent_present = cache
|
||||
.get::<EpiphanyToolInvocationIntent>(&tool_invocation_intent_key(HOSTILE_INTENT_ID))?
|
||||
.is_some();
|
||||
let receipt_absent = cache
|
||||
.get::<EpiphanyToolInvocationReceipt>(&tool_invocation_receipt_key(HOSTILE_INTENT_ID))?
|
||||
.is_none();
|
||||
if !(binding_present && intent_present && receipt_absent) {
|
||||
bail!("packaged no-grant refusal left an invalid execution family");
|
||||
}
|
||||
Ok(json!({
|
||||
"schemaVersion": "epiphany.public_source_execution_no_grant_gate_proof.v0",
|
||||
"status": "passed",
|
||||
"fixtureStore": store,
|
||||
"intentId": HOSTILE_INTENT_ID,
|
||||
"executionOwner": "require_runtime_tool_execution_binding",
|
||||
"storeByteIdentical": true,
|
||||
"bindingPresent": binding_present,
|
||||
"intentPresent": intent_present,
|
||||
"receiptAbsent": receipt_absent,
|
||||
"refusedBeforeAdapterExecution": true,
|
||||
"privateStateExposed": false
|
||||
}))
|
||||
}
|
||||
|
||||
fn sha256(bytes: &[u8]) -> String {
|
||||
format!("{:x}", Sha256::digest(bytes))
|
||||
}
|
||||
|
|
@ -1,253 +0,0 @@
|
|||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let result = run_smoke()?;
|
||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_smoke() -> Result<Value> {
|
||||
let root = env::current_dir().context("failed to resolve current dir")?;
|
||||
let workspace = root.join(".epiphany-smoke").join("rider-bridge-workspace");
|
||||
let artifact_root = root.join(".epiphany-smoke").join("rider-bridge-artifacts");
|
||||
let fake_rider_root = root.join(".epiphany-smoke").join("rider-bridge-install");
|
||||
let result_path = root
|
||||
.join(".epiphany-smoke")
|
||||
.join("rider-bridge-smoke-result.json");
|
||||
let source = prepare_workspace(&workspace)?;
|
||||
reset_path(&artifact_root)?;
|
||||
reset_path(&fake_rider_root)?;
|
||||
let fake_rider = create_fake_rider(&fake_rider_root)?;
|
||||
|
||||
let status = run_bridge(&root, &workspace, &artifact_root, &fake_rider, &["status"])?;
|
||||
require(
|
||||
status["status"] == "discovered",
|
||||
"fake Rider path should be reported as discovered",
|
||||
)?;
|
||||
require(
|
||||
status["riderPath"] == fake_rider.to_string_lossy().as_ref(),
|
||||
"status should resolve fake Rider path",
|
||||
)?;
|
||||
require(
|
||||
status
|
||||
.get("solutionPath")
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|path| path.ends_with("Aetheria.sln")),
|
||||
"status should find the workspace solution",
|
||||
)?;
|
||||
require_artifact(&status, "rider-bridge-summary.json")?;
|
||||
require_artifact(&status, "rider-bridge-status.md")?;
|
||||
|
||||
let context = run_bridge(
|
||||
&root,
|
||||
&workspace,
|
||||
&artifact_root,
|
||||
&fake_rider,
|
||||
&[
|
||||
"context",
|
||||
"--file",
|
||||
source.to_str().unwrap_or_default(),
|
||||
"--selection-start",
|
||||
"3",
|
||||
"--selection-end",
|
||||
"5",
|
||||
"--symbol-name",
|
||||
"GravityTileRenderer",
|
||||
"--symbol-kind",
|
||||
"class",
|
||||
"--symbol-namespace",
|
||||
"Aetheria.Rendering",
|
||||
],
|
||||
)?;
|
||||
require(
|
||||
context["status"] == "captured",
|
||||
"context command should capture a packet",
|
||||
)?;
|
||||
require(
|
||||
context["filePath"] == "Assets/Scripts/GravityTileRenderer.cs",
|
||||
"context file should be project-relative",
|
||||
)?;
|
||||
require_artifact(&context, "rider-context.json")?;
|
||||
require_artifact(&context, "rider-context.md")?;
|
||||
|
||||
let open_ref = run_bridge(
|
||||
&root,
|
||||
&workspace,
|
||||
&artifact_root,
|
||||
&fake_rider,
|
||||
&[
|
||||
"open-ref",
|
||||
"--file",
|
||||
source.to_str().unwrap_or_default(),
|
||||
"--line",
|
||||
"3",
|
||||
],
|
||||
)?;
|
||||
require(
|
||||
open_ref["status"] == "planned",
|
||||
"open-ref should plan without launch",
|
||||
)?;
|
||||
require(
|
||||
open_ref
|
||||
.get("command")
|
||||
.and_then(Value::as_array)
|
||||
.and_then(|items| items.first())
|
||||
.and_then(Value::as_str)
|
||||
== Some(fake_rider.to_str().unwrap_or_default()),
|
||||
"open-ref should use discovered Rider path",
|
||||
)?;
|
||||
require_artifact(&open_ref, "rider-open-ref.json")?;
|
||||
|
||||
let result = json!({
|
||||
"workspace": workspace,
|
||||
"artifactRoot": artifact_root,
|
||||
"status": status["status"],
|
||||
"contextStatus": context["status"],
|
||||
"openRefStatus": open_ref["status"],
|
||||
"riderPath": status["riderPath"],
|
||||
});
|
||||
write_json(&result_path, &result)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn run_bridge(
|
||||
root: &Path,
|
||||
workspace: &Path,
|
||||
artifact_root: &Path,
|
||||
rider_path: &Path,
|
||||
args: &[&str],
|
||||
) -> Result<Value> {
|
||||
let bridge = native_bridge_exe()?;
|
||||
ensure_bridge_built(root, "epiphany-rider-bridge", &bridge)?;
|
||||
let output = Command::new(bridge)
|
||||
.current_dir(root)
|
||||
.env("EPIPHANY_RIDER_PATH", rider_path)
|
||||
.arg(args[0])
|
||||
.arg("--project-root")
|
||||
.arg(workspace)
|
||||
.arg("--artifact-root")
|
||||
.arg(artifact_root)
|
||||
.args(&args[1..])
|
||||
.output()
|
||||
.context("failed to run native rider bridge")?;
|
||||
require(
|
||||
output.status.success(),
|
||||
&format!(
|
||||
"rider bridge failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
),
|
||||
)?;
|
||||
serde_json::from_slice(&output.stdout).with_context(|| {
|
||||
format!(
|
||||
"rider bridge did not return JSON: {}",
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn prepare_workspace(workspace: &Path) -> Result<PathBuf> {
|
||||
reset_path(workspace)?;
|
||||
fs::write(
|
||||
workspace.join("Aetheria.sln"),
|
||||
"Microsoft Visual Studio Solution File\n",
|
||||
)?;
|
||||
let source = workspace
|
||||
.join("Assets")
|
||||
.join("Scripts")
|
||||
.join("GravityTileRenderer.cs");
|
||||
fs::create_dir_all(source.parent().expect("source path has parent"))?;
|
||||
fs::write(
|
||||
&source,
|
||||
"namespace Aetheria.Rendering\n{\n public sealed class GravityTileRenderer\n {\n }\n}\n",
|
||||
)?;
|
||||
Ok(source)
|
||||
}
|
||||
|
||||
fn create_fake_rider(root: &Path) -> Result<PathBuf> {
|
||||
let rider = root
|
||||
.join("JetBrains Rider 2026.1.0.1")
|
||||
.join("bin")
|
||||
.join("rider64.exe");
|
||||
fs::create_dir_all(rider.parent().expect("rider path has parent"))?;
|
||||
fs::write(&rider, "fake rider binary for smoke planning only\n")?;
|
||||
Ok(rider)
|
||||
}
|
||||
|
||||
fn require_artifact(summary: &Value, name: &str) -> Result<()> {
|
||||
let artifact_path = summary
|
||||
.get("artifactPath")
|
||||
.and_then(Value::as_str)
|
||||
.map(PathBuf::from)
|
||||
.ok_or_else(|| anyhow!("missing artifactPath"))?;
|
||||
require(
|
||||
artifact_path.join(name).exists(),
|
||||
&format!("missing rider artifact: {name}"),
|
||||
)
|
||||
}
|
||||
|
||||
fn native_bridge_exe() -> Result<PathBuf> {
|
||||
let target_dir = env::var_os("CARGO_TARGET_DIR")
|
||||
.unwrap_or_else(|| r"C:\Users\Meta\.cargo-target-codex".into());
|
||||
Ok(PathBuf::from(target_dir)
|
||||
.join("debug")
|
||||
.join(format!("epiphany-rider-bridge{}", env::consts::EXE_SUFFIX)))
|
||||
}
|
||||
|
||||
fn ensure_bridge_built(root: &Path, bin: &str, exe: &Path) -> Result<()> {
|
||||
if exe.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
let status = Command::new("cargo")
|
||||
.current_dir(root)
|
||||
.arg("build")
|
||||
.arg("--manifest-path")
|
||||
.arg(root.join("epiphany-core").join("Cargo.toml"))
|
||||
.arg("--bin")
|
||||
.arg(bin)
|
||||
.status()
|
||||
.with_context(|| format!("failed to build native {bin}"))?;
|
||||
require(
|
||||
status.success() && exe.exists(),
|
||||
&format!("native {bin} executable was not built at {}", exe.display()),
|
||||
)
|
||||
}
|
||||
|
||||
fn reset_path(path: &Path) -> Result<()> {
|
||||
if path.exists() {
|
||||
let root = env::current_dir()?.join(".epiphany-smoke").canonicalize()?;
|
||||
let resolved = path.canonicalize()?;
|
||||
if resolved == root || !resolved.starts_with(&root) {
|
||||
return Err(anyhow!(
|
||||
"refusing to reset non-smoke path: {}",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
fs::remove_dir_all(path)?;
|
||||
}
|
||||
fs::create_dir_all(path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_json(path: &Path, value: &Value) -> Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::write(path, format!("{}\n", serde_json::to_string_pretty(value)?))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn require(condition: bool, message: &str) -> Result<()> {
|
||||
if condition {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("{message}"))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,656 +0,0 @@
|
|||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use serde_json::json;
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
const RIDER_EXECUTABLE_NAMES: &[&str] = &["rider64.exe", "rider.exe", "rider.bat", "rider"];
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut args = env::args().skip(1);
|
||||
let Some(command) = args.next() else {
|
||||
usage()?;
|
||||
return Ok(());
|
||||
};
|
||||
match run_command(&command, args.collect()) {
|
||||
Ok((code, value)) => {
|
||||
println!("{}", serde_json::to_string_pretty(&value)?);
|
||||
std::process::exit(code);
|
||||
}
|
||||
Err(error) => {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"status": "error",
|
||||
"error": error.to_string(),
|
||||
}))?
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_command(command: &str, args: Vec<String>) -> Result<(i32, serde_json::Value)> {
|
||||
match command {
|
||||
"status" => {
|
||||
let options = RiderOptions::parse(args)?;
|
||||
Ok((0, status_rider(&options)?))
|
||||
}
|
||||
"context" => {
|
||||
let options = RiderOptions::parse(args)?;
|
||||
Ok((0, context_rider(&options)?))
|
||||
}
|
||||
"open-ref" => {
|
||||
let options = RiderOptions::parse(args)?;
|
||||
Ok((0, open_ref_rider(&options)?))
|
||||
}
|
||||
"guidance" => {
|
||||
let options = RiderOptions::parse(args)?;
|
||||
Ok((
|
||||
0,
|
||||
json!({ "guidance": bridge_guidance(&options.project_root)? }),
|
||||
))
|
||||
}
|
||||
_ => usage(),
|
||||
}
|
||||
}
|
||||
|
||||
fn usage() -> Result<(i32, serde_json::Value)> {
|
||||
Err(anyhow!(
|
||||
"usage: epiphany-rider-bridge <status|context|open-ref|guidance> --project-root <path>"
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RiderOptions {
|
||||
project_root: PathBuf,
|
||||
artifact_root: PathBuf,
|
||||
artifact_dir: Option<PathBuf>,
|
||||
packet: Option<PathBuf>,
|
||||
file: Option<PathBuf>,
|
||||
line: Option<u32>,
|
||||
column: Option<u32>,
|
||||
selection_start: Option<u32>,
|
||||
selection_end: Option<u32>,
|
||||
symbol_name: Option<String>,
|
||||
symbol_kind: Option<String>,
|
||||
symbol_namespace: Option<String>,
|
||||
launch: bool,
|
||||
}
|
||||
|
||||
impl RiderOptions {
|
||||
fn parse(args: Vec<String>) -> Result<Self> {
|
||||
let root = repo_root()?;
|
||||
let mut options = Self {
|
||||
project_root: env::current_dir()?,
|
||||
artifact_root: root.join(".epiphany-gui").join("rider"),
|
||||
artifact_dir: None,
|
||||
packet: None,
|
||||
file: None,
|
||||
line: None,
|
||||
column: None,
|
||||
selection_start: None,
|
||||
selection_end: None,
|
||||
symbol_name: None,
|
||||
symbol_kind: None,
|
||||
symbol_namespace: None,
|
||||
launch: false,
|
||||
};
|
||||
let mut iter = args.into_iter();
|
||||
while let Some(arg) = iter.next() {
|
||||
match arg.as_str() {
|
||||
"--project-root" => options.project_root = next_path(&mut iter, &arg)?,
|
||||
"--artifact-root" => options.artifact_root = next_path(&mut iter, &arg)?,
|
||||
"--artifact-dir" => options.artifact_dir = Some(next_path(&mut iter, &arg)?),
|
||||
"--packet" => options.packet = Some(next_path(&mut iter, &arg)?),
|
||||
"--file" => options.file = Some(next_path(&mut iter, &arg)?),
|
||||
"--line" => options.line = Some(next_value(&mut iter, &arg)?.parse()?),
|
||||
"--column" => options.column = Some(next_value(&mut iter, &arg)?.parse()?),
|
||||
"--selection-start" => {
|
||||
options.selection_start = Some(next_value(&mut iter, &arg)?.parse()?)
|
||||
}
|
||||
"--selection-end" => {
|
||||
options.selection_end = Some(next_value(&mut iter, &arg)?.parse()?)
|
||||
}
|
||||
"--symbol-name" => options.symbol_name = Some(next_value(&mut iter, &arg)?),
|
||||
"--symbol-kind" => options.symbol_kind = Some(next_value(&mut iter, &arg)?),
|
||||
"--symbol-namespace" => {
|
||||
options.symbol_namespace = Some(next_value(&mut iter, &arg)?)
|
||||
}
|
||||
"--launch" => options.launch = true,
|
||||
_ => return Err(anyhow!("unknown argument {arg:?}")),
|
||||
}
|
||||
}
|
||||
Ok(options)
|
||||
}
|
||||
}
|
||||
|
||||
fn next_value(iter: &mut impl Iterator<Item = String>, name: &str) -> Result<String> {
|
||||
iter.next()
|
||||
.ok_or_else(|| anyhow!("{name} requires a value"))
|
||||
}
|
||||
|
||||
fn next_path(iter: &mut impl Iterator<Item = String>, name: &str) -> Result<PathBuf> {
|
||||
Ok(PathBuf::from(next_value(iter, name)?))
|
||||
}
|
||||
|
||||
fn repo_root() -> Result<PathBuf> {
|
||||
Ok(Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("epiphany-core has no parent repo root"))?
|
||||
.to_path_buf())
|
||||
}
|
||||
|
||||
fn now_iso() -> String {
|
||||
chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
|
||||
}
|
||||
|
||||
fn artifact_dir(root: &Path, action: &str) -> Result<PathBuf> {
|
||||
let nanos = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
|
||||
Ok(root
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| root.to_path_buf())
|
||||
.join(format!("rider-{action}-{nanos}-{}", std::process::id())))
|
||||
}
|
||||
|
||||
fn write_json(path: &Path, value: &serde_json::Value) -> Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::write(path, format!("{}\n", serde_json::to_string_pretty(value)?))
|
||||
.with_context(|| format!("failed to write {}", path.display()))
|
||||
}
|
||||
|
||||
fn write_text(path: &Path, value: &str) -> Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::write(path, value).with_context(|| format!("failed to write {}", path.display()))
|
||||
}
|
||||
|
||||
fn maybe_resolve(path: &Path) -> String {
|
||||
clean_windows_path(
|
||||
&path
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| path.to_path_buf())
|
||||
.display()
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
fn clean_windows_path(value: &str) -> String {
|
||||
value.strip_prefix(r"\\?\").unwrap_or(value).to_string()
|
||||
}
|
||||
|
||||
fn is_inside(path: &Path, root: &Path) -> bool {
|
||||
let Ok(path) = path.canonicalize() else {
|
||||
return false;
|
||||
};
|
||||
let Ok(root) = root.canonicalize() else {
|
||||
return false;
|
||||
};
|
||||
path.starts_with(root)
|
||||
}
|
||||
|
||||
fn relative_or_absolute(path: &Path, root: &Path) -> String {
|
||||
match (path.canonicalize(), root.canonicalize()) {
|
||||
(Ok(path), Ok(root)) => path
|
||||
.strip_prefix(root)
|
||||
.map(|value| value.to_string_lossy().replace('\\', "/"))
|
||||
.unwrap_or_else(|_| path.display().to_string()),
|
||||
_ => path.display().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn dedupe_paths(paths: Vec<PathBuf>) -> Vec<PathBuf> {
|
||||
let mut seen = HashSet::new();
|
||||
let mut result = Vec::new();
|
||||
for path in paths {
|
||||
let key = maybe_resolve(&path).to_lowercase();
|
||||
if seen.insert(key) {
|
||||
result.push(path);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn rider_search_roots() -> Vec<PathBuf> {
|
||||
let mut roots = Vec::new();
|
||||
if let Ok(value) = env::var("EPIPHANY_RIDER_ROOTS") {
|
||||
roots.extend(env::split_paths(&value));
|
||||
}
|
||||
for (variable, fallback) in [
|
||||
("ProgramFiles", r"C:\Program Files".to_string()),
|
||||
("ProgramFiles(x86)", r"C:\Program Files (x86)".to_string()),
|
||||
(
|
||||
"LOCALAPPDATA",
|
||||
dirs_home()
|
||||
.join("AppData")
|
||||
.join("Local")
|
||||
.display()
|
||||
.to_string(),
|
||||
),
|
||||
] {
|
||||
let base = env::var(variable).unwrap_or(fallback);
|
||||
roots.push(PathBuf::from(base).join("JetBrains"));
|
||||
}
|
||||
dedupe_paths(roots)
|
||||
}
|
||||
|
||||
fn dirs_home() -> PathBuf {
|
||||
env::var_os("USERPROFILE")
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| env::var_os("HOME").map(PathBuf::from))
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
}
|
||||
|
||||
fn discover_rider_installations() -> Result<Vec<serde_json::Value>> {
|
||||
let mut candidates = Vec::new();
|
||||
if let Ok(value) = env::var("EPIPHANY_RIDER_PATH") {
|
||||
candidates.push(PathBuf::from(value));
|
||||
}
|
||||
if let Some(path_var) = env::var_os("PATH") {
|
||||
for dir in env::split_paths(&path_var) {
|
||||
for name in RIDER_EXECUTABLE_NAMES {
|
||||
candidates.push(dir.join(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
for root in rider_search_roots() {
|
||||
if !root.exists() {
|
||||
continue;
|
||||
}
|
||||
for entry in fs::read_dir(root)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let Some(name) = path.file_name().and_then(|value| value.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
if !path.is_dir() || !name.contains("Rider") {
|
||||
continue;
|
||||
}
|
||||
for exe in RIDER_EXECUTABLE_NAMES {
|
||||
candidates.push(path.join("bin").join(exe));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut installations = Vec::new();
|
||||
for path in dedupe_paths(candidates) {
|
||||
if !path.exists() {
|
||||
continue;
|
||||
}
|
||||
let version_hint = path
|
||||
.components()
|
||||
.rev()
|
||||
.filter_map(|part| part.as_os_str().to_str())
|
||||
.find(|part| part.contains("Rider"))
|
||||
.map(ToOwned::to_owned);
|
||||
installations.push(json!({
|
||||
"path": maybe_resolve(&path),
|
||||
"exists": true,
|
||||
"versionHint": version_hint,
|
||||
}));
|
||||
}
|
||||
Ok(installations)
|
||||
}
|
||||
|
||||
fn discover_solution(project_root: &Path) -> Result<serde_json::Value> {
|
||||
let root = project_root.canonicalize()?;
|
||||
let mut top_level = Vec::new();
|
||||
for entry in fs::read_dir(&root)? {
|
||||
let path = entry?.path();
|
||||
if path.extension().and_then(|value| value.to_str()) == Some("sln") {
|
||||
top_level.push(path);
|
||||
}
|
||||
}
|
||||
top_level.sort();
|
||||
if !top_level.is_empty() {
|
||||
return Ok(json!({
|
||||
"status": "found",
|
||||
"path": top_level[0],
|
||||
"candidates": top_level.iter().take(8).collect::<Vec<_>>(),
|
||||
}));
|
||||
}
|
||||
let mut candidates = Vec::new();
|
||||
collect_solutions(&root, &root, &mut candidates)?;
|
||||
Ok(json!({
|
||||
"status": if candidates.is_empty() { "missingSolution" } else { "found" },
|
||||
"path": candidates.first(),
|
||||
"candidates": candidates.iter().take(8).collect::<Vec<_>>(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn collect_solutions(root: &Path, current: &Path, candidates: &mut Vec<PathBuf>) -> Result<()> {
|
||||
if candidates.len() >= 8 {
|
||||
return Ok(());
|
||||
}
|
||||
for entry in fs::read_dir(current)? {
|
||||
let path = entry?.path();
|
||||
let rel = path.strip_prefix(root).unwrap_or(&path);
|
||||
if rel.components().any(|component| {
|
||||
let value = component.as_os_str().to_string_lossy();
|
||||
value.starts_with('.')
|
||||
|| matches!(
|
||||
value.as_ref(),
|
||||
"Library" | "Temp" | "obj" | "bin" | "node_modules"
|
||||
)
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
if path.is_dir() {
|
||||
collect_solutions(root, &path, candidates)?;
|
||||
} else if path.extension().and_then(|value| value.to_str()) == Some("sln") {
|
||||
candidates.push(path);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_git(project_root: &Path, args: &[&str]) -> Option<String> {
|
||||
let output = Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(project_root)
|
||||
.args(args)
|
||||
.output()
|
||||
.ok()?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
}
|
||||
|
||||
fn vcs_summary(project_root: &Path) -> serde_json::Value {
|
||||
let branch = run_git(project_root, &["rev-parse", "--abbrev-ref", "HEAD"]);
|
||||
let status = run_git(project_root, &["status", "--porcelain=v1"]);
|
||||
let changed = run_git(project_root, &["diff", "--name-only"]);
|
||||
let staged = run_git(project_root, &["diff", "--cached", "--name-only"]);
|
||||
let mut changed_files: Vec<String> = changed
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.lines()
|
||||
.map(ToOwned::to_owned)
|
||||
.collect();
|
||||
if let Some(status) = &status {
|
||||
for line in status.lines() {
|
||||
let path = line.get(3..).unwrap_or_default().trim().to_string();
|
||||
if !path.is_empty() && !changed_files.contains(&path) {
|
||||
changed_files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
json!({
|
||||
"status": if branch.is_some() { "gitDetected" } else { "notGit" },
|
||||
"branch": branch,
|
||||
"dirty": status.as_deref().is_some_and(|value| !value.is_empty()),
|
||||
"changedFiles": changed_files,
|
||||
"stagedFiles": staged.as_deref().unwrap_or_default().lines().collect::<Vec<_>>(),
|
||||
"changedRangesKnown": branch.is_some(),
|
||||
})
|
||||
}
|
||||
|
||||
fn render_status(summary: &serde_json::Value) -> String {
|
||||
format!(
|
||||
"# Rider Bridge Status\n\n- status: {}\n- workspace: `{}`\n- solution: `{}`\n- rider: `{}`\n- installations: {}\n- branch: {}\n- dirty: {}\n- note: {}\n\nRider is a source-context organ. This artifact is an operator-safe projection, not durable Epiphany truth.\n",
|
||||
summary["status"].as_str().unwrap_or("unknown"),
|
||||
summary["workspace"].as_str().unwrap_or("unknown"),
|
||||
summary["solutionPath"].as_str().unwrap_or("none"),
|
||||
summary["riderPath"].as_str().unwrap_or("missing"),
|
||||
summary["installationCount"].as_u64().unwrap_or_default(),
|
||||
summary["vcs"]["branch"].as_str().unwrap_or("unknown"),
|
||||
summary["vcs"]["dirty"].as_bool().unwrap_or(false),
|
||||
summary["note"].as_str().unwrap_or(""),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_context(packet: &serde_json::Value, summary: &serde_json::Value) -> String {
|
||||
format!(
|
||||
"# Rider Context Packet\n\n- status: {}\n- project: `{}`\n- solution: `{}`\n- file: `{}`\n- selection: {}-{}\n- symbol: {}\n- artifact: `{}`\n\nModeling may use this packet as scratch/context. It is not accepted map state until reviewed.\n",
|
||||
summary["status"].as_str().unwrap_or("unknown"),
|
||||
packet["projectRoot"].as_str().unwrap_or("unknown"),
|
||||
packet["solutionPath"].as_str().unwrap_or("none"),
|
||||
packet["filePath"].as_str().unwrap_or("none"),
|
||||
packet["selection"]["startLine"]
|
||||
.as_i64()
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_else(|| "none".to_string()),
|
||||
packet["selection"]["endLine"]
|
||||
.as_i64()
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_else(|| "none".to_string()),
|
||||
packet["symbol"]["name"].as_str().unwrap_or("none"),
|
||||
summary["artifactPath"].as_str().unwrap_or("unknown"),
|
||||
)
|
||||
}
|
||||
|
||||
fn status_rider(options: &RiderOptions) -> Result<serde_json::Value> {
|
||||
let project_root = options.project_root.canonicalize()?;
|
||||
let directory = options
|
||||
.artifact_dir
|
||||
.clone()
|
||||
.map(Ok)
|
||||
.unwrap_or_else(|| artifact_dir(&options.artifact_root, "inspect"))?;
|
||||
let installations = discover_rider_installations()?;
|
||||
let solution = discover_solution(&project_root)?;
|
||||
let first_rider = installations.first();
|
||||
let status = if first_rider.is_some() {
|
||||
"discovered"
|
||||
} else {
|
||||
"missingRider"
|
||||
};
|
||||
let note = if first_rider.is_some() {
|
||||
"A Rider executable candidate was discovered for explicit source navigation and context capture."
|
||||
} else {
|
||||
"No Rider executable was found. Install Rider or set EPIPHANY_RIDER_PATH."
|
||||
};
|
||||
let summary = json!({
|
||||
"kind": "riderBridgeStatus",
|
||||
"status": status,
|
||||
"workspace": project_root,
|
||||
"solutionPath": solution["path"],
|
||||
"solutionStatus": solution["status"],
|
||||
"solutionCandidates": solution["candidates"],
|
||||
"riderPath": first_rider.and_then(|value| value["path"].as_str()),
|
||||
"installationCount": installations.len(),
|
||||
"installations": installations,
|
||||
"searchRoots": rider_search_roots(),
|
||||
"vcs": vcs_summary(&project_root),
|
||||
"capturedAt": now_iso(),
|
||||
"artifactPath": directory,
|
||||
"note": note,
|
||||
});
|
||||
write_json(&directory.join("rider-bridge-summary.json"), &summary)?;
|
||||
write_text(
|
||||
&directory.join("rider-bridge-status.md"),
|
||||
&render_status(&summary),
|
||||
)?;
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
fn packet_from_options(options: &RiderOptions, project_root: &Path) -> Result<serde_json::Value> {
|
||||
if let Some(packet) = &options.packet {
|
||||
let value: serde_json::Value = serde_json::from_str(&fs::read_to_string(packet)?)?;
|
||||
if !value.is_object() {
|
||||
return Err(anyhow!("context packet must be a JSON object"));
|
||||
}
|
||||
return Ok(value);
|
||||
}
|
||||
let solution = discover_solution(project_root)?;
|
||||
let file_path = options
|
||||
.file
|
||||
.as_ref()
|
||||
.map(|path| path.canonicalize().unwrap_or_else(|_| path.to_path_buf()));
|
||||
if let Some(file_path) = &file_path
|
||||
&& !is_inside(file_path, project_root)
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"context file is outside project root: {}",
|
||||
file_path.display()
|
||||
));
|
||||
}
|
||||
Ok(json!({
|
||||
"kind": "riderContext",
|
||||
"capturedAt": now_iso(),
|
||||
"projectRoot": project_root,
|
||||
"solutionPath": solution["path"],
|
||||
"filePath": file_path.as_ref().map(|path| relative_or_absolute(path, project_root)),
|
||||
"caret": options.line.map(|line| json!({ "line": line, "column": options.column })),
|
||||
"selection": if options.selection_start.is_some() || options.selection_end.is_some() {
|
||||
json!({ "startLine": options.selection_start, "endLine": options.selection_end })
|
||||
} else {
|
||||
serde_json::Value::Null
|
||||
},
|
||||
"symbol": options.symbol_name.as_ref().map(|name| json!({
|
||||
"name": name,
|
||||
"kind": options.symbol_kind,
|
||||
"namespace": options.symbol_namespace,
|
||||
})),
|
||||
"vcs": vcs_summary(project_root),
|
||||
}))
|
||||
}
|
||||
|
||||
fn context_rider(options: &RiderOptions) -> Result<serde_json::Value> {
|
||||
let project_root = options.project_root.canonicalize()?;
|
||||
let directory = options
|
||||
.artifact_dir
|
||||
.clone()
|
||||
.map(Ok)
|
||||
.unwrap_or_else(|| artifact_dir(&options.artifact_root, "context"))?;
|
||||
let packet = packet_from_options(options, &project_root)?;
|
||||
let mut status_options = RiderOptions {
|
||||
..options.clone_for_status(directory.clone())
|
||||
};
|
||||
status_options.artifact_dir = Some(directory.clone());
|
||||
let mut summary = status_rider(&status_options)?;
|
||||
summary["kind"] = json!("riderContextCapture");
|
||||
summary["status"] = json!("captured");
|
||||
summary["contextPath"] = json!(directory.join("rider-context.json"));
|
||||
summary["filePath"] = packet["filePath"].clone();
|
||||
summary["symbol"] = packet["symbol"].clone();
|
||||
write_json(&directory.join("rider-context.json"), &packet)?;
|
||||
write_json(&directory.join("rider-bridge-summary.json"), &summary)?;
|
||||
write_text(
|
||||
&directory.join("rider-context.md"),
|
||||
&render_context(&packet, &summary),
|
||||
)?;
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
impl RiderOptions {
|
||||
fn clone_for_status(&self, directory: PathBuf) -> Self {
|
||||
Self {
|
||||
project_root: self.project_root.clone(),
|
||||
artifact_root: self.artifact_root.clone(),
|
||||
artifact_dir: Some(directory),
|
||||
packet: None,
|
||||
file: None,
|
||||
line: None,
|
||||
column: None,
|
||||
selection_start: None,
|
||||
selection_end: None,
|
||||
symbol_name: None,
|
||||
symbol_kind: None,
|
||||
symbol_namespace: None,
|
||||
launch: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_ref_rider(options: &RiderOptions) -> Result<serde_json::Value> {
|
||||
let project_root = options.project_root.canonicalize()?;
|
||||
let directory = options
|
||||
.artifact_dir
|
||||
.clone()
|
||||
.map(Ok)
|
||||
.unwrap_or_else(|| artifact_dir(&options.artifact_root, "open-ref"))?;
|
||||
let status_options = options.clone_for_status(directory.clone());
|
||||
let status = status_rider(&status_options)?;
|
||||
let file_path = options
|
||||
.file
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("open-ref requires --file"))?
|
||||
.canonicalize()?;
|
||||
if !is_inside(&file_path, &project_root) {
|
||||
return Err(anyhow!(
|
||||
"code ref is outside project root: {}",
|
||||
file_path.display()
|
||||
));
|
||||
}
|
||||
let rider_path = status["riderPath"].as_str();
|
||||
let mut command = Vec::<String>::new();
|
||||
if let Some(path) = rider_path {
|
||||
command.push(path.to_string());
|
||||
command.push(file_path.display().to_string());
|
||||
if let Some(line) = options.line {
|
||||
command.push("--line".to_string());
|
||||
command.push(line.to_string());
|
||||
}
|
||||
}
|
||||
if let Some(path) = rider_path
|
||||
&& options.launch
|
||||
{
|
||||
let mut launch = Command::new(path);
|
||||
launch.arg(&file_path);
|
||||
if let Some(line) = options.line {
|
||||
launch.arg("--line").arg(line.to_string());
|
||||
}
|
||||
launch.current_dir(&project_root).spawn()?;
|
||||
}
|
||||
let summary = json!({
|
||||
"kind": "riderOpenRef",
|
||||
"status": if rider_path.is_some() { if options.launch { "launched" } else { "planned" } } else { "missingRider" },
|
||||
"workspace": status["workspace"],
|
||||
"solutionPath": status["solutionPath"],
|
||||
"solutionStatus": status["solutionStatus"],
|
||||
"solutionCandidates": status["solutionCandidates"],
|
||||
"riderPath": status["riderPath"],
|
||||
"installationCount": status["installationCount"],
|
||||
"installations": status["installations"],
|
||||
"searchRoots": status["searchRoots"],
|
||||
"vcs": status["vcs"],
|
||||
"capturedAt": status["capturedAt"],
|
||||
"artifactPath": directory,
|
||||
"note": status["note"],
|
||||
"filePath": file_path,
|
||||
"line": options.line,
|
||||
"column": options.column,
|
||||
"command": command,
|
||||
"launched": options.launch && rider_path.is_some(),
|
||||
});
|
||||
write_json(&directory.join("rider-open-ref.json"), &summary)?;
|
||||
write_json(&directory.join("rider-bridge-summary.json"), &summary)?;
|
||||
write_text(
|
||||
&directory.join("rider-bridge-status.md"),
|
||||
&render_status(&summary),
|
||||
)?;
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
fn bridge_guidance(project_root: &Path) -> Result<String> {
|
||||
let root = repo_root()?;
|
||||
let installations = discover_rider_installations()?;
|
||||
let solution = discover_solution(project_root)?;
|
||||
let status_command = format!(
|
||||
"`epiphany-rider-bridge status --project-root {}`",
|
||||
project_root.display()
|
||||
);
|
||||
if let Some(first) = installations.first() {
|
||||
let solution_text = solution["path"].as_str().unwrap_or("no solution found yet");
|
||||
Ok(format!(
|
||||
"- Rider bridge: Rider is available at `{}` and solution context is `{}`. Use {} for an auditable source/IDE status receipt, and use `context --file <path> --selection-start <line> --selection-end <line> --symbol-name <name>` when the future plugin or operator sends a bounded source slice. Rider facts are scratch/context until modeling or verification accepts them.",
|
||||
first["path"].as_str().unwrap_or("unknown"),
|
||||
solution_text,
|
||||
status_command,
|
||||
))
|
||||
} else {
|
||||
Ok(format!(
|
||||
"- Rider bridge: no Rider executable was found. Use {} to write an auditable missing-IDE artifact, or set EPIPHANY_RIDER_PATH. Do not pretend IDE diagnostics, changed ranges, or source navigation were captured until the bridge has a receipt. Native bridge binary is rooted at `{}`.",
|
||||
status_command,
|
||||
root.display(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
use anyhow::{Context, Result, anyhow};
|
||||
use cultcache_rs::{
|
||||
CacheBackingStore, CultCacheEnvelope, RedbMessagePackBackingStore,
|
||||
SingleFileMessagePackBackingStore,
|
||||
};
|
||||
use epiphany_core::{EpiphanyRuntimeJob, RUNTIME_JOB_TYPE};
|
||||
use serde::Serialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::path::Path;
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct BenchmarkReceipt {
|
||||
schema_version: &'static str,
|
||||
store: String,
|
||||
job_id: String,
|
||||
job_created_at: String,
|
||||
selection_window_milliseconds: i64,
|
||||
selected_stored_ats: Vec<String>,
|
||||
selected_entry_count: usize,
|
||||
selected_identities: Vec<String>,
|
||||
selected_envelope_set_sha256: String,
|
||||
read_milliseconds: u128,
|
||||
cas_milliseconds: Vec<u128>,
|
||||
}
|
||||
|
||||
enum Store {
|
||||
Snapshot(SingleFileMessagePackBackingStore),
|
||||
Keyed(RedbMessagePackBackingStore),
|
||||
}
|
||||
|
||||
impl Store {
|
||||
fn open(path: &Path) -> Result<Self> {
|
||||
match path.extension().and_then(|value| value.to_str()) {
|
||||
Some("cc" | "msgpack") => {
|
||||
Ok(Self::Snapshot(SingleFileMessagePackBackingStore::new(path)))
|
||||
}
|
||||
Some("redb") => Ok(Self::Keyed(RedbMessagePackBackingStore::new(path)?)),
|
||||
extension => Err(anyhow!(
|
||||
"unsupported benchmark store extension {extension:?}"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn pull_all(&self) -> Result<Vec<CultCacheEnvelope>> {
|
||||
match self {
|
||||
Self::Snapshot(store) => store.pull_all(),
|
||||
Self::Keyed(store) => store.pull_all(),
|
||||
}
|
||||
}
|
||||
|
||||
fn compare_and_swap_batch(
|
||||
&self,
|
||||
expected: &[CultCacheEnvelope],
|
||||
replacements: Vec<CultCacheEnvelope>,
|
||||
) -> Result<bool> {
|
||||
match self {
|
||||
Self::Snapshot(store) => store.compare_and_swap_batch(expected, replacements),
|
||||
Self::Keyed(store) => store.compare_and_swap_batch(expected, replacements),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let store_path = args
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("usage: epiphany-runtime-store-benchmark <disposable-store> <historical-job-id> [repetitions]"))?;
|
||||
let job_id = args
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("missing historical job id"))?;
|
||||
let repetitions = args
|
||||
.next()
|
||||
.map(|value| value.parse::<usize>())
|
||||
.transpose()
|
||||
.context("invalid repetitions")?
|
||||
.unwrap_or(3);
|
||||
if repetitions == 0 || args.next().is_some() {
|
||||
return Err(anyhow!("repetitions must be positive and arguments exact"));
|
||||
}
|
||||
let path = Path::new(&store_path);
|
||||
let store = Store::open(path)?;
|
||||
let read_started = Instant::now();
|
||||
let entries = store.pull_all()?;
|
||||
let read_milliseconds = read_started.elapsed().as_millis();
|
||||
let job = entries
|
||||
.iter()
|
||||
.find(|entry| entry.r#type == RUNTIME_JOB_TYPE && entry.key == job_id)
|
||||
.ok_or_else(|| anyhow!("historical runtime job envelope is absent"))?;
|
||||
let runtime_job: EpiphanyRuntimeJob = rmp_serde::from_slice(&job.payload)?;
|
||||
let job_created_at = runtime_job.created_at;
|
||||
let job_created = chrono::DateTime::parse_from_rfc3339(&job_created_at)?;
|
||||
let selection_window_milliseconds = 2_000_i64;
|
||||
let mut selected = entries
|
||||
.into_iter()
|
||||
.filter(|entry| {
|
||||
chrono::DateTime::parse_from_rfc3339(&entry.stored_at)
|
||||
.map(|stored| {
|
||||
(stored.timestamp_millis() - job_created.timestamp_millis()).abs()
|
||||
<= selection_window_milliseconds
|
||||
})
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
selected.sort_by(|left, right| (&left.r#type, &left.key).cmp(&(&right.r#type, &right.key)));
|
||||
if selected.is_empty() {
|
||||
return Err(anyhow!("historical job commit selected no envelopes"));
|
||||
}
|
||||
let selected_identities = selected
|
||||
.iter()
|
||||
.map(|entry| format!("{}:{}", entry.r#type, entry.key))
|
||||
.collect::<Vec<_>>();
|
||||
let selected_stored_ats = selected
|
||||
.iter()
|
||||
.map(|entry| entry.stored_at.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let selected_envelope_set_sha256 =
|
||||
format!("{:x}", Sha256::digest(rmp_serde::to_vec_named(&selected)?));
|
||||
let mut cas_milliseconds = Vec::with_capacity(repetitions);
|
||||
for _ in 0..repetitions {
|
||||
let started = Instant::now();
|
||||
if !store.compare_and_swap_batch(&selected, selected.clone())? {
|
||||
return Err(anyhow!("exact historical batch CAS unexpectedly refused"));
|
||||
}
|
||||
cas_milliseconds.push(started.elapsed().as_millis());
|
||||
}
|
||||
let receipt = BenchmarkReceipt {
|
||||
schema_version: "epiphany.runtime_store_benchmark_receipt.v1",
|
||||
store: path.display().to_string(),
|
||||
job_id,
|
||||
job_created_at,
|
||||
selection_window_milliseconds,
|
||||
selected_stored_ats,
|
||||
selected_entry_count: selected.len(),
|
||||
selected_identities,
|
||||
selected_envelope_set_sha256,
|
||||
read_milliseconds,
|
||||
cas_milliseconds,
|
||||
};
|
||||
println!("{}", serde_json::to_string_pretty(&receipt)?);
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,641 +0,0 @@
|
|||
use anyhow::{Context, Result, bail};
|
||||
use chrono::Utc;
|
||||
use epiphany_core::*;
|
||||
use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{
|
||||
env, fs,
|
||||
io::{BufRead, BufReader},
|
||||
net::TcpListener,
|
||||
ops::{Deref, DerefMut},
|
||||
path::{Path, PathBuf},
|
||||
process::{Child, Command, Stdio},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
const RUNTIME: &str = "semantic-recovery-smoke";
|
||||
const PROJECTOR: &str = "epiphany-memory-semantic-projector";
|
||||
const SERVICE: &str = "epiphany-memory-semantic-projector-service";
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let root = env::args().nth(1).map(PathBuf::from).unwrap_or_else(|| {
|
||||
env::temp_dir().join(format!("epiphany-semantic-recovery-{}", Uuid::new_v4()))
|
||||
});
|
||||
if root.exists() {
|
||||
bail!("fixture root already exists: {}", root.display());
|
||||
}
|
||||
fs::create_dir_all(&root)?;
|
||||
let proof_path = root.join("proof.json");
|
||||
let result = run(&root);
|
||||
if let Err(error) = &result {
|
||||
fs::write(
|
||||
&proof_path,
|
||||
serde_json::to_vec_pretty(&json!({
|
||||
"schemaVersion":"epiphany.semantic_recovery_smoke_proof.v1", "status":"failed",
|
||||
"fixtureRoot":root, "error":format!("{error:#}"), "liveCanonicalInputsUsed":false
|
||||
}))?,
|
||||
)?;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn run(root: &Path) -> Result<()> {
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let swarm = format!("smoke-{id}");
|
||||
let mind = root.join("mind.cc");
|
||||
let modeling = root.join("modeling.cc");
|
||||
let verse = root.join("verse.cc");
|
||||
seed_mind(root, &mind, &swarm)?;
|
||||
seed_modeling(&modeling, &mind, &swarm)?;
|
||||
let input = agent_memory_semantic_projection_input(&mind)?;
|
||||
let modeling_input = runtime_modeling_semantic_projection_input(&modeling)?;
|
||||
|
||||
let exe_dir = env::current_exe()?
|
||||
.parent()
|
||||
.context("smoke executable has no directory")?
|
||||
.to_path_buf();
|
||||
let projector = sibling(&exe_dir, PROJECTOR);
|
||||
let supervisor = sibling(&exe_dir, "epiphany-daemon-supervisor");
|
||||
for path in [&projector, &supervisor] {
|
||||
if !path.is_file() {
|
||||
bail!("required sibling executable missing: {}", path.display());
|
||||
}
|
||||
}
|
||||
let receipt_id = Uuid::new_v4().to_string();
|
||||
let qdrant =
|
||||
env::var("EPIPHANY_QDRANT_URL").unwrap_or_else(|_| "http://127.0.0.1:16333".into());
|
||||
let ollama =
|
||||
env::var("EPIPHANY_OLLAMA_BASE_URL").unwrap_or_else(|_| "http://127.0.0.1:11434".into());
|
||||
let model = env::var("EPIPHANY_OLLAMA_MODEL").unwrap_or_else(|_| "qwen3-embedding:0.6b".into());
|
||||
let mind_collection = format!("epiphany_smoke_mind_{}", id.replace('-', ""));
|
||||
let modeling_collection = format!("epiphany_smoke_modeling_{}", id.replace('-', ""));
|
||||
for collection in [&mind_collection, &modeling_collection] {
|
||||
if collection_exists(&qdrant, collection)? {
|
||||
bail!("refused fixture collection collision: {collection}");
|
||||
}
|
||||
}
|
||||
let mut collections = FixtureCollections::new(
|
||||
qdrant.clone(),
|
||||
vec![mind_collection.clone(), modeling_collection.clone()],
|
||||
);
|
||||
let stalled = StalledHttpEndpoint::new()?;
|
||||
let old_args = vec![
|
||||
"pulse".into(),
|
||||
"--agent-store".into(),
|
||||
s(&mind),
|
||||
"--runtime-store".into(),
|
||||
s(&modeling),
|
||||
"--local-verse-store".into(),
|
||||
s(&verse),
|
||||
"--runtime-id".into(),
|
||||
RUNTIME.into(),
|
||||
"--interval-seconds".into(),
|
||||
"1".into(),
|
||||
"--qdrant-url".into(),
|
||||
qdrant.clone(),
|
||||
"--ollama-base-url".into(),
|
||||
stalled.url(),
|
||||
"--ollama-model".into(),
|
||||
model.clone(),
|
||||
];
|
||||
let mut old_child = ChildGuard(
|
||||
Command::new(&projector)
|
||||
.args(&old_args)
|
||||
.env("EPIPHANY_MIND_QDRANT_COLLECTION", &mind_collection)
|
||||
.env("EPIPHANY_MODELING_QDRANT_COLLECTION", &modeling_collection)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?,
|
||||
);
|
||||
let old = wait_for_running_claim(&mind, &input, Duration::from_secs(15))?;
|
||||
stop(&mut old_child);
|
||||
let old_pid = old_child.id();
|
||||
let args = vec![
|
||||
"serve".into(),
|
||||
"--agent-store".into(),
|
||||
s(&mind),
|
||||
"--runtime-store".into(),
|
||||
s(&modeling),
|
||||
"--local-verse-store".into(),
|
||||
s(&verse),
|
||||
"--runtime-id".into(),
|
||||
RUNTIME.into(),
|
||||
"--interval-seconds".into(),
|
||||
"3".into(),
|
||||
"--qdrant-url".into(),
|
||||
qdrant.clone(),
|
||||
"--ollama-base-url".into(),
|
||||
ollama.clone(),
|
||||
"--ollama-model".into(),
|
||||
model,
|
||||
];
|
||||
let policy = EpiphanyCultMeshManagedServicePolicyEntry {
|
||||
schema_version: EPIPHANY_CULTMESH_MANAGED_SERVICE_POLICY_SCHEMA_VERSION.into(),
|
||||
policy_id: "managed-service-policy-epiphany-memory-semantic-projector-service".into(),
|
||||
service_id: SERVICE.into(),
|
||||
owner_daemon_id: "epiphany-daemon-supervisor".into(),
|
||||
command: s(&projector),
|
||||
args: args.clone(),
|
||||
cwd: None,
|
||||
enabled: true,
|
||||
restart_mode: "always".into(),
|
||||
cooldown_seconds: 0,
|
||||
backoff_multiplier: 1,
|
||||
stdout_artifact: s(&root.join("projector.stdout.log")),
|
||||
stderr_artifact: s(&root.join("projector.stderr.log")),
|
||||
updated_at_utc: Utc::now().to_rfc3339(),
|
||||
private_state_exposed: false,
|
||||
notes: vec!["GUID-scoped recovery smoke".into()],
|
||||
};
|
||||
write_epiphany_cultmesh_semantic_projector_service_policy(&verse, RUNTIME, policy.clone())?;
|
||||
let (_, policy_digest) =
|
||||
load_epiphany_cultmesh_managed_service_policy_with_digest(&verse, RUNTIME, SERVICE)?
|
||||
.context("policy absent")?;
|
||||
let mut child = ChildGuard(
|
||||
Command::new(&projector)
|
||||
.args(&args)
|
||||
.env("EPIPHANY_STARTUP_LIFECYCLE_RECEIPT_ID", &receipt_id)
|
||||
.env("EPIPHANY_MIND_QDRANT_COLLECTION", &mind_collection)
|
||||
.env("EPIPHANY_MODELING_QDRANT_COLLECTION", &modeling_collection)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?,
|
||||
);
|
||||
let pid = child.id();
|
||||
let executable_sha256 = format!("sha256-{:x}", Sha256::digest(fs::read(&projector)?));
|
||||
let completed = Utc::now();
|
||||
write_epiphany_cultmesh_daemon_service_lifecycle_receipt(
|
||||
&verse,
|
||||
RUNTIME,
|
||||
EpiphanyCultMeshDaemonServiceLifecycleReceiptEntry {
|
||||
schema_version: EPIPHANY_CULTMESH_DAEMON_SERVICE_LIFECYCLE_RECEIPT_SCHEMA_VERSION
|
||||
.into(),
|
||||
receipt_id: receipt_id.clone(),
|
||||
service_id: SERVICE.into(),
|
||||
scheduler_id: "epiphany-daemon-supervisor".into(),
|
||||
runtime_id: RUNTIME.into(),
|
||||
daemon_selector: "epiphany-daemon-supervisor".into(),
|
||||
action: "launch".into(),
|
||||
status: "launched".into(),
|
||||
command: s(&projector),
|
||||
args: args.clone(),
|
||||
cwd: None,
|
||||
process_id: Some(pid),
|
||||
exit_code: None,
|
||||
started_at_utc: (completed - chrono::Duration::seconds(1)).to_rfc3339(),
|
||||
completed_at_utc: Some(completed.to_rfc3339()),
|
||||
operator_artifact_ref: format!("fixture://{id}/launch"),
|
||||
private_state_exposed: false,
|
||||
notes: vec![],
|
||||
executable_sha256,
|
||||
preflight_witness_id: String::new(),
|
||||
required_document_types: vec![],
|
||||
schema_preflight_passed: false,
|
||||
schema_catalog_sha256: String::new(),
|
||||
managed_policy_id: policy.policy_id,
|
||||
managed_policy_digest: policy_digest,
|
||||
provider_daemon_id: PROJECTOR.into(),
|
||||
startup_correlation_id: receipt_id.clone(),
|
||||
process_creation_token: 1,
|
||||
process_created_at_rfc3339: None,
|
||||
process_executable_path: projector.display().to_string(),
|
||||
},
|
||||
)?;
|
||||
let stdout = child
|
||||
.stdout
|
||||
.take()
|
||||
.context("projector stdout unavailable")?;
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
thread::spawn(move || {
|
||||
for line in BufReader::new(stdout).lines().map_while(Result::ok) {
|
||||
let _ = tx.send(line);
|
||||
}
|
||||
});
|
||||
let first = next_pulse(&rx, Duration::from_secs(20))?;
|
||||
let heartbeat = first["heartbeatId"]
|
||||
.as_str()
|
||||
.context("pulse heartbeat absent")?;
|
||||
if first["providerStatus"] != "ready" {
|
||||
stop(&mut child);
|
||||
bail!("projector did not establish ready recovery witness: {first}");
|
||||
}
|
||||
|
||||
let unrelated_id = Uuid::new_v4().to_string();
|
||||
write_epiphany_cultmesh_daemon_heartbeat_event(
|
||||
&verse,
|
||||
RUNTIME,
|
||||
EpiphanyCultMeshDaemonHeartbeatEventEntry {
|
||||
schema_version: EPIPHANY_CULTMESH_DAEMON_HEARTBEAT_EVENT_SCHEMA_VERSION.into(),
|
||||
heartbeat_id: unrelated_id.clone(),
|
||||
daemon_id: PROJECTOR.into(),
|
||||
cluster_id: "local".into(),
|
||||
provider_incarnation: first["providerIncarnation"].as_str().unwrap().into(),
|
||||
sequence: 999,
|
||||
status: "ready".into(),
|
||||
heartbeat_at: Utc::now().to_rfc3339(),
|
||||
private_state_exposed: false,
|
||||
startup_lifecycle_receipt_id: String::new(),
|
||||
},
|
||||
)?;
|
||||
let before_wrong = file_sha256(&mind)?;
|
||||
let wrong = supervisor_recover(
|
||||
&supervisor,
|
||||
&verse,
|
||||
&mind,
|
||||
&old.claim_id,
|
||||
&receipt_id,
|
||||
&unrelated_id,
|
||||
)?;
|
||||
if wrong.success {
|
||||
stop(&mut child);
|
||||
bail!("uncorrelated heartbeat authorized recovery");
|
||||
}
|
||||
if file_sha256(&mind)? != before_wrong {
|
||||
stop(&mut child);
|
||||
bail!("wrong heartbeat mutated canonical fixture store");
|
||||
}
|
||||
let recovered_process = supervisor_recover(
|
||||
&supervisor,
|
||||
&verse,
|
||||
&mind,
|
||||
&old.claim_id,
|
||||
&receipt_id,
|
||||
heartbeat,
|
||||
)?;
|
||||
if !recovered_process.success {
|
||||
stop(&mut child);
|
||||
bail!("exact recovery failed: {}", recovered_process.stderr);
|
||||
}
|
||||
let recovered: Value = serde_json::from_str(&recovered_process.stdout)?;
|
||||
if recovered["epoch"] != old.claim_epoch + 1 {
|
||||
stop(&mut child);
|
||||
bail!("recovered epoch did not advance exactly once");
|
||||
}
|
||||
let before_reuse = file_sha256(&mind)?;
|
||||
let reuse = supervisor_recover(
|
||||
&supervisor,
|
||||
&verse,
|
||||
&mind,
|
||||
&old.claim_id,
|
||||
&receipt_id,
|
||||
heartbeat,
|
||||
)?;
|
||||
if reuse.success {
|
||||
stop(&mut child);
|
||||
bail!("recovery evidence was reusable");
|
||||
}
|
||||
if file_sha256(&mind)? != before_reuse {
|
||||
stop(&mut child);
|
||||
bail!("single-use refusal mutated canonical fixture store");
|
||||
}
|
||||
let atomic = inspect_memory_semantic_recovery_for_smoke(
|
||||
&mind,
|
||||
&input,
|
||||
&old.executor_id,
|
||||
&old.executor_incarnation,
|
||||
)?;
|
||||
if atomic.claim_epoch != 2
|
||||
|| atomic.claim_status != "running"
|
||||
|| atomic.attempt_status != "running"
|
||||
|| !atomic.recovery_authorization_consumed
|
||||
|| !atomic.abandoned_attempt_failed
|
||||
|| atomic.old_owner_authenticates_current_claim
|
||||
{
|
||||
stop(&mut child);
|
||||
bail!("recovery atomic state is invalid: {atomic:?}");
|
||||
}
|
||||
let deadline = Instant::now() + Duration::from_secs(60);
|
||||
let terminal = loop {
|
||||
if let Some(receipt) =
|
||||
load_memory_semantic_projection_success(&mind, input.obligation(), input.source_head())?
|
||||
{
|
||||
break receipt;
|
||||
}
|
||||
if Instant::now() > deadline {
|
||||
stop(&mut child);
|
||||
bail!("recovered successor did not reach terminal success");
|
||||
}
|
||||
let _ = next_pulse(&rx, Duration::from_secs(5));
|
||||
};
|
||||
stop(&mut child);
|
||||
collections.cleanup()?;
|
||||
let _readiness = load_memory_semantic_projection_readiness(&mind, &input)?
|
||||
.context("terminal success lacks readiness")?;
|
||||
let query_eligible = memory_semantic_projection_query_eligible(
|
||||
input.obligation(),
|
||||
input.source_head(),
|
||||
&terminal,
|
||||
);
|
||||
if !query_eligible {
|
||||
bail!("terminal receipt is not query eligible");
|
||||
}
|
||||
let proof = json!({"schemaVersion":"epiphany.semantic_recovery_smoke_proof.v1","status":"passed","fixtureId":id,"fixtureRoot":root,
|
||||
"liveCanonicalInputsUsed":false,"fixtureStores":{"mind":{"path":mind,"sourceId":input.source_head().canonical_source_id},"modeling":{"path":modeling,"sourceId":modeling_input.source_head().canonical_source_id},"verse":{"path":verse,"runtimeId":RUNTIME}},
|
||||
"processBoundary":{"supervisor":supervisor,"projector":projector,"abandonedProjectorPid":old_pid,"replacementProjectorPid":pid},
|
||||
"fencing":{"wrongHeartbeat":wrong,"wrongHeartbeatCanonicalBytesUnchanged":true,"singleUse":reuse,"singleUseCanonicalBytesUnchanged":true,"predecessorClaimId":old.claim_id,"predecessorEpoch":old.claim_epoch,"atomicRecovery":atomic},
|
||||
"successor":{"authorizationId":recovered["authorizationId"],"epoch":recovered["epoch"],"receiptId":terminal.receipt_id,"queryEligible":query_eligible,"readinessPresent":true},
|
||||
"cleanup":{"mindCollection":mind_collection,"modelingCollection":modeling_collection,"preflightAbsent":true,"deleted":true,"verifiedAbsent":true}});
|
||||
fs::write(root.join("proof.json"), serde_json::to_vec_pretty(&proof)?)?;
|
||||
println!("{}", serde_json::to_string_pretty(&proof)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn seed_mind(root: &Path, store: &Path, swarm: &str) -> Result<()> {
|
||||
let dir = root.join("agents");
|
||||
fs::create_dir(&dir)?;
|
||||
for (role, agent, file) in [
|
||||
(
|
||||
"imagination",
|
||||
"epiphany.imagination",
|
||||
"imagination.agent-state.json",
|
||||
),
|
||||
("modeling", "epiphany.modeling", "modeling.agent-state.json"),
|
||||
("verification", "epiphany.soul", "soul.agent-state.json"),
|
||||
("implementation", "epiphany.hands", "hands.agent-state.json"),
|
||||
("research", "epiphany.eyes", "eyes.agent-state.json"),
|
||||
("Persona", "epiphany.Persona", "Persona.agent-state.json"),
|
||||
("coordinator", "epiphany.self", "self.agent-state.json"),
|
||||
] {
|
||||
let j = json!({"schema_version":"ghostlight.agent_state.v1","world":{"world_id":"fixture","setting":"isolated fixture","time":{"label":"now"},"canon_context":["fixture"]},"agents":[{"agent_id":agent,"identity":{"name":role,"roles":["fixture"],"origin":"fixture","public_description":"fixture","private_notes":[]},"canonical_state":{"underlying_organization":{"x":{"mean":0.5,"plasticity":0.5,"current_activation":0.5}},"stable_dispositions":{"x":{"mean":0.5,"plasticity":0.5,"current_activation":0.5}},"behavioral_dimensions":{"x":{"mean":0.5,"plasticity":0.5,"current_activation":0.5}},"presentation_strategy":{"x":{"mean":0.5,"plasticity":0.5,"current_activation":0.5}},"voice_style":{"x":{"mean":0.5,"plasticity":0.5,"current_activation":0.5}},"situational_state":{"x":{"mean":0.5,"plasticity":0.5,"current_activation":0.5}},"values":[{"value_id":"v","label":"fixture","priority":0.5,"unforgivable_if_betrayed":false}]},"goals":[{"goal_id":"g","description":"fixture","scope":"life","priority":0.5,"emotional_stake":"fixture","blockers":[],"status":"active"}],"memories":{"episodic":[],"semantic":[],"relationship_summaries":[]},"perceived_state_overlays":[]}],"relationships":[],"events":[],"scenes":[]});
|
||||
let mut j = j;
|
||||
j["schema_version"] = json!("ghostlight.agent_state.v0");
|
||||
fs::write(dir.join(file), serde_json::to_vec(&j)?)?;
|
||||
}
|
||||
migrate_agent_memory_json_dir_to_cultcache(dir, store)?;
|
||||
ensure_agent_memory_swarm_identity(store, swarm)?;
|
||||
admit_legacy_agent_memory_generation(store)?;
|
||||
Ok(())
|
||||
}
|
||||
fn seed_modeling(store: &Path, mind: &Path, swarm: &str) -> Result<()> {
|
||||
let at = Utc::now().to_rfc3339();
|
||||
initialize_runtime_spine(
|
||||
store,
|
||||
RuntimeSpineInitOptions {
|
||||
runtime_id: format!("runtime-{swarm}"),
|
||||
display_name: "fixture".into(),
|
||||
created_at: at.clone(),
|
||||
},
|
||||
)?;
|
||||
bind_runtime_to_agent_memory_swarm(store, mind, &at)?;
|
||||
let mut graph = EpiphanyMemoryGraphSnapshot {
|
||||
schema_version: Some(MEMORY_GRAPH_PROJECTION_SCHEMA_VERSION.into()),
|
||||
graph_id: format!("model-{swarm}"),
|
||||
model_revision: 1,
|
||||
..Default::default()
|
||||
};
|
||||
graph.model_hash = memory_graph_model_hash(&graph)?;
|
||||
ensure_runtime_repo_model(store, store.with_extension("absent"), &graph, &at)?;
|
||||
Ok(())
|
||||
}
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ProcessEvidence {
|
||||
success: bool,
|
||||
exit_code: Option<i32>,
|
||||
stdout: String,
|
||||
stderr: String,
|
||||
stderr_class: String,
|
||||
}
|
||||
|
||||
fn supervisor_recover(
|
||||
exe: &Path,
|
||||
verse: &Path,
|
||||
mind: &Path,
|
||||
claim: &str,
|
||||
receipt: &str,
|
||||
heartbeat: &str,
|
||||
) -> Result<ProcessEvidence> {
|
||||
let out = Command::new(exe)
|
||||
.args([
|
||||
"semantic-recover",
|
||||
"--store",
|
||||
&s(verse),
|
||||
"--runtime-id",
|
||||
RUNTIME,
|
||||
"--agent-store",
|
||||
&s(mind),
|
||||
"--expected-claim-id",
|
||||
claim,
|
||||
"--receipt-id",
|
||||
receipt,
|
||||
"--provider-heartbeat-id",
|
||||
heartbeat,
|
||||
])
|
||||
.output()?;
|
||||
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
|
||||
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
|
||||
let stderr_class = stderr
|
||||
.lines()
|
||||
.find(|line| !line.trim().is_empty())
|
||||
.unwrap_or("none")
|
||||
.chars()
|
||||
.take(240)
|
||||
.collect();
|
||||
Ok(ProcessEvidence {
|
||||
success: out.status.success(),
|
||||
exit_code: out.status.code(),
|
||||
stdout,
|
||||
stderr,
|
||||
stderr_class,
|
||||
})
|
||||
}
|
||||
|
||||
fn wait_for_running_claim(
|
||||
store: &Path,
|
||||
input: &MemorySemanticProjectionInput,
|
||||
timeout: Duration,
|
||||
) -> Result<SemanticRecoverySmokeInspection> {
|
||||
let deadline = Instant::now() + timeout;
|
||||
loop {
|
||||
if let Ok(row) =
|
||||
inspect_memory_semantic_recovery_for_smoke(store, input, "absent", "absent")
|
||||
{
|
||||
if row.claim_status == "running" && row.claim_epoch == 1 {
|
||||
return Ok(row);
|
||||
}
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
bail!("real projector did not persist its running epoch-1 claim");
|
||||
}
|
||||
thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
|
||||
fn collection_exists(base: &str, name: &str) -> Result<bool> {
|
||||
let url = format!("{}/collections/{}", base.trim_end_matches('/'), name);
|
||||
let status = reqwest::blocking::Client::new().get(url).send()?.status();
|
||||
if status.is_success() {
|
||||
return Ok(true);
|
||||
}
|
||||
if status.as_u16() == 404 {
|
||||
return Ok(false);
|
||||
}
|
||||
bail!("Qdrant collection preflight returned {status} for {name}")
|
||||
}
|
||||
|
||||
fn file_sha256(path: &Path) -> Result<String> {
|
||||
Ok(format!("{:x}", Sha256::digest(fs::read(path)?)))
|
||||
}
|
||||
|
||||
struct StalledHttpEndpoint {
|
||||
url: String,
|
||||
running: Arc<AtomicBool>,
|
||||
}
|
||||
impl StalledHttpEndpoint {
|
||||
fn new() -> Result<Self> {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")?;
|
||||
listener.set_nonblocking(true)?;
|
||||
let url = format!("http://{}", listener.local_addr()?);
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
let alive = running.clone();
|
||||
thread::spawn(move || {
|
||||
let mut held = Vec::new();
|
||||
while alive.load(Ordering::Acquire) {
|
||||
match listener.accept() {
|
||||
Ok((stream, _)) => held.push(stream),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
thread::sleep(Duration::from_millis(5))
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(Self { url, running })
|
||||
}
|
||||
fn url(&self) -> String {
|
||||
self.url.clone()
|
||||
}
|
||||
}
|
||||
impl Drop for StalledHttpEndpoint {
|
||||
fn drop(&mut self) {
|
||||
self.running.store(false, Ordering::Release);
|
||||
}
|
||||
}
|
||||
fn next_pulse(rx: &std::sync::mpsc::Receiver<String>, timeout: Duration) -> Result<Value> {
|
||||
let deadline = Instant::now() + timeout;
|
||||
loop {
|
||||
let left = deadline.saturating_duration_since(Instant::now());
|
||||
let line = rx
|
||||
.recv_timeout(left)
|
||||
.context("timed out waiting for projector pulse")?;
|
||||
if let Ok(v) = serde_json::from_str::<Value>(&line) {
|
||||
return Ok(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn cleanup_collection(base: &str, name: &str) -> Result<()> {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let url = format!("{}/collections/{}", base.trim_end_matches('/'), name);
|
||||
let response = client.delete(&url).send()?;
|
||||
if !response.status().is_success() && response.status().as_u16() != 404 {
|
||||
bail!(
|
||||
"failed to delete fixture collection {name}: {}",
|
||||
response.status()
|
||||
)
|
||||
}
|
||||
let verification = client.get(&url).send()?.status();
|
||||
if verification.as_u16() != 404 {
|
||||
bail!(
|
||||
"fixture collection cleanup verification expected HTTP 404 for {name}, observed {verification}"
|
||||
)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn stop(child: &mut Child) {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
|
||||
struct ChildGuard(Child);
|
||||
impl Deref for ChildGuard {
|
||||
type Target = Child;
|
||||
fn deref(&self) -> &Child {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl DerefMut for ChildGuard {
|
||||
fn deref_mut(&mut self) -> &mut Child {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
impl Drop for ChildGuard {
|
||||
fn drop(&mut self) {
|
||||
stop(&mut self.0);
|
||||
}
|
||||
}
|
||||
|
||||
struct FixtureCollections {
|
||||
base: String,
|
||||
names: Vec<String>,
|
||||
cleaned: bool,
|
||||
}
|
||||
impl FixtureCollections {
|
||||
fn new(base: String, names: Vec<String>) -> Self {
|
||||
Self {
|
||||
base,
|
||||
names,
|
||||
cleaned: false,
|
||||
}
|
||||
}
|
||||
fn cleanup(&mut self) -> Result<()> {
|
||||
for name in &self.names {
|
||||
cleanup_collection(&self.base, name)?;
|
||||
}
|
||||
self.cleaned = true;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl Drop for FixtureCollections {
|
||||
fn drop(&mut self) {
|
||||
if !self.cleaned {
|
||||
for name in &self.names {
|
||||
let _ = cleanup_collection(&self.base, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn sibling(dir: &Path, name: &str) -> PathBuf {
|
||||
dir.join(if cfg!(windows) {
|
||||
format!("{name}.exe")
|
||||
} else {
|
||||
name.into()
|
||||
})
|
||||
}
|
||||
fn s(path: &Path) -> String {
|
||||
path.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn fixture_has_no_canonical_store_or_default_collection_mouth() {
|
||||
let source = include_str!("epiphany-semantic-recovery-smoke.rs");
|
||||
let forbidden = [
|
||||
["state/", "agents.msgpack"].concat(),
|
||||
["state\\", "agents.msgpack"].concat(),
|
||||
[".epiphany", "-run"].concat(),
|
||||
["epiphany_mind", "_v1"].concat(),
|
||||
["epiphany_modeling", "_v1"].concat(),
|
||||
];
|
||||
for forbidden in forbidden {
|
||||
assert!(
|
||||
!source.contains(&forbidden),
|
||||
"forbidden live surface: {forbidden}"
|
||||
);
|
||||
}
|
||||
let mutation_mouth = ["seed_abandoned_memory_semantic_projection", "_for_smoke"].concat();
|
||||
assert!(!source.contains(&mutation_mouth));
|
||||
assert!(source.contains("inspect_memory_semantic_recovery_for_smoke"));
|
||||
assert!(source.contains("Command::new(&projector)"));
|
||||
assert!(source.contains("Command::new(exe)"));
|
||||
let manifest = include_str!("../../Cargo.toml");
|
||||
assert!(manifest.contains("required-features = [\"semantic-recovery-smoke\"]"));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use epiphany_core::EpiphanyBranchRecord;
|
||||
use epiphany_core::EpiphanyLedgerEvidenceRecord;
|
||||
use epiphany_core::add_state_branch;
|
||||
use epiphany_core::append_state_evidence;
|
||||
use epiphany_core::close_state_branch;
|
||||
use epiphany_core::state_ledger_status;
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut args = env::args().skip(1);
|
||||
let Some(command) = args.next() else {
|
||||
print_usage();
|
||||
std::process::exit(2);
|
||||
};
|
||||
match command.as_str() {
|
||||
"status" => {
|
||||
let store = require_path_arg(&mut args, "--store")?;
|
||||
print_json(&state_ledger_status(store)?)?;
|
||||
}
|
||||
"add-evidence" => {
|
||||
let store = require_path_arg(&mut args, "--store")?;
|
||||
let record = EpiphanyLedgerEvidenceRecord {
|
||||
ts: require_string_arg(&mut args, "--ts")?,
|
||||
evidence_type: require_string_arg(&mut args, "--type")?,
|
||||
status: require_string_arg(&mut args, "--status")?,
|
||||
note: require_string_arg(&mut args, "--note")?,
|
||||
branch: optional_string_arg(&mut args, "--branch")?,
|
||||
};
|
||||
print_json(&append_state_evidence(store, record)?)?;
|
||||
}
|
||||
"add-branch" => {
|
||||
let store = require_path_arg(&mut args, "--store")?;
|
||||
let id = require_string_arg(&mut args, "--id")?;
|
||||
let hypothesis = require_string_arg(&mut args, "--hypothesis")?;
|
||||
let (artifacts, note) = collect_branch_tail(&mut args)?;
|
||||
let branch = EpiphanyBranchRecord {
|
||||
id,
|
||||
hypothesis,
|
||||
status: "active".to_string(),
|
||||
artifacts,
|
||||
notes: note.unwrap_or_default(),
|
||||
};
|
||||
print_json(&add_state_branch(store, branch)?)?;
|
||||
}
|
||||
"close-branch" => {
|
||||
let store = require_path_arg(&mut args, "--store")?;
|
||||
let id = require_string_arg(&mut args, "--id")?;
|
||||
let status = require_string_arg(&mut args, "--status")?;
|
||||
let note = optional_string_arg(&mut args, "--note")?;
|
||||
print_json(&close_state_branch(store, &id, &status, note)?)?;
|
||||
}
|
||||
_ => {
|
||||
print_usage();
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn require_path_arg(args: &mut impl Iterator<Item = String>, name: &str) -> Result<PathBuf> {
|
||||
Ok(PathBuf::from(require_string_arg(args, name)?))
|
||||
}
|
||||
|
||||
fn require_string_arg(args: &mut impl Iterator<Item = String>, name: &str) -> Result<String> {
|
||||
let Some(flag) = args.next() else {
|
||||
anyhow::bail!("missing {name}");
|
||||
};
|
||||
if flag != name {
|
||||
anyhow::bail!("expected {name}, got {flag}");
|
||||
}
|
||||
args.next()
|
||||
.with_context(|| format!("missing value for {name}"))
|
||||
}
|
||||
|
||||
fn optional_string_arg(
|
||||
args: &mut impl Iterator<Item = String>,
|
||||
name: &str,
|
||||
) -> Result<Option<String>> {
|
||||
let remaining = args.collect::<Vec<_>>();
|
||||
let mut found = None;
|
||||
let mut index = 0;
|
||||
while index < remaining.len() {
|
||||
if remaining[index] == name {
|
||||
found = remaining.get(index + 1).cloned();
|
||||
index += 2;
|
||||
} else {
|
||||
anyhow::bail!("unexpected argument {}", remaining[index]);
|
||||
}
|
||||
}
|
||||
Ok(found)
|
||||
}
|
||||
|
||||
fn collect_branch_tail(
|
||||
args: &mut impl Iterator<Item = String>,
|
||||
) -> Result<(Vec<String>, Option<String>)> {
|
||||
let remaining = args.collect::<Vec<_>>();
|
||||
let mut artifacts = Vec::new();
|
||||
let mut note = None;
|
||||
let mut index = 0;
|
||||
while index < remaining.len() {
|
||||
if remaining[index] == "--artifact" {
|
||||
artifacts.push(
|
||||
remaining
|
||||
.get(index + 1)
|
||||
.cloned()
|
||||
.context("missing value for --artifact")?,
|
||||
);
|
||||
index += 2;
|
||||
} else if remaining[index] == "--note" {
|
||||
note = Some(
|
||||
remaining
|
||||
.get(index + 1)
|
||||
.cloned()
|
||||
.context("missing value for --note")?,
|
||||
);
|
||||
index += 2;
|
||||
} else {
|
||||
anyhow::bail!("unexpected argument {}", remaining[index]);
|
||||
}
|
||||
}
|
||||
Ok((artifacts, note))
|
||||
}
|
||||
|
||||
fn print_json<T: serde::Serialize>(value: &T) -> Result<()> {
|
||||
println!("{}", serde_json::to_string_pretty(value)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_usage() {
|
||||
eprintln!(
|
||||
"usage: epiphany-state-ledger-store <status|add-evidence|add-branch|close-branch> ..."
|
||||
);
|
||||
}
|
||||
|
|
@ -1,490 +0,0 @@
|
|||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
const PROJECT_VERSION: &str = "6000.1.10f1";
|
||||
const WRONG_VERSION: &str = "6000.4.2f1";
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let result = run_smoke()?;
|
||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_smoke() -> Result<Value> {
|
||||
let root = env::current_dir().context("failed to resolve current dir")?;
|
||||
let workspace = root.join(".epiphany-smoke").join("unity-bridge-workspace");
|
||||
let artifact_root = root.join(".epiphany-smoke").join("unity-bridge-artifacts");
|
||||
let fake_roots = root.join(".epiphany-smoke").join("unity-bridge-editors");
|
||||
let result_path = root
|
||||
.join(".epiphany-smoke")
|
||||
.join("unity-bridge-smoke-result.json");
|
||||
prepare_project(&workspace, false)?;
|
||||
reset_path(&artifact_root)?;
|
||||
reset_path(&fake_roots)?;
|
||||
|
||||
let wrong_editor = create_fake_editor(&fake_roots, WRONG_VERSION)?;
|
||||
let missing = run_bridge(
|
||||
&root,
|
||||
&workspace,
|
||||
&artifact_root,
|
||||
&fake_roots,
|
||||
&["inspect"],
|
||||
true,
|
||||
)?;
|
||||
require(
|
||||
missing["status"] == "missingEditor",
|
||||
"wrong-only Hub root should not satisfy pin",
|
||||
)?;
|
||||
require(
|
||||
missing["projectVersion"] == PROJECT_VERSION,
|
||||
"inspection should report the project-pinned version",
|
||||
)?;
|
||||
require_artifact(&missing, "unity-bridge-summary.json")?;
|
||||
require_artifact(&missing, "unity-bridge-inspection.md")?;
|
||||
|
||||
let exact_editor = create_fake_editor(&fake_roots, PROJECT_VERSION)?;
|
||||
let resolved = run_bridge(
|
||||
&root,
|
||||
&workspace,
|
||||
&artifact_root,
|
||||
&fake_roots,
|
||||
&["inspect"],
|
||||
true,
|
||||
)?;
|
||||
require(
|
||||
resolved["status"] == "resolved",
|
||||
"exact editor path should resolve the pin",
|
||||
)?;
|
||||
require(
|
||||
resolved["editorPath"] == exact_editor.to_string_lossy().as_ref(),
|
||||
"inspection should resolve exact editor",
|
||||
)?;
|
||||
require(
|
||||
resolved
|
||||
.pointer("/editorBridge/exists")
|
||||
.and_then(Value::as_bool)
|
||||
== Some(false),
|
||||
"fresh smoke project should not have bridge package yet",
|
||||
)?;
|
||||
|
||||
let planned = run_bridge(
|
||||
&root,
|
||||
&workspace,
|
||||
&artifact_root,
|
||||
&fake_roots,
|
||||
&["run", "--dry-run", "--", "-nographics"],
|
||||
true,
|
||||
)?;
|
||||
require(
|
||||
planned["status"] == "resolved",
|
||||
"dry run should still require exact editor",
|
||||
)?;
|
||||
require(
|
||||
planned["runStatus"] == "planned",
|
||||
"dry run should plan instead of executing",
|
||||
)?;
|
||||
let command = value_array(&planned, "command")?;
|
||||
require(
|
||||
command.first().and_then(Value::as_str) == Some(exact_editor.to_str().unwrap_or_default()),
|
||||
"command should use exact editor path",
|
||||
)?;
|
||||
require_command_contains(command, "-batchmode")?;
|
||||
require_command_contains(command, "-quit")?;
|
||||
require_command_contains(command, "-projectPath")?;
|
||||
require_command_contains(command, "-logFile")?;
|
||||
require_command_contains(command, "-nographics")?;
|
||||
require_command_contains(command, &workspace.to_string_lossy())?;
|
||||
require(
|
||||
!command
|
||||
.iter()
|
||||
.any(|item| item.as_str() == Some(wrong_editor.to_str().unwrap_or_default())),
|
||||
"command must not use wrong editor",
|
||||
)?;
|
||||
require_artifact(&planned, "unity-command.json")?;
|
||||
|
||||
let method_override = run_bridge(
|
||||
&root,
|
||||
&workspace,
|
||||
&artifact_root,
|
||||
&fake_roots,
|
||||
&[
|
||||
"run",
|
||||
"--dry-run",
|
||||
"--",
|
||||
"-ExecuteMethod",
|
||||
"Epiphany.SmokeProbe",
|
||||
],
|
||||
false,
|
||||
)?;
|
||||
require(
|
||||
method_override["status"] == "error"
|
||||
&& method_override["error"]
|
||||
.as_str()
|
||||
.is_some_and(|error| error.contains("executeMethod")),
|
||||
"caller-authored executeMethod must fail at the bridge owner boundary",
|
||||
)?;
|
||||
|
||||
let missing_package = run_bridge(
|
||||
&root,
|
||||
&workspace,
|
||||
&artifact_root,
|
||||
&fake_roots,
|
||||
&[
|
||||
"probe",
|
||||
"--dry-run",
|
||||
"--operation",
|
||||
"scene-facts",
|
||||
"--scene",
|
||||
"Assets/Scenes/Main.unity",
|
||||
],
|
||||
false,
|
||||
)?;
|
||||
require(
|
||||
missing_package["status"] == "missingEditorBridgePackage",
|
||||
"named probes should require the resident editor package",
|
||||
)?;
|
||||
require(
|
||||
missing_package["runStatus"] == "blocked",
|
||||
"missing package probe should be blocked",
|
||||
)?;
|
||||
|
||||
prepare_project(&workspace, true)?;
|
||||
let bridged = run_bridge(
|
||||
&root,
|
||||
&workspace,
|
||||
&artifact_root,
|
||||
&fake_roots,
|
||||
&["inspect"],
|
||||
true,
|
||||
)?;
|
||||
require(
|
||||
bridged["status"] == "resolved",
|
||||
"bridge project should still resolve exact editor",
|
||||
)?;
|
||||
require(
|
||||
bridged
|
||||
.pointer("/editorBridge/exists")
|
||||
.and_then(Value::as_bool)
|
||||
== Some(true),
|
||||
"resident editor bridge should be detected",
|
||||
)?;
|
||||
|
||||
let scene_probe = run_bridge(
|
||||
&root,
|
||||
&workspace,
|
||||
&artifact_root,
|
||||
&fake_roots,
|
||||
&[
|
||||
"probe",
|
||||
"--dry-run",
|
||||
"--operation",
|
||||
"scene-facts",
|
||||
"--scene",
|
||||
"Assets/Scenes/Main.unity",
|
||||
"--max-objects",
|
||||
"25",
|
||||
"--max-properties",
|
||||
"12",
|
||||
],
|
||||
true,
|
||||
)?;
|
||||
require(
|
||||
scene_probe["runStatus"] == "planned",
|
||||
"scene probe should plan under dry run",
|
||||
)?;
|
||||
let scene_command = value_array(&scene_probe, "command")?;
|
||||
require(
|
||||
scene_command.first().and_then(Value::as_str)
|
||||
== Some(exact_editor.to_str().unwrap_or_default()),
|
||||
"scene probe should use exact editor path",
|
||||
)?;
|
||||
require_command_contains(scene_command, "-executeMethod")?;
|
||||
require_command_contains(
|
||||
scene_command,
|
||||
"GameCult.Epiphany.Unity.EpiphanyEditorBridge.RunProbe",
|
||||
)?;
|
||||
require_command_contains(scene_command, "-epiphanyArtifactDir")?;
|
||||
let scene_artifact_path = path_value(&scene_probe, "artifactPath")?;
|
||||
require_command_contains(scene_command, &scene_artifact_path.to_string_lossy())?;
|
||||
require_command_contains(scene_command, "-epiphanyOperation")?;
|
||||
require_command_contains(scene_command, "scene-facts")?;
|
||||
require_command_contains(scene_command, "-epiphanyScene")?;
|
||||
require_command_contains(scene_command, "Assets/Scenes/Main.unity")?;
|
||||
require_command_contains(scene_command, "-epiphanyMaxObjects")?;
|
||||
require_command_contains(scene_command, "25")?;
|
||||
let scene_artifact = read_command_artifact(&scene_probe)?;
|
||||
require(
|
||||
scene_artifact["operation"] == "scene-facts",
|
||||
"scene command artifact should name operation",
|
||||
)?;
|
||||
require(
|
||||
array_contains(scene_artifact.get("expectedArtifacts"), "scene-facts.json"),
|
||||
"scene facts artifact should be expected",
|
||||
)?;
|
||||
|
||||
let compilation = run_bridge(
|
||||
&root,
|
||||
&workspace,
|
||||
&artifact_root,
|
||||
&fake_roots,
|
||||
&["check-compilation", "--dry-run"],
|
||||
true,
|
||||
)?;
|
||||
require(
|
||||
compilation["runStatus"] == "planned",
|
||||
"compilation probe should plan under dry run",
|
||||
)?;
|
||||
let compilation_artifact = read_command_artifact(&compilation)?;
|
||||
require(
|
||||
array_contains(
|
||||
compilation_artifact.get("expectedArtifacts"),
|
||||
"compilation.json",
|
||||
),
|
||||
"compilation artifact should be expected",
|
||||
)?;
|
||||
|
||||
let tests = run_bridge(
|
||||
&root,
|
||||
&workspace,
|
||||
&artifact_root,
|
||||
&fake_roots,
|
||||
&[
|
||||
"run-tests",
|
||||
"--dry-run",
|
||||
"--platform",
|
||||
"editmode",
|
||||
"--filter",
|
||||
"SmokeSuite",
|
||||
],
|
||||
true,
|
||||
)?;
|
||||
require(
|
||||
tests["runStatus"] == "planned",
|
||||
"Unity test run should plan under dry run",
|
||||
)?;
|
||||
let test_command = value_array(&tests, "command")?;
|
||||
require_command_contains(test_command, "-runTests")?;
|
||||
require_command_contains(test_command, "-testPlatform")?;
|
||||
require_command_contains(test_command, "editmode")?;
|
||||
require_command_contains(test_command, "-testFilter")?;
|
||||
require_command_contains(test_command, "SmokeSuite")?;
|
||||
let tests_artifact = read_command_artifact(&tests)?;
|
||||
require(
|
||||
array_contains(tests_artifact.get("expectedArtifacts"), "test-results.xml"),
|
||||
"test results should be expected",
|
||||
)?;
|
||||
|
||||
let blocked = run_bridge(
|
||||
&root,
|
||||
&workspace,
|
||||
&artifact_root,
|
||||
&fake_roots.join("missing"),
|
||||
&["run", "--dry-run", "--", "-nographics"],
|
||||
false,
|
||||
)?;
|
||||
require(
|
||||
blocked["runStatus"] == "blocked",
|
||||
"missing editor run should be blocked",
|
||||
)?;
|
||||
|
||||
let result = json!({
|
||||
"workspace": workspace,
|
||||
"artifactRoot": artifact_root,
|
||||
"missingStatus": missing["status"],
|
||||
"editorResolutionStatus": resolved["status"],
|
||||
"plannedStatus": planned["runStatus"],
|
||||
"methodOverrideStatus": method_override["status"],
|
||||
"missingPackageStatus": missing_package["status"],
|
||||
"sceneProbeStatus": scene_probe["runStatus"],
|
||||
"testStatus": tests["runStatus"],
|
||||
"blockedStatus": blocked["runStatus"],
|
||||
"resolvedEditor": resolved["editorPath"],
|
||||
});
|
||||
write_json(&result_path, &result)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn run_bridge(
|
||||
root: &Path,
|
||||
workspace: &Path,
|
||||
artifact_root: &Path,
|
||||
editor_roots: &Path,
|
||||
args: &[&str],
|
||||
expect_success: bool,
|
||||
) -> Result<Value> {
|
||||
let bridge = native_bridge_exe()?;
|
||||
ensure_bridge_built(root, "epiphany-unity-bridge", &bridge)?;
|
||||
let output = Command::new(bridge)
|
||||
.current_dir(root)
|
||||
.env("EPIPHANY_UNITY_EDITOR_ROOTS", editor_roots)
|
||||
.arg(args[0])
|
||||
.arg("--project-path")
|
||||
.arg(workspace)
|
||||
.arg("--artifact-root")
|
||||
.arg(artifact_root)
|
||||
.args(&args[1..])
|
||||
.output()
|
||||
.context("failed to run native unity bridge")?;
|
||||
if expect_success {
|
||||
require(
|
||||
output.status.success(),
|
||||
&format!(
|
||||
"bridge should succeed, got {:?}: {}",
|
||||
output.status.code(),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
),
|
||||
)?;
|
||||
} else {
|
||||
require(
|
||||
!output.status.success(),
|
||||
"bridge should fail for blocked runtime execution",
|
||||
)?;
|
||||
}
|
||||
serde_json::from_slice(&output.stdout).with_context(|| {
|
||||
format!(
|
||||
"bridge did not return JSON: {}",
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn prepare_project(workspace: &Path, include_bridge: bool) -> Result<()> {
|
||||
reset_path(workspace)?;
|
||||
let settings = workspace.join("ProjectSettings");
|
||||
fs::create_dir_all(&settings)?;
|
||||
fs::write(
|
||||
settings.join("ProjectVersion.txt"),
|
||||
format!(
|
||||
"m_EditorVersion: {PROJECT_VERSION}\nm_EditorVersionWithRevision: {PROJECT_VERSION} (3c681a6c22ff)\n"
|
||||
),
|
||||
)?;
|
||||
if include_bridge {
|
||||
let bridge = workspace
|
||||
.join("Assets")
|
||||
.join("Editor")
|
||||
.join("Epiphany")
|
||||
.join("EpiphanyEditorBridge.cs");
|
||||
fs::create_dir_all(bridge.parent().expect("bridge path has parent"))?;
|
||||
fs::write(
|
||||
bridge,
|
||||
"// smoke marker for resident Epiphany editor bridge\n",
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_fake_editor(root: &Path, version: &str) -> Result<PathBuf> {
|
||||
let editor = root.join(version).join("Editor").join("Unity.exe");
|
||||
fs::create_dir_all(editor.parent().expect("editor path has parent"))?;
|
||||
fs::write(&editor, "fake unity binary for dry-run smoke only\n")?;
|
||||
Ok(editor)
|
||||
}
|
||||
|
||||
fn read_command_artifact(summary: &Value) -> Result<Value> {
|
||||
let command_path = path_value(summary, "artifactPath")?.join("unity-command.json");
|
||||
require(command_path.exists(), "missing unity-command.json")?;
|
||||
let bytes = fs::read(&command_path)?;
|
||||
serde_json::from_slice(&bytes).context("failed to parse unity-command.json")
|
||||
}
|
||||
|
||||
fn require_artifact(summary: &Value, name: &str) -> Result<()> {
|
||||
let artifact_path = path_value(summary, "artifactPath")?;
|
||||
require(
|
||||
artifact_path.join(name).exists(),
|
||||
&format!("missing bridge artifact: {name}"),
|
||||
)
|
||||
}
|
||||
|
||||
fn require_command_contains(command: &[Value], value: &str) -> Result<()> {
|
||||
require(
|
||||
command.iter().any(|item| item.as_str() == Some(value)),
|
||||
&format!("command should contain {value:?}: {command:?}"),
|
||||
)
|
||||
}
|
||||
|
||||
fn native_bridge_exe() -> Result<PathBuf> {
|
||||
let target_dir = env::var_os("CARGO_TARGET_DIR")
|
||||
.unwrap_or_else(|| r"C:\Users\Meta\.cargo-target-codex".into());
|
||||
Ok(PathBuf::from(target_dir)
|
||||
.join("debug")
|
||||
.join(format!("epiphany-unity-bridge{}", env::consts::EXE_SUFFIX)))
|
||||
}
|
||||
|
||||
fn ensure_bridge_built(root: &Path, bin: &str, exe: &Path) -> Result<()> {
|
||||
if exe.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
let status = Command::new("cargo")
|
||||
.current_dir(root)
|
||||
.arg("build")
|
||||
.arg("--manifest-path")
|
||||
.arg(root.join("epiphany-core").join("Cargo.toml"))
|
||||
.arg("--bin")
|
||||
.arg(bin)
|
||||
.status()
|
||||
.with_context(|| format!("failed to build native {bin}"))?;
|
||||
require(
|
||||
status.success() && exe.exists(),
|
||||
&format!("native {bin} executable was not built at {}", exe.display()),
|
||||
)
|
||||
}
|
||||
|
||||
fn value_array<'a>(value: &'a Value, key: &str) -> Result<&'a Vec<Value>> {
|
||||
value
|
||||
.get(key)
|
||||
.and_then(Value::as_array)
|
||||
.ok_or_else(|| anyhow!("missing array field {key}"))
|
||||
}
|
||||
|
||||
fn path_value(value: &Value, key: &str) -> Result<PathBuf> {
|
||||
value
|
||||
.get(key)
|
||||
.and_then(Value::as_str)
|
||||
.map(PathBuf::from)
|
||||
.ok_or_else(|| anyhow!("missing path field {key}"))
|
||||
}
|
||||
|
||||
fn array_contains(value: Option<&Value>, needle: &str) -> bool {
|
||||
value
|
||||
.and_then(Value::as_array)
|
||||
.is_some_and(|items| items.iter().any(|item| item.as_str() == Some(needle)))
|
||||
}
|
||||
|
||||
fn reset_path(path: &Path) -> Result<()> {
|
||||
if path.exists() {
|
||||
let root = env::current_dir()?.join(".epiphany-smoke").canonicalize()?;
|
||||
let resolved = path.canonicalize()?;
|
||||
if resolved == root || !resolved.starts_with(&root) {
|
||||
return Err(anyhow!(
|
||||
"refusing to reset non-smoke path: {}",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
fs::remove_dir_all(path)?;
|
||||
}
|
||||
fs::create_dir_all(path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_json(path: &Path, value: &Value) -> Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::write(path, format!("{}\n", serde_json::to_string_pretty(value)?))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn require(condition: bool, message: &str) -> Result<()> {
|
||||
if condition {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("{message}"))
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,620 +0,0 @@
|
|||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use postgres::NoTls;
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::blocking::ClientBuilder;
|
||||
use reqwest::header::HeaderMap;
|
||||
use reqwest::header::HeaderValue;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
const STATUS_SCHEMA_VERSION: &str = "epiphany.void_memory_status.v0";
|
||||
const SEARCH_SCHEMA_VERSION: &str = "epiphany.void_memory_search.v0";
|
||||
const CONTEXT_SCHEMA_VERSION: &str = "epiphany.void_message_context.v0";
|
||||
const DEFAULT_CONFIG: &str = "state/void-memory.toml";
|
||||
const MAX_LIMIT: usize = 20;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut args = env::args().skip(1);
|
||||
let Some(command) = args.next() else {
|
||||
return usage();
|
||||
};
|
||||
let mut config_path = PathBuf::from(DEFAULT_CONFIG);
|
||||
let mut query: Option<String> = None;
|
||||
let mut message_id: Option<String> = None;
|
||||
let mut limit = 5_usize;
|
||||
let mut before = 4_usize;
|
||||
let mut after = 4_usize;
|
||||
let mut guild_id: Option<String> = None;
|
||||
let mut channel_id: Option<String> = None;
|
||||
let mut author_id: Option<String> = None;
|
||||
let mut repo_name: Option<String> = None;
|
||||
let mut path_prefix: Option<String> = None;
|
||||
let mut language: Option<String> = None;
|
||||
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--config" => config_path = PathBuf::from(take(&mut args, "--config")?),
|
||||
"--query" => query = Some(take(&mut args, "--query")?),
|
||||
"--message-id" => message_id = Some(take(&mut args, "--message-id")?),
|
||||
"--limit" => limit = take(&mut args, "--limit")?.parse()?,
|
||||
"--before" => before = take(&mut args, "--before")?.parse()?,
|
||||
"--after" => after = take(&mut args, "--after")?.parse()?,
|
||||
"--guild-id" => guild_id = Some(take(&mut args, "--guild-id")?),
|
||||
"--channel-id" => channel_id = Some(take(&mut args, "--channel-id")?),
|
||||
"--author-id" => author_id = Some(take(&mut args, "--author-id")?),
|
||||
"--repo-name" => repo_name = Some(take(&mut args, "--repo-name")?),
|
||||
"--path-prefix" => path_prefix = Some(take(&mut args, "--path-prefix")?),
|
||||
"--language" => language = Some(take(&mut args, "--language")?),
|
||||
other => return Err(anyhow!("unknown argument {other:?}")),
|
||||
}
|
||||
}
|
||||
|
||||
limit = limit.clamp(1, MAX_LIMIT);
|
||||
before = before.min(MAX_LIMIT);
|
||||
after = after.min(MAX_LIMIT);
|
||||
let config = BridgeConfig::load(&config_path)?;
|
||||
let output = match command.as_str() {
|
||||
"status" => run_status(&config),
|
||||
"search-history" => run_search(
|
||||
&config,
|
||||
query.context("missing --query")?,
|
||||
limit,
|
||||
SearchCorpus::History {
|
||||
guild_id,
|
||||
channel_id,
|
||||
author_id,
|
||||
},
|
||||
),
|
||||
"search-sources" => run_search(
|
||||
&config,
|
||||
query.context("missing --query")?,
|
||||
limit,
|
||||
SearchCorpus::Source {
|
||||
repo_name,
|
||||
path_prefix,
|
||||
language,
|
||||
},
|
||||
),
|
||||
"message-context" => run_message_context(
|
||||
&config,
|
||||
message_id.context("missing --message-id")?,
|
||||
before,
|
||||
after,
|
||||
),
|
||||
"smoke" => run_smoke(),
|
||||
_ => return usage(),
|
||||
}?;
|
||||
println!("{}", serde_json::to_string_pretty(&output)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct BridgeConfig {
|
||||
database_dsn_env: Option<String>,
|
||||
database_dsn: String,
|
||||
qdrant_url: String,
|
||||
qdrant_api_key_env: Option<String>,
|
||||
qdrant_history_collection: String,
|
||||
qdrant_source_collection: String,
|
||||
qdrant_timeout_ms: u64,
|
||||
ollama_base_url: String,
|
||||
ollama_model: String,
|
||||
ollama_timeout_ms: u64,
|
||||
history_query_instruction: String,
|
||||
source_query_instruction: String,
|
||||
archive_path: PathBuf,
|
||||
}
|
||||
|
||||
impl BridgeConfig {
|
||||
fn load(path: &Path) -> Result<Self> {
|
||||
let raw = fs::read_to_string(path)
|
||||
.with_context(|| format!("failed to read {}", path.display()))?;
|
||||
Ok(Self {
|
||||
database_dsn_env: toml_string(&raw, "database_dsn_env"),
|
||||
database_dsn: toml_string(&raw, "database_dsn")
|
||||
.unwrap_or_else(|| "postgres://voidbot:voidbot@localhost:5432/voidbot".to_string()),
|
||||
qdrant_url: normalize_base_url(
|
||||
&toml_string(&raw, "qdrant_url")
|
||||
.unwrap_or_else(|| "http://127.0.0.1:6333".to_string()),
|
||||
),
|
||||
qdrant_api_key_env: toml_string(&raw, "qdrant_api_key_env"),
|
||||
qdrant_history_collection: toml_string(&raw, "qdrant_history_collection")
|
||||
.unwrap_or_else(|| "voidbot_discord_history_chunks".to_string()),
|
||||
qdrant_source_collection: toml_string(&raw, "qdrant_source_collection")
|
||||
.unwrap_or_else(|| "voidbot_repository_source_chunks".to_string()),
|
||||
qdrant_timeout_ms: toml_number(&raw, "qdrant_timeout_ms").unwrap_or(30000),
|
||||
ollama_base_url: normalize_base_url(
|
||||
&toml_string(&raw, "ollama_base_url")
|
||||
.unwrap_or_else(|| "http://127.0.0.1:11434".to_string()),
|
||||
),
|
||||
ollama_model: toml_string(&raw, "ollama_model")
|
||||
.unwrap_or_else(|| "qwen3-embedding:0.6b".to_string()),
|
||||
ollama_timeout_ms: toml_number(&raw, "ollama_timeout_ms").unwrap_or(30000),
|
||||
history_query_instruction: toml_string(&raw, "history_query_instruction")
|
||||
.unwrap_or_else(|| "Given a Discord history question, retrieve relevant messages and discussion snippets that answer it.".to_string()),
|
||||
source_query_instruction: toml_string(&raw, "source_query_instruction")
|
||||
.unwrap_or_else(|| "Given a source-tree, codebase, or lore question, retrieve relevant files, code snippets, and lore passages that answer it.".to_string()),
|
||||
archive_path: PathBuf::from(
|
||||
toml_string(&raw, "archive_path")
|
||||
.unwrap_or_else(|| r"E:\Projects\VoidBot\.voidbot\rag\messages.json".to_string()),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn database_dsn(&self) -> String {
|
||||
self.database_dsn_env
|
||||
.as_ref()
|
||||
.and_then(|name| env::var(name).ok())
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| self.database_dsn.clone())
|
||||
}
|
||||
|
||||
fn qdrant_api_key(&self) -> Option<String> {
|
||||
self.qdrant_api_key_env
|
||||
.as_ref()
|
||||
.and_then(|name| env::var(name).ok())
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
enum SearchCorpus {
|
||||
History {
|
||||
guild_id: Option<String>,
|
||||
channel_id: Option<String>,
|
||||
author_id: Option<String>,
|
||||
},
|
||||
Source {
|
||||
repo_name: Option<String>,
|
||||
path_prefix: Option<String>,
|
||||
language: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
fn run_status(config: &BridgeConfig) -> Result<Value> {
|
||||
let postgres = postgres_status(config);
|
||||
let qdrant = qdrant_status(config);
|
||||
let archive = archive_status(&config.archive_path);
|
||||
Ok(json!({
|
||||
"schemaVersion": STATUS_SCHEMA_VERSION,
|
||||
"ok": postgres["ok"].as_bool().unwrap_or(false)
|
||||
|| qdrant["history"]["ok"].as_bool().unwrap_or(false)
|
||||
|| archive["ok"].as_bool().unwrap_or(false),
|
||||
"postgres": postgres,
|
||||
"qdrant": qdrant,
|
||||
"archive": archive,
|
||||
"notes": [
|
||||
"Void currently keeps raw Discord archives file-backed under .voidbot while Qdrant owns semantic history/source vectors.",
|
||||
"Postgres is checked for live Void state organs: jobs, audit events, interaction memory, identity profiles, and rate limits."
|
||||
],
|
||||
}))
|
||||
}
|
||||
|
||||
fn run_search(
|
||||
config: &BridgeConfig,
|
||||
query: String,
|
||||
limit: usize,
|
||||
corpus: SearchCorpus,
|
||||
) -> Result<Value> {
|
||||
let (collection, instruction, filter, corpus_name) = match corpus {
|
||||
SearchCorpus::History {
|
||||
guild_id,
|
||||
channel_id,
|
||||
author_id,
|
||||
} => (
|
||||
config.qdrant_history_collection.as_str(),
|
||||
config.history_query_instruction.as_str(),
|
||||
qdrant_filter([
|
||||
("corpusKind", Some("discord_history".to_string())),
|
||||
("guildId", guild_id),
|
||||
("channelId", channel_id),
|
||||
("authorId", author_id),
|
||||
]),
|
||||
"discord_history",
|
||||
),
|
||||
SearchCorpus::Source {
|
||||
repo_name,
|
||||
path_prefix,
|
||||
language,
|
||||
} => (
|
||||
config.qdrant_source_collection.as_str(),
|
||||
config.source_query_instruction.as_str(),
|
||||
qdrant_filter([
|
||||
("corpusKind", Some("repository_source".to_string())),
|
||||
("repoName", repo_name),
|
||||
("pathPrefixes", path_prefix.map(normalize_path_prefix)),
|
||||
("language", language),
|
||||
]),
|
||||
"repository_source",
|
||||
),
|
||||
};
|
||||
let vector = embed_query(config, instruction, &query)?;
|
||||
let hits = query_qdrant(config, collection, &vector, limit, filter)?;
|
||||
Ok(json!({
|
||||
"schemaVersion": SEARCH_SCHEMA_VERSION,
|
||||
"ok": true,
|
||||
"corpus": corpus_name,
|
||||
"query": query,
|
||||
"collection": collection,
|
||||
"results": hits,
|
||||
}))
|
||||
}
|
||||
|
||||
fn run_message_context(
|
||||
config: &BridgeConfig,
|
||||
message_id: String,
|
||||
before: usize,
|
||||
after: usize,
|
||||
) -> Result<Value> {
|
||||
let archive = load_archive(&config.archive_path)?;
|
||||
let Some(anchor) = archive
|
||||
.messages
|
||||
.iter()
|
||||
.find(|message| message.id == message_id)
|
||||
else {
|
||||
return Ok(json!({
|
||||
"schemaVersion": CONTEXT_SCHEMA_VERSION,
|
||||
"ok": false,
|
||||
"reason": "message-not-found",
|
||||
"messageId": message_id,
|
||||
}));
|
||||
};
|
||||
let mut messages = archive
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|message| !message.deleted())
|
||||
.filter(|message| belongs_to_same_conversation(message, anchor))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
messages.sort_by(|left, right| left.timestamp.cmp(&right.timestamp));
|
||||
let anchor_index = messages
|
||||
.iter()
|
||||
.position(|message| message.id == message_id)
|
||||
.context("anchor disappeared from context set")?;
|
||||
let start = anchor_index.saturating_sub(before);
|
||||
let end = (anchor_index + after + 1).min(messages.len());
|
||||
Ok(json!({
|
||||
"schemaVersion": CONTEXT_SCHEMA_VERSION,
|
||||
"ok": true,
|
||||
"messageId": message_id,
|
||||
"archivePath": config.archive_path,
|
||||
"messages": messages[start..end].iter().map(format_message).collect::<Vec<_>>(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn postgres_status(config: &BridgeConfig) -> Value {
|
||||
match postgres::Client::connect(&config.database_dsn(), NoTls) {
|
||||
Ok(mut client) => {
|
||||
let tables = [
|
||||
"jobs",
|
||||
"audit_events",
|
||||
"interaction_memory_events",
|
||||
"interaction_identity_profiles",
|
||||
"void_usage_rate_limit_state",
|
||||
];
|
||||
let counts = tables
|
||||
.into_iter()
|
||||
.map(|table| {
|
||||
let count = client
|
||||
.query_one(&format!("select count(*)::bigint from {table}"), &[])
|
||||
.map(|row| row.get::<_, i64>(0));
|
||||
(table, count)
|
||||
})
|
||||
.map(|(table, count)| match count {
|
||||
Ok(count) => json!({"table": table, "ok": true, "count": count}),
|
||||
Err(error) => json!({"table": table, "ok": false, "error": error.to_string()}),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
json!({"ok": true, "dsn": redact_dsn(&config.database_dsn()), "tables": counts})
|
||||
}
|
||||
Err(error) => json!({
|
||||
"ok": false,
|
||||
"dsn": redact_dsn(&config.database_dsn()),
|
||||
"error": error.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn qdrant_status(config: &BridgeConfig) -> Value {
|
||||
json!({
|
||||
"url": config.qdrant_url,
|
||||
"history": qdrant_collection_status(config, &config.qdrant_history_collection),
|
||||
"source": qdrant_collection_status(config, &config.qdrant_source_collection),
|
||||
})
|
||||
}
|
||||
|
||||
fn qdrant_collection_status(config: &BridgeConfig, collection: &str) -> Value {
|
||||
let client = qdrant_client(config);
|
||||
match client
|
||||
.get(format!("{}/collections/{collection}", config.qdrant_url))
|
||||
.send()
|
||||
{
|
||||
Ok(response) => match decode_json(response) {
|
||||
Ok(payload) => json!({
|
||||
"ok": true,
|
||||
"collection": collection,
|
||||
"pointsCount": payload.pointer("/result/points_count"),
|
||||
"vectorsCount": payload.pointer("/result/vectors_count"),
|
||||
"status": payload.pointer("/result/status"),
|
||||
}),
|
||||
Err(error) => {
|
||||
json!({"ok": false, "collection": collection, "error": error.to_string()})
|
||||
}
|
||||
},
|
||||
Err(error) => json!({"ok": false, "collection": collection, "error": error.to_string()}),
|
||||
}
|
||||
}
|
||||
|
||||
fn archive_status(path: &Path) -> Value {
|
||||
match load_archive(path) {
|
||||
Ok(archive) => json!({
|
||||
"ok": true,
|
||||
"path": path,
|
||||
"messageCount": archive.messages.len(),
|
||||
"activeMessageCount": archive.messages.iter().filter(|message| !message.deleted()).count(),
|
||||
}),
|
||||
Err(error) => json!({"ok": false, "path": path, "error": error.to_string()}),
|
||||
}
|
||||
}
|
||||
|
||||
fn embed_query(config: &BridgeConfig, instruction: &str, query: &str) -> Result<Vec<f32>> {
|
||||
let client = ClientBuilder::new()
|
||||
.timeout(Duration::from_millis(config.ollama_timeout_ms))
|
||||
.build()
|
||||
.context("failed to build Ollama client")?;
|
||||
let formatted = format!("Instruct: {instruction}\nQuery: {query}");
|
||||
let response = client
|
||||
.post(format!("{}/api/embed", config.ollama_base_url))
|
||||
.json(&json!({"model": config.ollama_model, "input": [formatted]}))
|
||||
.send()
|
||||
.context("failed to contact Ollama embedding backend")?;
|
||||
let payload = decode_json(response).context("failed to decode Ollama embedding response")?;
|
||||
let embedding = payload["embeddings"]
|
||||
.as_array()
|
||||
.and_then(|items| items.first())
|
||||
.and_then(Value::as_array)
|
||||
.ok_or_else(|| anyhow!("Ollama embedding response did not include embeddings[0]"))?;
|
||||
embedding
|
||||
.iter()
|
||||
.map(|value| {
|
||||
value
|
||||
.as_f64()
|
||||
.map(|value| value as f32)
|
||||
.ok_or_else(|| anyhow!("Ollama embedding included a non-number"))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn query_qdrant(
|
||||
config: &BridgeConfig,
|
||||
collection: &str,
|
||||
vector: &[f32],
|
||||
limit: usize,
|
||||
filter: Option<Value>,
|
||||
) -> Result<Vec<Value>> {
|
||||
let response = qdrant_client(config)
|
||||
.post(format!(
|
||||
"{}/collections/{collection}/points/query",
|
||||
config.qdrant_url
|
||||
))
|
||||
.json(&json!({
|
||||
"query": vector,
|
||||
"limit": limit,
|
||||
"with_payload": true,
|
||||
"with_vector": false,
|
||||
"filter": filter,
|
||||
}))
|
||||
.send()
|
||||
.with_context(|| format!("failed to query Qdrant collection {collection}"))?;
|
||||
let payload = decode_json(response)?;
|
||||
let points = payload
|
||||
.pointer("/result/points")
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
Ok(points.into_iter().filter_map(format_qdrant_point).collect())
|
||||
}
|
||||
|
||||
fn format_qdrant_point(point: Value) -> Option<Value> {
|
||||
let payload = point.get("payload")?;
|
||||
Some(json!({
|
||||
"score": point.get("score"),
|
||||
"chunkId": payload.get("chunkId"),
|
||||
"sourceId": payload.get("sourceId"),
|
||||
"sourceKind": payload.get("sourceKind"),
|
||||
"text": payload.get("text"),
|
||||
"metadata": payload.get("metadata"),
|
||||
}))
|
||||
}
|
||||
|
||||
fn qdrant_client(config: &BridgeConfig) -> Client {
|
||||
let mut headers = HeaderMap::new();
|
||||
if let Some(api_key) = config.qdrant_api_key() {
|
||||
if let Ok(value) = HeaderValue::from_str(&api_key) {
|
||||
headers.insert("api-key", value);
|
||||
}
|
||||
}
|
||||
ClientBuilder::new()
|
||||
.timeout(Duration::from_millis(config.qdrant_timeout_ms))
|
||||
.default_headers(headers)
|
||||
.build()
|
||||
.expect("Qdrant client should build")
|
||||
}
|
||||
|
||||
fn qdrant_filter<const N: usize>(fields: [(&str, Option<String>); N]) -> Option<Value> {
|
||||
let must = fields
|
||||
.into_iter()
|
||||
.filter_map(|(key, value)| {
|
||||
let value = value?.trim().to_string();
|
||||
(!value.is_empty()).then(|| json!({"key": key, "match": {"value": value}}))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
(!must.is_empty()).then(|| json!({"must": must}))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
struct ArchiveStore {
|
||||
messages: Vec<ArchivedMessage>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ArchivedMessage {
|
||||
id: String,
|
||||
guild_id: Option<String>,
|
||||
channel_id: String,
|
||||
author_id: String,
|
||||
author_name: String,
|
||||
content: String,
|
||||
timestamp: String,
|
||||
deleted_at: Option<String>,
|
||||
thread_id: Option<String>,
|
||||
metadata: Option<Value>,
|
||||
}
|
||||
|
||||
impl ArchivedMessage {
|
||||
fn deleted(&self) -> bool {
|
||||
self.deleted_at
|
||||
.as_deref()
|
||||
.is_some_and(|value| !value.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
fn load_archive(path: &Path) -> Result<ArchiveStore> {
|
||||
let raw =
|
||||
fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?;
|
||||
serde_json::from_str(&raw).with_context(|| format!("failed to decode {}", path.display()))
|
||||
}
|
||||
|
||||
fn belongs_to_same_conversation(message: &ArchivedMessage, anchor: &ArchivedMessage) -> bool {
|
||||
message.channel_id == anchor.channel_id && message.thread_id == anchor.thread_id
|
||||
}
|
||||
|
||||
fn format_message(message: &ArchivedMessage) -> Value {
|
||||
json!({
|
||||
"id": message.id,
|
||||
"timestamp": message.timestamp,
|
||||
"authorId": message.author_id,
|
||||
"authorName": message.author_name,
|
||||
"guildId": message.guild_id,
|
||||
"channelId": message.channel_id,
|
||||
"threadId": message.thread_id,
|
||||
"content": message.content,
|
||||
"jumpUrl": message.metadata.as_ref().and_then(|metadata| metadata.get("jumpUrl")),
|
||||
})
|
||||
}
|
||||
|
||||
fn run_smoke() -> Result<Value> {
|
||||
let dir = env::temp_dir().join(format!(
|
||||
"epiphany-void-memory-smoke-{}",
|
||||
uuid::Uuid::new_v4()
|
||||
));
|
||||
fs::create_dir(&dir)?;
|
||||
let archive_path = dir.join("messages.json");
|
||||
fs::write(
|
||||
&archive_path,
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"version": 1,
|
||||
"messages": [
|
||||
{"id":"1","guildId":"g","channelId":"c","authorId":"a","authorName":"A","content":"before","timestamp":"2026-05-06T00:00:00Z","threadId":"t","metadata":{"jumpUrl":"https://discord.invalid/1"}},
|
||||
{"id":"2","guildId":"g","channelId":"c","authorId":"b","authorName":"B","content":"anchor","timestamp":"2026-05-06T00:01:00Z","threadId":"t","metadata":{"jumpUrl":"https://discord.invalid/2"}},
|
||||
{"id":"3","guildId":"g","channelId":"c","authorId":"a","authorName":"A","content":"after","timestamp":"2026-05-06T00:02:00Z","threadId":"t","metadata":{"jumpUrl":"https://discord.invalid/3"}}
|
||||
]
|
||||
}))?,
|
||||
)?;
|
||||
let config = BridgeConfig {
|
||||
database_dsn_env: None,
|
||||
database_dsn: "postgres://voidbot:voidbot@localhost:5432/voidbot".to_string(),
|
||||
qdrant_url: "http://127.0.0.1:1".to_string(),
|
||||
qdrant_api_key_env: None,
|
||||
qdrant_history_collection: "history".to_string(),
|
||||
qdrant_source_collection: "source".to_string(),
|
||||
qdrant_timeout_ms: 50,
|
||||
ollama_base_url: "http://127.0.0.1:1".to_string(),
|
||||
ollama_model: "qwen3-embedding:0.6b".to_string(),
|
||||
ollama_timeout_ms: 50,
|
||||
history_query_instruction: "history".to_string(),
|
||||
source_query_instruction: "source".to_string(),
|
||||
archive_path,
|
||||
};
|
||||
let context = run_message_context(&config, "2".to_string(), 1, 1)?;
|
||||
let status = run_status(&config)?;
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
Ok(json!({
|
||||
"ok": context["ok"] == true
|
||||
&& context["messages"].as_array().is_some_and(|messages| messages.len() == 3),
|
||||
"context": context,
|
||||
"status": status,
|
||||
}))
|
||||
}
|
||||
|
||||
fn decode_json(response: reqwest::blocking::Response) -> Result<Value> {
|
||||
let status = response.status();
|
||||
let body = response.text().context("failed to read response body")?;
|
||||
if !status.is_success() {
|
||||
return Err(anyhow!("HTTP {status}: {body}"));
|
||||
}
|
||||
serde_json::from_str(&body).context("response was not JSON")
|
||||
}
|
||||
|
||||
fn toml_string(raw: &str, key: &str) -> Option<String> {
|
||||
raw.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.starts_with('#'))
|
||||
.find_map(|line| {
|
||||
let (left, right) = line.split_once('=')?;
|
||||
(left.trim() == key).then(|| parse_toml_string(right.trim()))?
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_toml_string(value: &str) -> Option<String> {
|
||||
value
|
||||
.strip_prefix('"')
|
||||
.and_then(|value| value.strip_suffix('"'))
|
||||
.map(|value| value.replace("\\\\", "\\"))
|
||||
}
|
||||
|
||||
fn toml_number(raw: &str, key: &str) -> Option<u64> {
|
||||
raw.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.starts_with('#'))
|
||||
.find_map(|line| {
|
||||
let (left, right) = line.split_once('=')?;
|
||||
(left.trim() == key).then(|| right.trim().parse().ok())?
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_base_url(value: &str) -> String {
|
||||
value.trim_end_matches('/').to_string()
|
||||
}
|
||||
|
||||
fn normalize_path_prefix(value: String) -> String {
|
||||
value.replace('\\', "/").trim_start_matches('/').to_string()
|
||||
}
|
||||
|
||||
fn redact_dsn(dsn: &str) -> String {
|
||||
if let Some((prefix, rest)) = dsn.split_once("://") {
|
||||
if let Some(at) = rest.find('@') {
|
||||
return format!("{prefix}://***@{}", &rest[at + 1..]);
|
||||
}
|
||||
}
|
||||
dsn.to_string()
|
||||
}
|
||||
|
||||
fn take(args: &mut impl Iterator<Item = String>, name: &str) -> Result<String> {
|
||||
args.next()
|
||||
.ok_or_else(|| anyhow!("{name} requires a value"))
|
||||
}
|
||||
|
||||
fn usage() -> Result<()> {
|
||||
Err(anyhow!(
|
||||
"usage: epiphany-void-memory <status|search-history|search-sources|message-context|smoke> [--config <path>] [--query <text>] [--message-id <id>] [--limit <n>]"
|
||||
))
|
||||
}
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use chrono::Utc;
|
||||
use epiphany_core::EPIPHANY_CULTMESH_LOCAL_AREA_VERSE_ID;
|
||||
use epiphany_core::EPIPHANY_CULTMESH_WEKSA_LOWERING_RECEIPT_SCHEMA_VERSION;
|
||||
use epiphany_core::EpiphanyCultMeshWeksaLoweringReceiptEntry;
|
||||
use epiphany_core::WEKSA_INTERLINGUA_PACKET_SCHEMA_VERSION;
|
||||
use epiphany_core::WEKSA_TARGET_LOWERING_RECEIPT_SCHEMA_VERSION;
|
||||
use epiphany_core::WEKSA_TARGET_LOWERING_REQUEST_SCHEMA_VERSION;
|
||||
use epiphany_core::WeksaInterlinguaInput;
|
||||
use epiphany_core::WeksaSpeakerContext;
|
||||
use epiphany_core::build_weksa_interlingua_packet;
|
||||
use epiphany_core::build_weksa_lowering_prompt;
|
||||
use epiphany_core::build_weksa_target_lowering_request;
|
||||
use epiphany_core::load_latest_epiphany_cultmesh_weksa_lowering_receipt;
|
||||
use epiphany_core::record_weksa_target_lowering_receipt;
|
||||
use epiphany_core::write_epiphany_cultmesh_weksa_lowering_receipt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
if let Some(arg) = std::env::args().nth(1) {
|
||||
anyhow::bail!("Weksa smoke accepts no arguments; unexpected {arg:?}");
|
||||
}
|
||||
let cultmesh_store = PathBuf::from(".epiphany-smoke")
|
||||
.join("weksa-interlingua")
|
||||
.join("local-verse.ccmp");
|
||||
let runtime_id = "weksa-interlingua-smoke";
|
||||
let packet = build_weksa_interlingua_packet(WeksaInterlinguaInput {
|
||||
packet_id: "weksa-packet-persona-smoke".to_string(),
|
||||
source_interpreter_ref: "persona-interpreter:smoke-say".to_string(),
|
||||
source_speech_audit_ref: "persona-speech-audit:smoke-ok".to_string(),
|
||||
speaker: WeksaSpeakerContext {
|
||||
persona_id: "epiphany.Persona".to_string(),
|
||||
display_name: "Epiphany".to_string(),
|
||||
source_surface: "eve://epiphany/persona".to_string(),
|
||||
source_language: "en".to_string(),
|
||||
utterance_state_ref: "state/agents.msgpack#Persona:utterance-state".to_string(),
|
||||
},
|
||||
meaning:
|
||||
"Tell the room that Epiphany can keep working inside its branch-local body, while publication and merge still need Bifrost receipts."
|
||||
.to_string(),
|
||||
speech_act: "status-reply".to_string(),
|
||||
delivery_register: "warm-technical".to_string(),
|
||||
target_audience: "local public Persona room".to_string(),
|
||||
safety_notes: vec![
|
||||
"Do not claim upstream merge authority.".to_string(),
|
||||
"Do not expose private worker thought or private Verse payloads.".to_string(),
|
||||
],
|
||||
})?;
|
||||
assert_eq!(
|
||||
packet.schema_version,
|
||||
WEKSA_INTERLINGUA_PACKET_SCHEMA_VERSION
|
||||
);
|
||||
assert!(!packet.private_state_exposed);
|
||||
assert!(packet.meaning.contains("Bifrost receipts"));
|
||||
assert!(packet.contract.contains("interlingua cargo"));
|
||||
|
||||
let request = build_weksa_target_lowering_request(
|
||||
"weksa-lower-persona-smoke",
|
||||
packet,
|
||||
"es",
|
||||
"warm-technical",
|
||||
"discord",
|
||||
)?;
|
||||
assert_eq!(
|
||||
request.schema_version,
|
||||
WEKSA_TARGET_LOWERING_REQUEST_SCHEMA_VERSION
|
||||
);
|
||||
assert!(request.model_required);
|
||||
assert!(!request.private_state_exposed);
|
||||
|
||||
let prompt = build_weksa_lowering_prompt(&request);
|
||||
assert_contains(&prompt, "You are Weksa");
|
||||
assert_contains(&prompt, "Preserve meaning");
|
||||
assert_contains(&prompt, "Return only target-language text");
|
||||
assert_contains(&prompt, "Do not claim upstream merge authority");
|
||||
|
||||
let receipt = record_weksa_target_lowering_receipt(
|
||||
&request,
|
||||
"weksa-lowering-receipt-persona-smoke",
|
||||
"Epiphany puede seguir trabajando dentro de su cuerpo de rama local, mientras que la publicacion y la fusion aun necesitan recibos de Bifrost.",
|
||||
"deterministic-smoke-lowering",
|
||||
)?;
|
||||
assert_eq!(
|
||||
receipt.schema_version,
|
||||
WEKSA_TARGET_LOWERING_RECEIPT_SCHEMA_VERSION
|
||||
);
|
||||
assert!(receipt.lowered_text.contains("Bifrost"));
|
||||
assert!(receipt.transport_authority.contains("must publish"));
|
||||
assert!(!receipt.private_state_exposed);
|
||||
|
||||
let cultmesh_receipt = EpiphanyCultMeshWeksaLoweringReceiptEntry {
|
||||
schema_version: EPIPHANY_CULTMESH_WEKSA_LOWERING_RECEIPT_SCHEMA_VERSION.to_string(),
|
||||
receipt_id: receipt.receipt_id.clone(),
|
||||
runtime_id: runtime_id.to_string(),
|
||||
verse_id: EPIPHANY_CULTMESH_LOCAL_AREA_VERSE_ID.to_string(),
|
||||
packet_id: receipt.packet_id.clone(),
|
||||
request_id: receipt.request_id.clone(),
|
||||
persona_agent_id: request.packet.speaker.persona_id.clone(),
|
||||
target_language: receipt.target_language.clone(),
|
||||
target_register: receipt.target_register.clone(),
|
||||
delivery_surface: receipt.delivery_surface.clone(),
|
||||
lowering_method: receipt.lowering_method.clone(),
|
||||
transport_authority: receipt.transport_authority.clone(),
|
||||
publication_authorized: false,
|
||||
lowered_text_ref: "artifact://weksa/interlingua-smoke/es".to_string(),
|
||||
lowered_text_preview: receipt.lowered_text.clone(),
|
||||
created_at_utc: Utc::now().to_rfc3339(),
|
||||
private_state_exposed: false,
|
||||
notes: vec!["Executable smoke mirrored Weksa lowering into local Verse sight.".to_string()],
|
||||
};
|
||||
write_epiphany_cultmesh_weksa_lowering_receipt(&cultmesh_store, cultmesh_receipt.clone())?;
|
||||
let latest = load_latest_epiphany_cultmesh_weksa_lowering_receipt(&cultmesh_store, runtime_id)?
|
||||
.expect("Weksa lowering receipt should round trip through CultMesh");
|
||||
assert_eq!(latest.receipt_id, cultmesh_receipt.receipt_id);
|
||||
assert!(!latest.publication_authorized);
|
||||
assert!(!latest.private_state_exposed);
|
||||
|
||||
println!(
|
||||
"status=ok weksaInterlingua=packet loweringRequest=model-required loweringReceipt=recorded cultmeshReceipt=mirrored targetLanguage={} transportAuthority=none privateStateExposed=false",
|
||||
latest.target_language
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn assert_contains(haystack: &str, needle: &str) {
|
||||
assert!(
|
||||
haystack.contains(needle),
|
||||
"expected Weksa smoke artifact to contain `{needle}`"
|
||||
);
|
||||
}
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
use anyhow::{Result, anyhow};
|
||||
use serde_json::{Value, json};
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
const HISTORICAL_REASON: &str = "canonical frontier workflow required";
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut args = env::args().skip(1);
|
||||
let Some(command) = args.next() else {
|
||||
print_usage();
|
||||
std::process::exit(2);
|
||||
};
|
||||
let parsed = parse_legacy_args(args)?;
|
||||
let result = match command.as_str() {
|
||||
"overview" | "proof-bundle" | "status" => run_overview(parsed)?,
|
||||
"accept" | "derive-plan" | "imagine" | "plan-from-pressure" | "plan" | "run" | "adopt"
|
||||
| "promote" | "execute" | "exec" | "verify" | "soul-verify" | "close" | "closure"
|
||||
| "verify-close" | "readiness" | "readiness-report" | "mvp-readiness" | "export-proof"
|
||||
| "public-proof" | "tick" | "pulse" | "schedule" | "queue-run" | "run-queue"
|
||||
| "queue-tick" | "scheduler-run" | "serve" | "loop" | "daemon" => {
|
||||
return Err(canonical_frontier_required(&command));
|
||||
}
|
||||
other => return Err(anyhow!("unknown epiphany-work command {other:?}")),
|
||||
};
|
||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct LegacyArgs {
|
||||
workspace: Option<PathBuf>,
|
||||
item: Option<String>,
|
||||
}
|
||||
|
||||
fn parse_legacy_args(args: impl Iterator<Item = String>) -> Result<LegacyArgs> {
|
||||
let mut parsed = LegacyArgs::default();
|
||||
let mut args = args.peekable();
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--workspace" => {
|
||||
parsed.workspace = Some(PathBuf::from(required_value(&mut args, &arg)?))
|
||||
}
|
||||
"--item" => parsed.item = Some(required_value(&mut args, &arg)?),
|
||||
flag if flag.starts_with("--") => {
|
||||
if args.peek().is_some_and(|v| !v.starts_with("--")) {
|
||||
args.next();
|
||||
}
|
||||
}
|
||||
positional => return Err(anyhow!("unexpected positional argument {positional:?}")),
|
||||
}
|
||||
}
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
fn required_value(
|
||||
args: &mut std::iter::Peekable<impl Iterator<Item = String>>,
|
||||
flag: &str,
|
||||
) -> Result<String> {
|
||||
args.next()
|
||||
.filter(|v| !v.starts_with("--"))
|
||||
.ok_or_else(|| anyhow!("{flag} requires a value"))
|
||||
}
|
||||
|
||||
fn canonical_frontier_required(operation: &str) -> anyhow::Error {
|
||||
anyhow!("{operation} is historical-only: {HISTORICAL_REASON}")
|
||||
}
|
||||
|
||||
fn run_overview(args: LegacyArgs) -> Result<Value> {
|
||||
let workspace = args
|
||||
.workspace
|
||||
.ok_or_else(|| anyhow!("overview requires --workspace"))?;
|
||||
Ok(json!({
|
||||
"schemaVersion": "epiphany.repo_work_historical_overview.v0",
|
||||
"status": "historical-only", "reason": HISTORICAL_REASON,
|
||||
"workspace": workspace, "item": args.item,
|
||||
"authority": { "durableStateAdmitted": false, "readinessApprovalAuthorized": false,
|
||||
"publicationAuthorized": false, "serviceLifecycleAuthority": false, "handsActionAuthorized": false },
|
||||
"privateStateExposed": false
|
||||
}))
|
||||
}
|
||||
|
||||
fn print_usage() {
|
||||
eprintln!(
|
||||
"usage: epiphany-work <legacy-command> [options]\nLegacy mutations are historical-only.\nRead history: epiphany-work overview --workspace <path> [--item <id>]"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn mutation_refusal_and_history_are_explicit() -> Result<()> {
|
||||
assert!(
|
||||
canonical_frontier_required("plan")
|
||||
.to_string()
|
||||
.contains(HISTORICAL_REASON)
|
||||
);
|
||||
let row = run_overview(LegacyArgs {
|
||||
workspace: Some(PathBuf::from("E:/test")),
|
||||
item: None,
|
||||
})?;
|
||||
assert_eq!(row["status"], "historical-only");
|
||||
assert_eq!(row["authority"]["handsActionAuthorized"], false);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,522 +0,0 @@
|
|||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use epiphany_core::{
|
||||
ProcessInstanceObservation, RuntimeSpineInitOptions,
|
||||
WorkspaceCoverageManagedProcessLaunchEntry, authenticate_workspace_coverage_recovery_receipt,
|
||||
bind_repository_body, bind_runtime_to_agent_memory_swarm,
|
||||
current_workspace_coverage_recovery_target, ensure_agent_memory_swarm_identity,
|
||||
initialize_runtime_spine, load_latest_workspace_coverage_managed_process_launch,
|
||||
observe_process_instance, observe_repository_body,
|
||||
process_identity_from_workspace_coverage_launch, seed_epiphany_local_verse_context,
|
||||
workspace_coverage_execution_collection,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::BTreeSet;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use uuid::Uuid;
|
||||
|
||||
const RUNTIME_ID: &str = "workspace-coverage-recovery-smoke";
|
||||
const SERVICE_ID: &str = "epiphany-workspace-coverage-projector-service";
|
||||
const SMOKE_SOURCE: &[u8] = include_bytes!("epiphany-workspace-coverage-recovery-smoke.rs");
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let smoke_id = Uuid::new_v4().to_string();
|
||||
let root = std::env::temp_dir().join(format!("epiphany-workspace-coverage-{smoke_id}"));
|
||||
fs::create_dir_all(&root)?;
|
||||
let repo = root.join("repo");
|
||||
let runtime = root.join("runtime.ccmp");
|
||||
let agents = root.join("agents.ccmp");
|
||||
let body = root.join("body.ccmp");
|
||||
let verse = root.join("verse.ccmp");
|
||||
let proof_path = root.join("proof.json");
|
||||
let qdrant_url = std::env::var("EPIPHANY_SMOKE_QDRANT_URL")
|
||||
.unwrap_or_else(|_| "http://127.0.0.1:6333".into());
|
||||
let ollama_url = std::env::var("EPIPHANY_SMOKE_OLLAMA_URL")
|
||||
.unwrap_or_else(|_| "http://127.0.0.1:11434".into());
|
||||
let model = std::env::var("EPIPHANY_SMOKE_OLLAMA_MODEL")
|
||||
.unwrap_or_else(|_| "qwen3-embedding:0.6b".into());
|
||||
require_http_ok(&format!("{qdrant_url}/collections"))?;
|
||||
let mut qdrant_cleanup = QdrantCollectionCleanup {
|
||||
base_url: qdrant_url.clone(),
|
||||
owned: BTreeSet::new(),
|
||||
};
|
||||
require_http_ok(&format!("{ollama_url}/api/tags"))?;
|
||||
prewarm_ollama(&ollama_url, &model)?;
|
||||
eprintln!("smoke: endpoints ready");
|
||||
create_fixture_repo(&repo)?;
|
||||
initialize_runtime_spine(
|
||||
&runtime,
|
||||
RuntimeSpineInitOptions {
|
||||
runtime_id: RUNTIME_ID.into(),
|
||||
display_name: "workspace coverage recovery smoke".into(),
|
||||
created_at: chrono::Utc::now().to_rfc3339(),
|
||||
},
|
||||
)?;
|
||||
ensure_agent_memory_swarm_identity(&agents, &format!("smoke-{smoke_id}"))?;
|
||||
bind_runtime_to_agent_memory_swarm(&runtime, &agents, &chrono::Utc::now().to_rfc3339())?;
|
||||
bind_repository_body(&repo, &body, &runtime, &format!("smoke-{smoke_id}"))?;
|
||||
observe_repository_body(&repo, &body, &runtime)?;
|
||||
seed_epiphany_local_verse_context(
|
||||
&verse,
|
||||
RUNTIME_ID,
|
||||
chrono::Utc::now().to_rfc3339(),
|
||||
"repo:C:/fixture/Epiphany",
|
||||
)?;
|
||||
let _process_cleanup = ManagedProcessCleanup {
|
||||
verse: verse.clone(),
|
||||
};
|
||||
|
||||
let supervisor = sibling_binary("epiphany-daemon-supervisor")?;
|
||||
run_supervisor(
|
||||
&supervisor,
|
||||
&[
|
||||
"workspace-coverage-projector-service-policy",
|
||||
"--store",
|
||||
path(&verse)?,
|
||||
"--runtime-id",
|
||||
RUNTIME_ID,
|
||||
"--daemon-id",
|
||||
"epiphany-daemon-supervisor",
|
||||
"--runtime-store",
|
||||
path(&runtime)?,
|
||||
"--qdrant-url",
|
||||
&qdrant_url,
|
||||
"--ollama-base-url",
|
||||
&ollama_url,
|
||||
"--ollama-model",
|
||||
&model,
|
||||
"--loop-interval-seconds",
|
||||
"30",
|
||||
"--stdout-artifact",
|
||||
path(&root.join("projector.stdout.log"))?,
|
||||
"--stderr-artifact",
|
||||
path(&root.join("projector.stderr.log"))?,
|
||||
],
|
||||
)?;
|
||||
eprintln!("smoke: policy persisted");
|
||||
run_supervisor(&supervisor, reconcile_args(&verse, &runtime)?)?;
|
||||
eprintln!("smoke: initial launch persisted");
|
||||
|
||||
let first = wait_for_claim(&runtime, 1, Duration::from_secs(90))?;
|
||||
qdrant_cleanup
|
||||
.owned
|
||||
.insert(workspace_coverage_execution_collection(
|
||||
&first.plan_id,
|
||||
&first.claim_id,
|
||||
first.claim_epoch,
|
||||
)?);
|
||||
eprintln!(
|
||||
"smoke: initial claim acquired at epoch {}",
|
||||
first.claim_epoch
|
||||
);
|
||||
let first_launch = load_latest_workspace_coverage_managed_process_launch(&verse, RUNTIME_ID)?
|
||||
.context("initial managed launch disappeared")?;
|
||||
if first.managed_process_launch_id != first_launch.launch_id {
|
||||
bail!("initial claim disagrees with latest managed launch");
|
||||
}
|
||||
kill_process(&first_launch)?;
|
||||
wait_for_process_exit(&first_launch, Duration::from_secs(20))?;
|
||||
eprintln!("smoke: initial process killed");
|
||||
|
||||
let recovery_output = run_supervisor(&supervisor, reconcile_args(&verse, &runtime)?)?;
|
||||
let recovery_json: Value = serde_json::from_str(&recovery_output)
|
||||
.context("supervisor recovery output was not JSON")?;
|
||||
if recovery_json.get("status").and_then(Value::as_str) != Some("recovered") {
|
||||
bail!("supervisor did not recover workspace coverage: {recovery_output}");
|
||||
}
|
||||
let recovery_id = required_json_string(&recovery_json, "recoveryReceiptId")?;
|
||||
let recovery_digest = required_json_string(&recovery_json, "recoveryReceiptDigest")?;
|
||||
eprintln!("smoke: supervisor recovery returned");
|
||||
let second = wait_for_claim(&runtime, 2, Duration::from_secs(20))?;
|
||||
qdrant_cleanup
|
||||
.owned
|
||||
.insert(workspace_coverage_execution_collection(
|
||||
&second.plan_id,
|
||||
&second.claim_id,
|
||||
second.claim_epoch,
|
||||
)?);
|
||||
if second.claim_id == first.claim_id
|
||||
|| second.claim_epoch != first.claim_epoch + 1
|
||||
|| second.managed_process_launch_id == first.managed_process_launch_id
|
||||
{
|
||||
bail!("recovered Body authority did not advance exactly once");
|
||||
}
|
||||
let replacement = load_latest_workspace_coverage_managed_process_launch(&verse, RUNTIME_ID)?
|
||||
.context("replacement managed launch disappeared")?;
|
||||
authenticate_workspace_coverage_recovery_receipt(
|
||||
&body,
|
||||
&verse,
|
||||
RUNTIME_ID,
|
||||
epiphany_core::open_default_host_identity()?.entry(),
|
||||
recovery_id,
|
||||
recovery_digest,
|
||||
)?;
|
||||
kill_process(&replacement)?;
|
||||
qdrant_cleanup.cleanup_and_verify()?;
|
||||
|
||||
let executable_sha256 = hash_file(&std::env::current_exe()?)?;
|
||||
let smoke_source_sha256 = hash_bytes(SMOKE_SOURCE);
|
||||
let source_head = git_output(&["rev-parse", "HEAD"])?;
|
||||
let source_diff_sha256 = hash_bytes(&git_bytes(&["diff", "--binary", "HEAD"])?);
|
||||
|
||||
let proof = json!({
|
||||
"schemaVersion": "epiphany.workspace_coverage_recovery_smoke.v0",
|
||||
"smokeId": smoke_id,
|
||||
"status": "passed",
|
||||
"runtimeId": RUNTIME_ID,
|
||||
"initialClaimId": first.claim_id,
|
||||
"initialClaimEpoch": first.claim_epoch,
|
||||
"initialLaunchId": first.managed_process_launch_id,
|
||||
"recoveredClaimId": second.claim_id,
|
||||
"recoveredClaimEpoch": second.claim_epoch,
|
||||
"replacementLaunchId": second.managed_process_launch_id,
|
||||
"recoveryReceiptId": recovery_id,
|
||||
"recoveryReceiptDigest": recovery_digest,
|
||||
"executableSha256": executable_sha256,
|
||||
"smokeSourceSha256": smoke_source_sha256,
|
||||
"sourceGitHead": source_head,
|
||||
"sourceDiffSha256": source_diff_sha256,
|
||||
"ownedCollectionsRemoved": true,
|
||||
"bodyStore": body,
|
||||
"verseStore": verse,
|
||||
"privateStateExposed": false
|
||||
});
|
||||
fs::write(&proof_path, serde_json::to_vec_pretty(&proof)?)?;
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"status": "passed",
|
||||
"proof": proof_path,
|
||||
"recoveryReceiptId": recovery_id,
|
||||
"initialEpoch": first.claim_epoch,
|
||||
"recoveredEpoch": second.claim_epoch,
|
||||
"privateStateExposed": false
|
||||
}))?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_fixture_repo(repo: &Path) -> Result<()> {
|
||||
fs::create_dir_all(repo)?;
|
||||
run(Command::new("git")
|
||||
.arg("init")
|
||||
.arg("-b")
|
||||
.arg("main")
|
||||
.current_dir(repo))?;
|
||||
for file_index in 0..160 {
|
||||
let mut text = String::new();
|
||||
for line in 0..120 {
|
||||
text.push_str(&format!(
|
||||
"pub fn smoke_{file_index}_{line}() -> usize {{ {} }}\n",
|
||||
file_index + line
|
||||
));
|
||||
}
|
||||
fs::write(repo.join(format!("body_{file_index:03}.rs")), text)?;
|
||||
}
|
||||
run(Command::new("git").arg("add").arg(".").current_dir(repo))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reconcile_args(verse: &Path, runtime: &Path) -> Result<Vec<String>> {
|
||||
Ok(vec![
|
||||
"managed-service-reconcile".into(),
|
||||
"--store".into(),
|
||||
path(verse)?.into(),
|
||||
"--runtime-id".into(),
|
||||
RUNTIME_ID.into(),
|
||||
"--daemon-id".into(),
|
||||
"epiphany-daemon-supervisor".into(),
|
||||
"--service-id".into(),
|
||||
SERVICE_ID.into(),
|
||||
"--runtime-store".into(),
|
||||
path(runtime)?.into(),
|
||||
"--force".into(),
|
||||
])
|
||||
}
|
||||
|
||||
fn wait_for_claim(
|
||||
runtime: &Path,
|
||||
minimum_epoch: u64,
|
||||
timeout: Duration,
|
||||
) -> Result<epiphany_core::WorkspaceCoverageRecoveryTarget> {
|
||||
let deadline = Instant::now() + timeout;
|
||||
while Instant::now() < deadline {
|
||||
if let Some(target) = current_workspace_coverage_recovery_target(runtime)?
|
||||
&& target.claim_epoch >= minimum_epoch
|
||||
{
|
||||
return Ok(target);
|
||||
}
|
||||
thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
Err(anyhow!(
|
||||
"workspace coverage claim epoch {minimum_epoch} did not appear"
|
||||
))
|
||||
}
|
||||
|
||||
fn run_supervisor<I, S>(supervisor: &Path, args: I) -> Result<String>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
let command_id = Uuid::new_v4();
|
||||
let stdout_path = std::env::temp_dir().join(format!("epiphany-smoke-{command_id}.stdout"));
|
||||
let stderr_path = std::env::temp_dir().join(format!("epiphany-smoke-{command_id}.stderr"));
|
||||
let stdout = fs::File::create(&stdout_path)?;
|
||||
let stderr = fs::File::create(&stderr_path)?;
|
||||
let status = Command::new(supervisor)
|
||||
.args(args)
|
||||
.env("EPIPHANY_WORKSPACE_COVERAGE_SMOKE_DIAGNOSTICS", "1")
|
||||
.env("EPIPHANY_OLLAMA_TIMEOUT_MS", "120000")
|
||||
.stdout(Stdio::from(stdout))
|
||||
.stderr(Stdio::from(stderr))
|
||||
.status()?;
|
||||
let stdout = fs::read_to_string(&stdout_path)?;
|
||||
let stderr = fs::read_to_string(&stderr_path)?;
|
||||
let _ = fs::remove_file(stdout_path);
|
||||
let _ = fs::remove_file(stderr_path);
|
||||
if !status.success() {
|
||||
bail!("supervisor failed: {stderr}");
|
||||
}
|
||||
Ok(stdout)
|
||||
}
|
||||
|
||||
fn run(command: &mut Command) -> Result<()> {
|
||||
let output = command.output()?;
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
"command failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sibling_binary(name: &str) -> Result<PathBuf> {
|
||||
let file = if cfg!(windows) {
|
||||
format!("{name}.exe")
|
||||
} else {
|
||||
name.into()
|
||||
};
|
||||
let path = std::env::current_exe()?.with_file_name(file);
|
||||
if !path.is_file() {
|
||||
bail!("required sibling binary is absent: {}", path.display());
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn path(value: &Path) -> Result<&str> {
|
||||
value.to_str().context("smoke path is not UTF-8")
|
||||
}
|
||||
|
||||
fn required_json_string<'a>(value: &'a Value, key: &str) -> Result<&'a str> {
|
||||
value
|
||||
.get(key)
|
||||
.and_then(Value::as_str)
|
||||
.with_context(|| format!("recovery output lacks {key}"))
|
||||
}
|
||||
|
||||
fn require_http_ok(url: &str) -> Result<()> {
|
||||
let response = reqwest::blocking::get(url)?;
|
||||
if !response.status().is_success() {
|
||||
bail!("required endpoint is unavailable: {url}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prewarm_ollama(base_url: &str, model: &str) -> Result<()> {
|
||||
let response = reqwest::blocking::Client::builder()
|
||||
.timeout(Duration::from_secs(120))
|
||||
.build()?
|
||||
.post(format!("{base_url}/api/embed"))
|
||||
.json(&json!({"model": model, "input": ["epiphany live smoke warmup"]}))
|
||||
.send()?;
|
||||
if !response.status().is_success() {
|
||||
bail!("Ollama smoke warmup failed with {}", response.status());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct ManagedProcessCleanup {
|
||||
verse: PathBuf,
|
||||
}
|
||||
|
||||
struct QdrantCollectionCleanup {
|
||||
base_url: String,
|
||||
owned: BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl QdrantCollectionCleanup {
|
||||
fn cleanup_and_verify(&mut self) -> Result<()> {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let names = self.owned.iter().cloned().collect::<Vec<_>>();
|
||||
for name in names {
|
||||
let response = client
|
||||
.delete(format!("{}/collections/{name}", self.base_url))
|
||||
.send()?;
|
||||
if !response.status().is_success()
|
||||
&& response.status() != reqwest::StatusCode::NOT_FOUND
|
||||
{
|
||||
bail!("Qdrant refused fixture collection deletion for {name}");
|
||||
}
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
loop {
|
||||
let status = client
|
||||
.get(format!("{}/collections/{name}", self.base_url))
|
||||
.send()?
|
||||
.status();
|
||||
if status == reqwest::StatusCode::NOT_FOUND {
|
||||
self.owned.remove(&name);
|
||||
break;
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
bail!("fixture collection {name} remained after deletion");
|
||||
}
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for QdrantCollectionCleanup {
|
||||
fn drop(&mut self) {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
for name in &self.owned {
|
||||
let _ = client
|
||||
.delete(format!("{}/collections/{name}", self.base_url))
|
||||
.send();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ManagedProcessCleanup {
|
||||
fn drop(&mut self) {
|
||||
if let Ok(Some(launch)) =
|
||||
load_latest_workspace_coverage_managed_process_launch(&self.verse, RUNTIME_ID)
|
||||
{
|
||||
let _ = kill_process(&launch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn kill_native_process(pid: u32) -> Result<()> {
|
||||
let status = Command::new("taskkill")
|
||||
.args(["/PID", &pid.to_string(), "/T", "/F"])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()?;
|
||||
if !status.success() {
|
||||
bail!("taskkill failed for PID {pid}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn kill_native_process(pid: u32) -> Result<()> {
|
||||
let status = Command::new("kill")
|
||||
.args(["-KILL", &pid.to_string()])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()?;
|
||||
if !status.success() {
|
||||
bail!("kill failed for PID {pid}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn kill_process(launch: &WorkspaceCoverageManagedProcessLaunchEntry) -> Result<()> {
|
||||
let expected = process_identity_from_workspace_coverage_launch(launch);
|
||||
match observe_process_instance(&expected) {
|
||||
ProcessInstanceObservation::ExactAlive => kill_native_process(expected.process_id),
|
||||
ProcessInstanceObservation::ExactExited { .. } | ProcessInstanceObservation::Missing => {
|
||||
bail!("managed launch {} already exited", launch.launch_id)
|
||||
}
|
||||
ProcessInstanceObservation::Replaced { .. } => {
|
||||
bail!(
|
||||
"managed launch {} PID has been reused; refusing kill",
|
||||
launch.launch_id
|
||||
)
|
||||
}
|
||||
ProcessInstanceObservation::Inaccessible => {
|
||||
bail!(
|
||||
"managed launch {} is inaccessible; refusing kill",
|
||||
launch.launch_id
|
||||
)
|
||||
}
|
||||
ProcessInstanceObservation::Indeterminate { reason } => {
|
||||
bail!(
|
||||
"managed launch {} is indeterminate: {reason}",
|
||||
launch.launch_id
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn wait_for_process_exit(
|
||||
launch: &WorkspaceCoverageManagedProcessLaunchEntry,
|
||||
timeout: Duration,
|
||||
) -> Result<()> {
|
||||
let expected = process_identity_from_workspace_coverage_launch(launch);
|
||||
let deadline = Instant::now() + timeout;
|
||||
while Instant::now() < deadline {
|
||||
match observe_process_instance(&expected) {
|
||||
ProcessInstanceObservation::ExactAlive => {}
|
||||
ProcessInstanceObservation::ExactExited { .. }
|
||||
| ProcessInstanceObservation::Missing => return Ok(()),
|
||||
ProcessInstanceObservation::Replaced { .. } => {
|
||||
bail!(
|
||||
"managed launch {} PID was reused while awaiting exit",
|
||||
launch.launch_id
|
||||
)
|
||||
}
|
||||
ProcessInstanceObservation::Inaccessible => {
|
||||
bail!("managed launch {} became inaccessible", launch.launch_id)
|
||||
}
|
||||
ProcessInstanceObservation::Indeterminate { reason } => {
|
||||
bail!(
|
||||
"managed launch {} became indeterminate: {reason}",
|
||||
launch.launch_id
|
||||
)
|
||||
}
|
||||
}
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
bail!("managed launch {} did not exit", launch.launch_id)
|
||||
}
|
||||
|
||||
fn hash_file(path: &Path) -> Result<String> {
|
||||
Ok(hash_bytes(&fs::read(path)?))
|
||||
}
|
||||
|
||||
fn hash_bytes(bytes: &[u8]) -> String {
|
||||
format!("sha256-{:x}", Sha256::digest(bytes))
|
||||
}
|
||||
|
||||
fn git_bytes(args: &[&str]) -> Result<Vec<u8>> {
|
||||
let repo = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.context("epiphany-core has no repository parent")?;
|
||||
let output = Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(repo)
|
||||
.args(args)
|
||||
.output()?;
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
"git provenance command failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
Ok(output.stdout)
|
||||
}
|
||||
|
||||
fn git_output(args: &[&str]) -> Result<String> {
|
||||
Ok(String::from_utf8(git_bytes(args)?)?.trim().to_string())
|
||||
}
|
||||
|
|
@ -41,7 +41,6 @@ mod resident_readiness;
|
|||
mod resident_self;
|
||||
mod runtime_spine;
|
||||
mod runtime_store_backend;
|
||||
mod runtime_store_migration;
|
||||
mod runtime_worker_attempt;
|
||||
mod semantic_backend;
|
||||
mod semantic_projector_service;
|
||||
|
|
@ -733,8 +732,6 @@ pub use memory_graph::SemanticLifecycle;
|
|||
pub use memory_graph::SemanticPartition;
|
||||
pub use memory_graph::SemanticProjectionCandidate;
|
||||
pub use memory_graph::SemanticProjectionDocument;
|
||||
#[cfg(feature = "semantic-recovery-smoke")]
|
||||
pub use memory_graph::SemanticRecoverySmokeInspection;
|
||||
pub use memory_graph::SemanticVectorIndex;
|
||||
pub use memory_graph::SemanticVisibility;
|
||||
pub use memory_graph::authorize_memory_semantic_physical_retirements;
|
||||
|
|
@ -745,8 +742,6 @@ pub use memory_graph::derive_memory_semantic_projection_health;
|
|||
pub use memory_graph::derive_memory_semantic_projection_obligation;
|
||||
pub use memory_graph::derive_semantic_projection;
|
||||
pub use memory_graph::execute_memory_semantic_physical_retirement;
|
||||
#[cfg(feature = "semantic-recovery-smoke")]
|
||||
pub use memory_graph::inspect_memory_semantic_recovery_for_smoke;
|
||||
pub use memory_graph::lifecycle_allowed_for_profile;
|
||||
pub use memory_graph::load_memory_semantic_projection_readiness;
|
||||
pub use memory_graph::load_memory_semantic_projection_success;
|
||||
|
|
@ -1051,10 +1046,6 @@ pub use runtime_spine::terminalize_model_pass_failure_session;
|
|||
pub use runtime_spine::validate_hands_action_authority;
|
||||
pub use runtime_spine::write_runtime_hello_frame;
|
||||
pub use runtime_spine::write_runtime_schema_catalog_json;
|
||||
pub use runtime_store_migration::{
|
||||
RUNTIME_STORE_MIGRATION_RECEIPT_SCHEMA_VERSION, RUNTIME_STORE_MIGRATION_RECEIPT_TYPE,
|
||||
RuntimeStoreMigrationReceipt, migrate_runtime_store_to_redb,
|
||||
};
|
||||
pub use runtime_worker_attempt::{RuntimeTypedRequestRef, WorkerProcessStatus};
|
||||
pub use semantic_projector_service::SemanticProjectorServiceBody;
|
||||
pub use semantic_projector_service::SemanticProjectorServicePulse;
|
||||
|
|
|
|||
|
|
@ -1453,101 +1453,6 @@ pub fn observe_memory_semantic_projection(
|
|||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "semantic-recovery-smoke")]
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SemanticRecoverySmokeInspection {
|
||||
pub claim_id: String,
|
||||
pub claim_epoch: u64,
|
||||
pub claim_status: String,
|
||||
pub executor_id: String,
|
||||
pub executor_incarnation: String,
|
||||
pub attempt_status: String,
|
||||
pub attempt_completed: bool,
|
||||
pub recovery_authorization_id: Option<String>,
|
||||
pub recovery_authorization_status: Option<String>,
|
||||
pub recovery_authorization_consumed: bool,
|
||||
pub abandoned_attempt_failed: bool,
|
||||
pub old_owner_authenticates_current_claim: bool,
|
||||
}
|
||||
|
||||
/// Read-only, feature-sealed witness for the native recovery smoke. It decodes
|
||||
/// and validates the same typed authority documents used by production; it
|
||||
/// cannot acquire, recover, complete, fail, or otherwise mutate a claim.
|
||||
#[cfg(feature = "semantic-recovery-smoke")]
|
||||
pub fn inspect_memory_semantic_recovery_for_smoke(
|
||||
store_path: impl AsRef<Path>,
|
||||
input: &MemorySemanticProjectionInput,
|
||||
old_executor_id: &str,
|
||||
old_executor_incarnation: &str,
|
||||
) -> Result<SemanticRecoverySmokeInspection> {
|
||||
let store_path = store_path.as_ref();
|
||||
observe_memory_semantic_projection(store_path, input)?;
|
||||
let envelopes = SingleFileMessagePackBackingStore::new(store_path).pull_all()?;
|
||||
let scope_id = projection_scope_id(&input.obligation.swarm_id, &input.obligation.partition)?;
|
||||
let claim = decode_one::<MemorySemanticProjectionClaim>(&envelopes, &scope_id)?
|
||||
.ok_or_else(|| anyhow!("semantic recovery smoke found no current claim"))?;
|
||||
validate_memory_semantic_projection_claim(&claim)?;
|
||||
let attempts = decode_all::<MemorySemanticProjectionAttempt>(&envelopes)?;
|
||||
for attempt in &attempts {
|
||||
validate_memory_semantic_projection_attempt(attempt)?;
|
||||
}
|
||||
let attempt = attempts
|
||||
.iter()
|
||||
.find(|attempt| attempt.attempt_id == claim.attempt_id)
|
||||
.ok_or_else(|| anyhow!("semantic recovery smoke current claim lost its attempt"))?;
|
||||
let authorizations = decode_all::<MemorySemanticProjectorRecoveryAuthorization>(&envelopes)?;
|
||||
for authorization in &authorizations {
|
||||
validate_memory_semantic_projector_recovery_authorization(authorization)?;
|
||||
}
|
||||
let authorization = authorizations
|
||||
.iter()
|
||||
.find(|authorization| authorization.resulting_claim_id.as_deref() == Some(&claim.claim_id));
|
||||
let abandoned_attempt_failed = authorization.is_some_and(|authorization| {
|
||||
attempts.iter().any(|attempt| {
|
||||
attempt.attempt_id == authorization.attempt_id
|
||||
&& attempt.claim_id == authorization.claim_id
|
||||
&& attempt.claim_epoch == authorization.claim_epoch
|
||||
&& attempt.status == "failed"
|
||||
&& attempt.completed_at.is_some()
|
||||
})
|
||||
});
|
||||
let old_owner_authenticates_current_claim = owned_running_memory_semantic_projection_claim(
|
||||
store_path,
|
||||
input,
|
||||
old_executor_id,
|
||||
old_executor_incarnation,
|
||||
)?
|
||||
.is_some();
|
||||
Ok(SemanticRecoverySmokeInspection {
|
||||
claim_id: claim.claim_id,
|
||||
claim_epoch: claim.epoch,
|
||||
claim_status: claim.status,
|
||||
executor_id: claim.executor_id,
|
||||
executor_incarnation: claim.executor_incarnation,
|
||||
attempt_status: attempt.status.clone(),
|
||||
attempt_completed: attempt.completed_at.is_some(),
|
||||
recovery_authorization_id: authorization.map(|row| row.authorization_id.clone()),
|
||||
recovery_authorization_status: authorization.map(|row| row.status.clone()),
|
||||
recovery_authorization_consumed: authorization
|
||||
.is_some_and(|row| row.status == "consumed" && row.consumed_at.is_some()),
|
||||
abandoned_attempt_failed,
|
||||
old_owner_authenticates_current_claim,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn semantic_recovery_smoke_exposes_no_raw_mutation_authority() {
|
||||
let source = include_str!("semantic_projector.rs");
|
||||
assert!(source.contains("pub(crate) fn idunn_acquire_memory_semantic_projection("));
|
||||
let public_acquire = ["pub fn idunn_", "acquire_memory_semantic_projection("].concat();
|
||||
let fixture_seed = ["seed_abandoned_memory_semantic_projection", "_for_smoke"].concat();
|
||||
assert!(!source.contains(&public_acquire));
|
||||
assert!(!source.contains(&fixture_seed));
|
||||
assert!(source.contains("pub fn inspect_memory_semantic_recovery_for_smoke("));
|
||||
}
|
||||
|
||||
pub(crate) fn projection_scope_id(swarm_id: &str, partition: &str) -> Result<String> {
|
||||
if swarm_id.trim().is_empty() || !matches!(partition, "mind" | "modeling") {
|
||||
return Err(anyhow!("semantic projection scope identity is invalid"));
|
||||
|
|
|
|||
|
|
@ -1087,7 +1087,6 @@ pub fn runtime_spine_cache(store_path: impl AsRef<Path>) -> Result<CultCache> {
|
|||
crate::mind_documents::register_mind_document_types(&mut cache)?;
|
||||
cache.register_entry_type::<crate::UserObjectiveIntake>()?;
|
||||
cache.register_entry_type::<EpiphanyRuntimeIdentity>()?;
|
||||
cache.register_entry_type::<crate::RuntimeStoreMigrationReceipt>()?;
|
||||
cache.register_entry_type::<EpiphanyRuntimeSwarmBinding>()?;
|
||||
cache.register_entry_type::<crate::AtlasSurfaceOfferWriteIntent>()?;
|
||||
cache.register_entry_type::<crate::AtlasDependencyClaimWriteIntent>()?;
|
||||
|
|
|
|||
|
|
@ -1,178 +0,0 @@
|
|||
use anyhow::{Result, anyhow};
|
||||
use cultcache_rs::{
|
||||
CacheBackingStore, CultCacheEnvelope, DatabaseEntry, RedbMessagePackBackingStore,
|
||||
SingleFileMessagePackBackingStore,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::BTreeSet;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub const RUNTIME_STORE_MIGRATION_RECEIPT_TYPE: &str = "epiphany.runtime_store_migration_receipt";
|
||||
pub const RUNTIME_STORE_MIGRATION_RECEIPT_SCHEMA_VERSION: &str =
|
||||
"epiphany.runtime_store_migration_receipt.v1";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeStoreMigrationReceipt {
|
||||
pub schema_version: String,
|
||||
pub migration_id: String,
|
||||
pub source_path: String,
|
||||
pub source_sha256: String,
|
||||
pub source_entry_count: usize,
|
||||
pub source_envelope_set_sha256: String,
|
||||
pub destination_path: String,
|
||||
pub migrated_at: String,
|
||||
}
|
||||
|
||||
impl DatabaseEntry for RuntimeStoreMigrationReceipt {
|
||||
const TYPE: &'static str = RUNTIME_STORE_MIGRATION_RECEIPT_TYPE;
|
||||
const SCHEMA_NAME: &'static str = "RuntimeStoreMigrationReceipt";
|
||||
}
|
||||
|
||||
fn sha256(bytes: &[u8]) -> String {
|
||||
format!("{:x}", Sha256::digest(bytes))
|
||||
}
|
||||
|
||||
fn sorted_envelopes(entries: &[CultCacheEnvelope]) -> Vec<CultCacheEnvelope> {
|
||||
let mut sorted = entries.to_vec();
|
||||
sorted.sort_by(|left, right| (&left.r#type, &left.key).cmp(&(&right.r#type, &right.key)));
|
||||
sorted
|
||||
}
|
||||
|
||||
fn envelope_set_sha256(entries: &[CultCacheEnvelope]) -> Result<String> {
|
||||
Ok(sha256(&rmp_serde::to_vec_named(&sorted_envelopes(
|
||||
entries,
|
||||
))?))
|
||||
}
|
||||
|
||||
pub fn migrate_runtime_store_to_redb(
|
||||
source: impl AsRef<Path>,
|
||||
destination: impl AsRef<Path>,
|
||||
migrated_at: &str,
|
||||
) -> Result<RuntimeStoreMigrationReceipt> {
|
||||
chrono::DateTime::parse_from_rfc3339(migrated_at)
|
||||
.map_err(|_| anyhow!("migration timestamp must be RFC3339"))?;
|
||||
let source = source.as_ref();
|
||||
let destination = destination.as_ref();
|
||||
if source.extension().and_then(|value| value.to_str()) != Some("cc") {
|
||||
return Err(anyhow!("migration source must be a legacy .cc snapshot"));
|
||||
}
|
||||
if destination.extension().and_then(|value| value.to_str()) != Some("redb") {
|
||||
return Err(anyhow!("migration destination must use .redb"));
|
||||
}
|
||||
if destination.exists() {
|
||||
return Err(anyhow!("migration destination already exists"));
|
||||
}
|
||||
|
||||
let source_bytes = fs::read(source)?;
|
||||
let source_sha256 = sha256(&source_bytes);
|
||||
let source_store = SingleFileMessagePackBackingStore::new(source);
|
||||
let entries = source_store.pull_all()?;
|
||||
let identities = entries
|
||||
.iter()
|
||||
.map(|entry| (&entry.r#type, &entry.key))
|
||||
.collect::<BTreeSet<_>>();
|
||||
if identities.len() != entries.len() {
|
||||
return Err(anyhow!(
|
||||
"legacy runtime snapshot contains duplicate typed identities"
|
||||
));
|
||||
}
|
||||
if entries
|
||||
.iter()
|
||||
.any(|entry| entry.r#type == RUNTIME_STORE_MIGRATION_RECEIPT_TYPE)
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"legacy runtime snapshot already contains a migration receipt"
|
||||
));
|
||||
}
|
||||
|
||||
let set_sha256 = envelope_set_sha256(&entries)?;
|
||||
let migration_id = format!("runtime-store-migration-{source_sha256}");
|
||||
let receipt = RuntimeStoreMigrationReceipt {
|
||||
schema_version: RUNTIME_STORE_MIGRATION_RECEIPT_SCHEMA_VERSION.to_string(),
|
||||
migration_id: migration_id.clone(),
|
||||
source_path: source.canonicalize()?.display().to_string(),
|
||||
source_sha256: source_sha256.clone(),
|
||||
source_entry_count: entries.len(),
|
||||
source_envelope_set_sha256: set_sha256.clone(),
|
||||
destination_path: PathBuf::from(destination).display().to_string(),
|
||||
migrated_at: migrated_at.to_string(),
|
||||
};
|
||||
let receipt_envelope = CultCacheEnvelope {
|
||||
key: migration_id,
|
||||
r#type: RUNTIME_STORE_MIGRATION_RECEIPT_TYPE.to_string(),
|
||||
payload: rmp_serde::to_vec_named(&receipt)?,
|
||||
stored_at: migrated_at.to_string(),
|
||||
schema_id: Some(RUNTIME_STORE_MIGRATION_RECEIPT_SCHEMA_VERSION.to_string()),
|
||||
};
|
||||
let destination_store = RedbMessagePackBackingStore::new(destination)?;
|
||||
let mut writes = entries.clone();
|
||||
writes.push(receipt_envelope.clone());
|
||||
if !destination_store.compare_and_swap_batch(&[], writes)? {
|
||||
return Err(anyhow!(
|
||||
"keyed runtime migration lost its empty-destination CAS"
|
||||
));
|
||||
}
|
||||
let mut migrated = destination_store.pull_all()?;
|
||||
let persisted_receipt = migrated
|
||||
.iter()
|
||||
.find(|entry| entry.r#type == RUNTIME_STORE_MIGRATION_RECEIPT_TYPE)
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow!("keyed runtime migration lost its receipt"))?;
|
||||
if persisted_receipt != receipt_envelope {
|
||||
return Err(anyhow!(
|
||||
"keyed runtime migration receipt changed during persistence"
|
||||
));
|
||||
}
|
||||
migrated.retain(|entry| entry.r#type != RUNTIME_STORE_MIGRATION_RECEIPT_TYPE);
|
||||
if sorted_envelopes(&migrated) != sorted_envelopes(&entries)
|
||||
|| envelope_set_sha256(&migrated)? != set_sha256
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"keyed runtime migration failed exact envelope equivalence"
|
||||
));
|
||||
}
|
||||
if sha256(&fs::read(source)?) != source_sha256 {
|
||||
return Err(anyhow!("legacy runtime snapshot changed during migration"));
|
||||
}
|
||||
Ok(receipt)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn migration_is_exact_seals_source_and_refuses_destination_reuse() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let source = temp.path().join("runtime.cc");
|
||||
let destination = temp.path().join("runtime.redb");
|
||||
let entry = CultCacheEnvelope {
|
||||
key: "state".to_string(),
|
||||
r#type: "epiphany.test".to_string(),
|
||||
payload: vec![1, 2, 3],
|
||||
stored_at: "2026-08-08T00:00:00Z".to_string(),
|
||||
schema_id: Some("epiphany.test".to_string()),
|
||||
};
|
||||
let source_store = SingleFileMessagePackBackingStore::new(&source);
|
||||
assert!(source_store.compare_and_swap_batch(&[], vec![entry.clone()])?);
|
||||
let before = fs::read(&source)?;
|
||||
let receipt = migrate_runtime_store_to_redb(&source, &destination, "2026-08-08T01:00:00Z")?;
|
||||
assert_eq!(receipt.source_entry_count, 1);
|
||||
assert_eq!(fs::read(&source)?, before);
|
||||
assert!(
|
||||
migrate_runtime_store_to_redb(&source, &destination, "2026-08-08T02:00:00Z").is_err()
|
||||
);
|
||||
let rows = RedbMessagePackBackingStore::new(&destination)?.pull_all()?;
|
||||
assert!(rows.contains(&entry));
|
||||
assert_eq!(
|
||||
rows.iter()
|
||||
.filter(|row| row.r#type == RUNTIME_STORE_MIGRATION_RECEIPT_TYPE)
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -198,10 +198,6 @@ impl WorkspaceCoverageProjectorServiceBody {
|
|||
Ok(pulse) => pulse,
|
||||
Err(_error) => {
|
||||
eprintln!("workspace coverage pulse failed: {_error:#}");
|
||||
#[cfg(feature = "workspace-coverage-recovery-smoke")]
|
||||
if std::env::var_os("EPIPHANY_WORKSPACE_COVERAGE_SMOKE_DIAGNOSTICS").is_some() {
|
||||
eprintln!("workspace coverage smoke diagnostic: {_error:#}");
|
||||
}
|
||||
WorkspaceCoverageProjectorServicePulse {
|
||||
status: WorkspaceCoverageProjectorPulseStatus::Refused,
|
||||
body_observation_id: None,
|
||||
|
|
|
|||
|
|
@ -1,136 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use cultcache_rs::CultCache;
|
||||
use cultcache_rs::SingleFileMessagePackBackingStore;
|
||||
use cultnet_rs::CultNetDocumentBinding;
|
||||
use cultnet_rs::CultNetDocumentRegistry;
|
||||
use epiphany_core::EpiphanyAgentMemoryEntry;
|
||||
use epiphany_core::agent_memory_status;
|
||||
use epiphany_core::migrate_agent_memory_json_dir_to_cultcache;
|
||||
|
||||
#[test]
|
||||
fn epiphany_agent_memory_can_move_over_cultnet_documents() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let agent_dir = temp.path().join("agents");
|
||||
let source_store = temp.path().join("source-agents.msgpack");
|
||||
let target_store = temp.path().join("target-agents.msgpack");
|
||||
write_agent_fixture(&agent_dir)?;
|
||||
migrate_agent_memory_json_dir_to_cultcache(&agent_dir, &source_store)?;
|
||||
|
||||
let mut source_cache = CultCache::new();
|
||||
source_cache.register_entry_type::<EpiphanyAgentMemoryEntry>()?;
|
||||
source_cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&source_store));
|
||||
source_cache.pull_all_backing_stores()?;
|
||||
|
||||
let mut registry = CultNetDocumentRegistry::new();
|
||||
registry.register(
|
||||
CultNetDocumentBinding::for_entry::<EpiphanyAgentMemoryEntry>(Some(
|
||||
"ghostlight.agent_state.v0".to_string(),
|
||||
)),
|
||||
);
|
||||
let snapshot = registry.create_snapshot_response(
|
||||
&source_cache,
|
||||
"epiphany-agent-memory-snapshot",
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
|
||||
let mut target_cache = CultCache::new();
|
||||
target_cache.register_entry_type::<EpiphanyAgentMemoryEntry>()?;
|
||||
target_cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store));
|
||||
target_cache.pull_all_backing_stores()?;
|
||||
let applied = registry
|
||||
.apply_snapshot_response::<EpiphanyAgentMemoryEntry>(&mut target_cache, &snapshot)?;
|
||||
assert_eq!(applied.len(), 7);
|
||||
assert!(
|
||||
target_cache
|
||||
.get::<EpiphanyAgentMemoryEntry>("coordinator")?
|
||||
.is_some()
|
||||
);
|
||||
|
||||
let status = agent_memory_status(&target_store)?;
|
||||
assert_eq!(status["ok"], true);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_agent_fixture(agent_dir: &std::path::Path) -> Result<()> {
|
||||
std::fs::create_dir_all(agent_dir)?;
|
||||
for (role_id, agent_id, filename) in [
|
||||
(
|
||||
"imagination",
|
||||
"epiphany.imagination",
|
||||
"imagination.agent-state.json",
|
||||
),
|
||||
("modeling", "epiphany.modeling", "modeling.agent-state.json"),
|
||||
("verification", "epiphany.soul", "soul.agent-state.json"),
|
||||
("implementation", "epiphany.hands", "hands.agent-state.json"),
|
||||
("research", "epiphany.eyes", "eyes.agent-state.json"),
|
||||
("Persona", "epiphany.Persona", "Persona.agent-state.json"),
|
||||
("coordinator", "epiphany.self", "self.agent-state.json"),
|
||||
] {
|
||||
std::fs::write(
|
||||
agent_dir.join(filename),
|
||||
sample_agent_json(agent_id, &format!("Agent {role_id}")),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sample_agent_json(agent_id: &str, name: &str) -> String {
|
||||
serde_json::json!({
|
||||
"schema_version": "ghostlight.agent_state.v0",
|
||||
"world": {
|
||||
"world_id": "epiphany-agent-memory",
|
||||
"setting": "Epiphany local harness role memory",
|
||||
"time": {"label": "standing memory"},
|
||||
"canon_context": ["Organ-state records preserve lane identity."]
|
||||
},
|
||||
"agents": [{
|
||||
"agent_id": agent_id,
|
||||
"identity": {
|
||||
"name": name,
|
||||
"roles": ["test"],
|
||||
"origin": "test lane",
|
||||
"public_description": "Test role memory.",
|
||||
"private_notes": []
|
||||
},
|
||||
"canonical_state": {
|
||||
"underlying_organization": {"test": {"mean": 0.5, "plasticity": 0.5, "current_activation": 0.5}},
|
||||
"stable_dispositions": {"test": {"mean": 0.5, "plasticity": 0.5, "current_activation": 0.5}},
|
||||
"behavioral_dimensions": {"test": {"mean": 0.5, "plasticity": 0.5, "current_activation": 0.5}},
|
||||
"presentation_strategy": {"test": {"mean": 0.5, "plasticity": 0.5, "current_activation": 0.5}},
|
||||
"voice_style": {"test": {"mean": 0.5, "plasticity": 0.5, "current_activation": 0.5}},
|
||||
"situational_state": {"test": {"mean": 0.5, "plasticity": 0.5, "current_activation": 0.5}},
|
||||
"values": [{
|
||||
"value_id": "value-test",
|
||||
"label": "Test value.",
|
||||
"priority": 0.5,
|
||||
"unforgivable_if_betrayed": false
|
||||
}]
|
||||
},
|
||||
"goals": [{
|
||||
"goal_id": "goal-test",
|
||||
"description": "Keep the test role valid.",
|
||||
"scope": "life",
|
||||
"priority": 0.5,
|
||||
"emotional_stake": "Broken memory makes broken agents.",
|
||||
"blockers": [],
|
||||
"status": "active"
|
||||
}],
|
||||
"memories": {
|
||||
"episodic": [],
|
||||
"semantic": [{
|
||||
"memory_id": "mem-test",
|
||||
"summary": "Test memory.",
|
||||
"salience": 0.5,
|
||||
"confidence": 0.5
|
||||
}],
|
||||
"relationship_summaries": []
|
||||
},
|
||||
"perceived_state_overlays": []
|
||||
}],
|
||||
"relationships": [],
|
||||
"events": [],
|
||||
"scenes": []
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
|
|
@ -103,7 +103,6 @@ pub fn required_packaged_release_binaries(target_triple: &str) -> Vec<(&'static
|
|||
file_name("epiphany-atlas-impact-ingress"),
|
||||
),
|
||||
("semantic-query", file_name("epiphany-memory-semantic")),
|
||||
("verse-query", file_name("epiphany-verse-query")),
|
||||
("repository-body", file_name("epiphany-repository-body")),
|
||||
("host-identity", file_name("epiphany-host-identity")),
|
||||
("swarm", file_name("epiphany-swarm")),
|
||||
|
|
@ -630,7 +629,6 @@ fn required_release_build_target(role: &str) -> Result<(&'static str, &'static s
|
|||
}
|
||||
"atlas-impact-ingress" => Ok(("epiphany-core", "epiphany-atlas-impact-ingress")),
|
||||
"semantic-query" => Ok(("epiphany-core", "epiphany-memory-semantic")),
|
||||
"verse-query" => Ok(("epiphany-core", "epiphany-verse-query")),
|
||||
"repository-body" => Ok(("epiphany-core", "epiphany-repository-body")),
|
||||
"host-identity" => Ok(("epiphany-core", "epiphany-host-identity")),
|
||||
"swarm" => Ok(("epiphany-core", "epiphany-swarm")),
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
# Epiphany Rider Plugin MVP
|
||||
|
||||
This is the first Rider-side bridge surface for Epiphany.
|
||||
|
||||
It is deliberately small:
|
||||
|
||||
- show an Epiphany tool window inside Rider
|
||||
- call native `epiphany-rider-bridge status`
|
||||
- send current editor file/selection context through
|
||||
native `epiphany-rider-bridge context`
|
||||
- write operator-safe artifacts under `.epiphany-gui/rider`
|
||||
|
||||
The plugin does not talk to worker transcripts, raw results, or agent internals.
|
||||
Rider is the code view and context organ; Epiphany remains the coordinator and
|
||||
source of durable state.
|
||||
|
||||
## Build
|
||||
|
||||
The project uses the IntelliJ Platform Gradle Plugin. This machine currently
|
||||
does not have `gradle` on PATH, so the scaffold is source-ready but not yet
|
||||
locally build-verified.
|
||||
|
||||
```powershell
|
||||
cd E:\Projects\EpiphanyAgent\integrations\rider
|
||||
gradle buildPlugin
|
||||
```
|
||||
|
||||
## Runtime Contract
|
||||
|
||||
The plugin shells out to the local native bridge instead of owning protocol
|
||||
logic:
|
||||
|
||||
```powershell
|
||||
epiphany-rider-bridge status --project-root <repo>
|
||||
epiphany-rider-bridge context --project-root <repo> --file <file> --selection-start <line> --selection-end <line>
|
||||
```
|
||||
|
||||
Set these environment variables if needed:
|
||||
|
||||
- `EPIPHANY_REPO_ROOT`
|
||||
- `EPIPHANY_RIDER_BRIDGE`
|
||||
- `CARGO_TARGET_DIR`
|
||||
- `EPIPHANY_RIDER_ARTIFACT_ROOT`
|
||||
|
||||
The first dogfood use can also skip the plugin and use the native bridge
|
||||
directly. The point is the receipt shape, not making Rider perform authority
|
||||
the Epiphany coordinator owns.
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
plugins {
|
||||
id("org.jetbrains.kotlin.jvm") version "2.0.21"
|
||||
id("org.jetbrains.intellij.platform") version "2.14.0"
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
intellijPlatform {
|
||||
defaultRepositories()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
intellijPlatform {
|
||||
rider("2026.1")
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
}
|
||||
|
||||
intellijPlatform {
|
||||
pluginConfiguration {
|
||||
id = "dev.epiphany.rider"
|
||||
name = "Epiphany"
|
||||
version = "0.1.0"
|
||||
|
||||
ideaVersion {
|
||||
sinceBuild = "261"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "epiphany-rider"
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
package dev.epiphany.rider
|
||||
|
||||
import com.intellij.openapi.project.Project
|
||||
import java.io.File
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class EpiphanyBridgeClient(private val project: Project) {
|
||||
private val projectRoot: File = File(project.basePath ?: ".").absoluteFile
|
||||
private val epiphanyRoot: File = findEpiphanyRoot()
|
||||
|
||||
fun status(): String {
|
||||
return runBridge(listOf("status", "--project-root", projectRoot.absolutePath))
|
||||
}
|
||||
|
||||
fun captureContext(filePath: String, startLine: Int?, endLine: Int?): String {
|
||||
val args = mutableListOf(
|
||||
"context",
|
||||
"--project-root",
|
||||
projectRoot.absolutePath,
|
||||
"--file",
|
||||
filePath,
|
||||
)
|
||||
if (startLine != null) {
|
||||
args.add("--selection-start")
|
||||
args.add(startLine.toString())
|
||||
}
|
||||
if (endLine != null) {
|
||||
args.add("--selection-end")
|
||||
args.add(endLine.toString())
|
||||
}
|
||||
return runBridge(args)
|
||||
}
|
||||
|
||||
private fun runBridge(args: List<String>): String {
|
||||
val bridge = bridgePath()
|
||||
val artifactRoot = System.getenv("EPIPHANY_RIDER_ARTIFACT_ROOT")
|
||||
?: File(epiphanyRoot, ".epiphany-gui/rider").absolutePath
|
||||
val command = mutableListOf(bridge.absolutePath)
|
||||
command.addAll(args)
|
||||
command.add("--artifact-root")
|
||||
command.add(artifactRoot)
|
||||
|
||||
val process = ProcessBuilder(command)
|
||||
.directory(epiphanyRoot)
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
val output = process.inputStream.readBytes().toString(StandardCharsets.UTF_8)
|
||||
process.waitFor(30, TimeUnit.SECONDS)
|
||||
return output.ifBlank { "Epiphany bridge returned no output." }
|
||||
}
|
||||
|
||||
private fun bridgePath(): File {
|
||||
System.getenv("EPIPHANY_RIDER_BRIDGE")?.let { return File(it).absoluteFile }
|
||||
val targetDir = System.getenv("CARGO_TARGET_DIR")
|
||||
?: "C:\\Users\\Meta\\.cargo-target-codex"
|
||||
val candidate = File(targetDir, "debug/epiphany-rider-bridge.exe").absoluteFile
|
||||
if (candidate.exists()) {
|
||||
return candidate
|
||||
}
|
||||
return File(epiphanyRoot, "epiphany-core/target/debug/epiphany-rider-bridge.exe").absoluteFile
|
||||
}
|
||||
|
||||
private fun findEpiphanyRoot(): File {
|
||||
System.getenv("EPIPHANY_REPO_ROOT")?.let { return File(it).absoluteFile }
|
||||
var cursor: File? = projectRoot
|
||||
while (cursor != null) {
|
||||
if (File(cursor, "epiphany-core/Cargo.toml").exists()) {
|
||||
return cursor
|
||||
}
|
||||
cursor = cursor.parentFile
|
||||
}
|
||||
return File("E:\\Projects\\EpiphanyAgent")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
package dev.epiphany.rider
|
||||
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.CommonDataKeys
|
||||
import com.intellij.openapi.ui.Messages
|
||||
|
||||
class EpiphanyCaptureContextAction : AnAction() {
|
||||
override fun actionPerformed(event: AnActionEvent) {
|
||||
val project = event.project ?: return
|
||||
val editor = event.getData(CommonDataKeys.EDITOR)
|
||||
val file = event.getData(CommonDataKeys.VIRTUAL_FILE)
|
||||
if (file == null) {
|
||||
Messages.showWarningDialog(project, "No file is selected.", "Epiphany")
|
||||
return
|
||||
}
|
||||
|
||||
val document = editor?.document
|
||||
val selection = editor?.selectionModel
|
||||
val startLine = if (document != null && selection?.hasSelection() == true) {
|
||||
document.getLineNumber(selection.selectionStart) + 1
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val endLine = if (document != null && selection?.hasSelection() == true) {
|
||||
document.getLineNumber(selection.selectionEnd) + 1
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val output = EpiphanyBridgeClient(project).captureContext(file.path, startLine, endLine)
|
||||
Messages.showInfoMessage(project, output.take(1200), "Epiphany Context Captured")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
package dev.epiphany.rider
|
||||
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.wm.ToolWindow
|
||||
import com.intellij.openapi.wm.ToolWindowFactory
|
||||
import com.intellij.ui.content.ContentFactory
|
||||
import java.awt.BorderLayout
|
||||
import javax.swing.JButton
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.JScrollPane
|
||||
import javax.swing.JTextArea
|
||||
|
||||
class EpiphanyToolWindowFactory : ToolWindowFactory {
|
||||
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
|
||||
val output = JTextArea()
|
||||
output.isEditable = false
|
||||
output.lineWrap = true
|
||||
output.wrapStyleWord = true
|
||||
output.text = "Epiphany Rider bridge is ready. Click Refresh to capture a source-context status receipt."
|
||||
|
||||
val refresh = JButton("Refresh")
|
||||
refresh.addActionListener {
|
||||
output.text = EpiphanyBridgeClient(project).status()
|
||||
}
|
||||
|
||||
val panel = JPanel(BorderLayout(8, 8))
|
||||
panel.add(refresh, BorderLayout.NORTH)
|
||||
panel.add(JScrollPane(output), BorderLayout.CENTER)
|
||||
|
||||
val content = ContentFactory.getInstance().createContent(panel, "", false)
|
||||
toolWindow.contentManager.addContent(content)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
<idea-plugin>
|
||||
<id>dev.epiphany.rider</id>
|
||||
<name>Epiphany</name>
|
||||
<vendor>GameCult</vendor>
|
||||
|
||||
<depends>com.intellij.modules.platform</depends>
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<toolWindow
|
||||
id="Epiphany"
|
||||
anchor="right"
|
||||
factoryClass="dev.epiphany.rider.EpiphanyToolWindowFactory" />
|
||||
</extensions>
|
||||
|
||||
<actions>
|
||||
<action
|
||||
id="Epiphany.CaptureContext"
|
||||
class="dev.epiphany.rider.EpiphanyCaptureContextAction"
|
||||
text="Send Context to Epiphany"
|
||||
description="Write the current Rider file and selection as an Epiphany source-context artifact.">
|
||||
<add-to-group group-id="EditorPopupMenu" anchor="last" />
|
||||
<keyboard-shortcut keymap="$default" first-keystroke="ctrl alt E" />
|
||||
</action>
|
||||
</actions>
|
||||
</idea-plugin>
|
||||
|
|
@ -1,211 +0,0 @@
|
|||
# Epiphany Verse Architecture
|
||||
|
||||
This note names the Verse model Epiphany should grow into before the swarm is
|
||||
migrated onto it.
|
||||
|
||||
## Objective
|
||||
|
||||
Make Epiphany light on prompt context by making its context dynamic, typed, and
|
||||
queryable.
|
||||
|
||||
An Epiphany should not carry every memory, repo fact, prompt scar, and project
|
||||
doctrine in one swollen turn. It should query its local Verse, retrieve compact
|
||||
semantic context cuts, and assemble only the state needed for the current rite.
|
||||
|
||||
The machine should know more than it says. The prompt should carry only what it
|
||||
must.
|
||||
|
||||
## Current Mechanism
|
||||
|
||||
Epiphany already has the first organs:
|
||||
|
||||
- CultCache-shaped typed documents for thread state, runtime-spine jobs,
|
||||
heartbeat physiology, role memory, memory graph state, and operator
|
||||
receipts.
|
||||
- CultMesh local stores for Verse policies, global room policies, organ
|
||||
contracts, operator status, operator snapshots, and operator run
|
||||
intents/receipts.
|
||||
- Memory graph context cuts over repo, agent, heartbeat, incubation, agency,
|
||||
and evidence profiles.
|
||||
- Prompt assembly tests for bounded worker prompts, rendered Epiphany state,
|
||||
and the Persona Projector -> Persona -> Mind Interpreter membrane.
|
||||
|
||||
The missing organ is not another prompt rule. It is a local Verse query path
|
||||
that prompt assembly, Aquarium, CLI tools, and future workers can use to fetch
|
||||
compact state packets by authority and semantic relevance.
|
||||
|
||||
## Vocabulary
|
||||
|
||||
### Verse
|
||||
|
||||
A Verse is a scoped typed-state world.
|
||||
|
||||
It contains documents, policies, leases, receipts, rooms, and context surfaces
|
||||
for one trust boundary. Every project is an extension of the Verse. Every
|
||||
Epiphany is a series of nested Verses:
|
||||
|
||||
- private sub-agent Verse
|
||||
- trusted GameCult local-area Verse
|
||||
- public/global dream Verse
|
||||
- project/repo/topic/tool-specific sub-Verse surfaces
|
||||
|
||||
Verse membership is authority-bearing. A document being visible in one Verse
|
||||
does not make it safe to export into another.
|
||||
|
||||
### Local Verse
|
||||
|
||||
The local Verse is the Epiphany-owned queryable state surface for one instance.
|
||||
|
||||
It should expose compact TUI/API packets for:
|
||||
|
||||
- operator status
|
||||
- role and heartbeat state
|
||||
- runtime jobs and receipts
|
||||
- memory graph context cuts
|
||||
- organ contract policy
|
||||
- local Verse policies
|
||||
- admissible tool/query affordances
|
||||
|
||||
Local Verse context is prompt input, not durable truth by itself. Mind still
|
||||
owns durable adoption.
|
||||
|
||||
### Odin
|
||||
|
||||
Odin is the all-seer coordinator of Verse discovery.
|
||||
|
||||
Odin may know every Verse's advertised public/operator-safe surface: schemas,
|
||||
leases, status, public rooms, hosted services, and discovery metadata. Odin
|
||||
must not bypass Verse trust boundaries, Mind adoption gates, Substrate Gate
|
||||
access receipts, or private-state export rules.
|
||||
|
||||
Odin sees the map. Odin does not get to steal the throne.
|
||||
|
||||
### Yggdrasil
|
||||
|
||||
Yggdrasil is the hosting spine for important trusted GameCult Verses.
|
||||
|
||||
It hosts or tunnels key services such as Bifrost, schema publication, shared
|
||||
CultNet/CultMesh nodes, and other infrastructure whose availability matters to
|
||||
multiple projects.
|
||||
|
||||
`gamecult-local` may use explicit Yggdrasil tunnel policy for trusted sharing.
|
||||
`epiphany-internal` must not. `epiphany-global` is public thought weather, not
|
||||
a private-state bridge.
|
||||
|
||||
### Bifrost
|
||||
|
||||
Bifrost is a hosted governance and labor Verse.
|
||||
|
||||
It owns topics, work items, dispatch packets, review receipts, ledger/credit
|
||||
pressure, and public operator-safe proof. It can consume Epiphany results, but
|
||||
it does not become Epiphany's private memory or scheduler.
|
||||
|
||||
## Prompt Assembly Direction
|
||||
|
||||
Prompt assembly should become:
|
||||
|
||||
```text
|
||||
operator/human request
|
||||
-> Self routes the authority question
|
||||
-> local Verse query gathers compact typed context
|
||||
-> memory graph semantic search retrieves relevant shared memories
|
||||
-> Substrate Gate / Eyes provide source-grounded context when repo facts are needed
|
||||
-> prompt assembler renders a bounded packet for the chosen organ
|
||||
-> model output returns as thought/proposal/receipt
|
||||
-> Mind reviews any durable state effect
|
||||
```
|
||||
|
||||
The current smokes prove prompt rendering, bounded specialist prompt ownership,
|
||||
and the first launch-document handoff: `epiphany-prompt-context-smoke` renders
|
||||
Verse plus semantic-memory context and proves a fixed role launch document can
|
||||
carry that bounded packet.
|
||||
|
||||
## Query Surface
|
||||
|
||||
The first native query tool is:
|
||||
|
||||
```powershell
|
||||
cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-verse-query -- smoke
|
||||
cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-verse-query -- seed
|
||||
cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-verse-query -- query
|
||||
cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-prompt-context-smoke
|
||||
```
|
||||
|
||||
This writes and reads a compact CultMesh-backed local Verse context bundle:
|
||||
|
||||
- three Verse policies: `epiphany-internal`, `gamecult-local`,
|
||||
`epiphany-global`
|
||||
- Yggdrasil tunnel policy for `gamecult-local`
|
||||
- public global room policies for Persona/dream surfaces
|
||||
- operator status
|
||||
- latest operator run/snapshot receipts when present
|
||||
- organ contract summaries for Mind, Substrate Gate, Eyes, Hands, Soul, and
|
||||
Continuity
|
||||
|
||||
This is not semantic search yet. It is the first typed inspection packet the
|
||||
future prompt assembler can depend on.
|
||||
|
||||
`epiphany-prompt-context-smoke` is the first prompt-context proof on top of
|
||||
that packet. It seeds local Verse context, builds a memory graph context cut,
|
||||
renders the combined dynamic prompt packet, asserts that Verse/Odin/
|
||||
Yggdrasil/Bifrost context plus relevant semantic memory appear while unrelated
|
||||
private-looking text stays absent, and proves the packet is preserved on a role
|
||||
worker launch document. It is still a local proof, not a live swarm runner.
|
||||
|
||||
Role and reorient worker launch documents now carry optional
|
||||
`dynamicPromptContext`. The launch document owns this context for one worker
|
||||
launch. Runtime prompt assembly reads that field and inserts it between the
|
||||
role-local instruction and the output contract, so dynamic Verse/memory context
|
||||
is executable worker input without becoming durable state authority.
|
||||
|
||||
The bridge launch path now feeds that field for live role/reorient launches.
|
||||
It derives a sibling `local-verse.ccmp` CultMesh store from the runtime-spine
|
||||
store path, seeds/queries the local Verse context, refreshes/loads a sibling
|
||||
`memory-graph.msgpack` from the current typed thread-state repo graph, cuts a
|
||||
bounded semantic memory packet from that graph, and renders both into
|
||||
`dynamicPromptContext`. If the graph is thin, the packet says so instead of
|
||||
pretending semantic retrieval was rich.
|
||||
|
||||
The local-run status path now reads the same native Verse query surface.
|
||||
`tools/epiphany_local_run.ps1 -Mode status` builds/calls `epiphany-verse-query`
|
||||
and writes `local-verse-context.json` beside `status.json` and the operator
|
||||
snapshot artifact. This gives Aquarium/local-run an operator-safe Verse context
|
||||
packet without making launcher JSON the owner of status, contracts, prompts, or
|
||||
memory.
|
||||
|
||||
## Invariants
|
||||
|
||||
- CultCache documents are the data.
|
||||
- CultMesh is the ergonomic local/distributed Verse surface.
|
||||
- CultNet is the wire.
|
||||
- Qdrant/vector search is rebuildable resonance, not canonical memory.
|
||||
- Private state is never exported by flipping a visibility flag.
|
||||
- Foreign/public Verse material is thought weather until a reviewed local
|
||||
adoption receipt digests it.
|
||||
- Prompt assembly may read Verse context; it may not grant itself state,
|
||||
action, repo, or public-speech authority.
|
||||
- Every tool exposed to agents should have a compact, inspectable TUI/API
|
||||
surface before it becomes ambient prompt power.
|
||||
|
||||
## Migration Cut
|
||||
|
||||
For swarm migration, the next useful chain is:
|
||||
|
||||
1. Keep `epiphany-verse-query` as the local Verse context smoke.
|
||||
2. Keep semantic memory graph query packets beside the Verse policy/status
|
||||
packet for worker launch context.
|
||||
3. Keep local-run status reading native Verse context before Aquarium increases
|
||||
swarm cadence.
|
||||
|
||||
The bridge launch-context test is now the first launch/runtime smoke for this
|
||||
chain. It renders dynamic context, opens a runtime-spine worker request, reloads
|
||||
the persisted launch document, and asserts the Verse/memory packet survived.
|
||||
|
||||
The local-run status smoke is now the first operator read proof for this chain.
|
||||
It writes `local-verse-context.json` from a CultMesh store under
|
||||
`.epiphany-run/cultmesh/local-verse.ccmp` and leaves the compact packet beside
|
||||
the operator snapshot.
|
||||
|
||||
No live swarm runner is cleared until pause/brake, stale active-turn recovery,
|
||||
Persona eligibility, and memory lifecycle receipts remain inspectable through the
|
||||
same Verse model.
|
||||
|
|
@ -441,7 +441,7 @@ coordinator `12/12`, swarm `10/10`, and model-runtime `13/13`.
|
|||
|
||||
The Ox17 deployment lane is paused. Yggdrasil exposed 96 GiB of dead native
|
||||
Cargo targets after interrupted full-workspace builds; exact cleanup returned
|
||||
the builder target root to 4 KiB. The first causal source cut is verified:
|
||||
the builder target root to 4 KiB. Exact `1d5a1f17` landed the first causal cut:
|
||||
|
||||
- 21 duplicate core/release binary declarations are gone; each production
|
||||
entrypoint has one Cargo owner;
|
||||
|
|
@ -455,11 +455,19 @@ the builder target root to 4 KiB. The first causal source cut is verified:
|
|||
- core library tests remain 494/494 and Hands remains 5/5 under the sole release
|
||||
package owner.
|
||||
|
||||
Next, map and cut the surviving legacy `agent_memory` aggregate,
|
||||
`runtime_store_migration` library/tests, and the local PowerShell operator
|
||||
control plane. Tests survive only when they falsify a named live invariant.
|
||||
Do not run a full-workspace compile, package, deployment, or Ox root until the
|
||||
subtraction audit closes. Never resume Ox10, Ox12, Ox13, Ox15, or Ox16.
|
||||
The second verified cut is not yet committed. It deletes the
|
||||
`runtime_store_migration` module/test, all 29 remaining unshipped core
|
||||
executables, the shipped `epiphany-verse-query` parallel control plane, the
|
||||
local PowerShell launcher, and its orphan Rider bridge. `epiphany-core` now
|
||||
owns only `epiphany-prepare-compaction`; the release bundle owns 25 runtime
|
||||
executables. Focused core, supervisor, and release-construction tests pass
|
||||
493/493, 32/32, and 21/21.
|
||||
|
||||
Next, split the few live Persona/heartbeat inputs from `agent_memory`, then
|
||||
delete its aggregate identity, generation, SoA, migration/repair, and generic
|
||||
selfPatch authorities with the tests that preserve them. Do not run a
|
||||
full-workspace compile, package, deployment, or Ox root until the subtraction
|
||||
audit closes. Never resume Ox10, Ox12, Ox13, Ox15, or Ox16.
|
||||
|
||||
## Operational state that matters
|
||||
|
||||
|
|
|
|||
|
|
@ -58,8 +58,6 @@ HandsActionIntent
|
|||
`epiphany-core::hands_gateway` owns the typed document bodies and constructors.
|
||||
`epiphany-core::runtime_spine` can persist and reread the intent, review, patch,
|
||||
command, and commit receipts from the runtime-spine CultCache store.
|
||||
`epiphany-hands-action-smoke` proves the compact chain without executing shell
|
||||
commands, editing files, or creating a real commit.
|
||||
The launch organ contract's repo-action proof profile now requires the full
|
||||
Hands chain, not only the final patch receipt.
|
||||
`epiphany-mvp-coordinator` now turns `continueImplementation` into a persisted
|
||||
|
|
@ -69,14 +67,7 @@ the operator step.
|
|||
`record-patch`, `record-command`, and `record-commit` load the stored
|
||||
intent/review, reject mismatches, require an approved review, enforce allowed
|
||||
operations, and keep patch/commit changed paths inside the requested scope.
|
||||
`record-pass --gate-summary <coordinator-summary.json>` consumes the
|
||||
coordinator artifact and emits patch, command, and commit receipts in one
|
||||
post-implementation call.
|
||||
`epiphany-mvp-coordinator-smoke` now proves the deterministic chain from
|
||||
simulated `continueImplementation` through gate emission to `record-pass`
|
||||
receipts.
|
||||
|
||||
This is still a gate and receipt artery, not full action execution. The next
|
||||
live-action cut must call the recorder from the implementation turn or local
|
||||
wrapper after real edits/checks/commit and before Soul/Mind call the result
|
||||
accepted.
|
||||
Focused `epiphany-hands-action` tests prove that the live packaged recorder
|
||||
refuses wrong authority, scope escape, and substituted consequence cargo. The
|
||||
separate main-only smoke executables were deleted; they were compiled by Cargo
|
||||
but did not participate in the Rust test harness.
|
||||
|
|
|
|||
|
|
@ -482,9 +482,10 @@ does not invent that grant.
|
|||
|
||||
### Provider-Status Ownership Cut
|
||||
|
||||
`epiphany-cluster-daemon` is the legitimate production writer of its typed
|
||||
heartbeat/status. Idunn records stale observation, command execution, restart,
|
||||
and awaiting-provider-heartbeat in scheduler/poke/recovery state without
|
||||
Each packaged provider daemon is the sole writer of its typed heartbeat/status.
|
||||
There is no generic cluster daemon that may synthesize another provider's
|
||||
liveness. Idunn records stale observation, command execution, restart, and
|
||||
awaiting-provider-heartbeat in scheduler/poke/recovery state without
|
||||
rewriting provider status, operator action, or heartbeat time. A restarted
|
||||
provider becomes ready only when that provider publishes a newer authentic
|
||||
heartbeat; synthetic provider writers remain confined to test/quarantine
|
||||
|
|
@ -531,8 +532,8 @@ CultMesh health sight is landed.
|
|||
operator repair commands cannot create obligations, claims, attempts,
|
||||
receipts, readiness, or semantic-query admission.
|
||||
- Shared paths: `epiphany-memory-semantic health` is the sole explicit
|
||||
publication pulse, and `epiphany-verse-query semantic-health` only lowers the
|
||||
latest mirrors. Index execution emits its canonical receipt without calling
|
||||
publication pulse; Eve/CultMesh consumers lower its latest mirrors without a
|
||||
second operator command plane. Index execution emits its canonical receipt without calling
|
||||
the sight publisher, so a mirror failure cannot redefine execution success.
|
||||
- Cut line: there is no mirror-to-canonical import, mirror-to-Qdrant query, or
|
||||
mirror-to-readiness edge. A stale sealed input is refused instead of being
|
||||
|
|
|
|||
|
|
@ -216,9 +216,6 @@ node --test web/model-atlas-conformance.test.mjs
|
|||
node --test packages/eve-contracts/test/contracts.test.mjs
|
||||
```
|
||||
|
||||
`cargo check --workspace --all-targets --all-features` still reaches the older
|
||||
feature-gated `epiphany-workspace-coverage-recovery-smoke`, which imports
|
||||
`current_workspace_coverage_recovery_target` and
|
||||
`WorkspaceCoverageRecoveryTarget`; those symbols were already absent before
|
||||
this slice. Do not restore them as an Atlas compatibility shim. The ordinary
|
||||
all-target workspace and all Atlas-bearing targets compile.
|
||||
Atlas verification belongs to the owning library and packaged runtime tests.
|
||||
The old feature-gated recovery smoke executable and its smoke-only authority
|
||||
exports have been deleted.
|
||||
|
|
|
|||
|
|
@ -22,13 +22,8 @@ developer's damp recollection.
|
|||
companion custom-label fields.
|
||||
- [cultnet/epiphany.work_organ_state.v0.schema.json](./cultnet/epiphany.work_organ_state.v0.schema.json):
|
||||
light function-shaped state for Epiphany internal work organs.
|
||||
- [canonical-agent-state-schema.md](./canonical-agent-state-schema.md):
|
||||
human-facing explanation of Epiphany's lean work-organ state and Persona split.
|
||||
- [agent-state-variable-glossary.md](./agent-state-variable-glossary.md):
|
||||
full Persona-family glossary plus current Epiphany work-organ lattice receipts.
|
||||
- [organ-state-profiles.md](./organ-state-profiles.md):
|
||||
explicit split between lean Epiphany work-organ state and portable Persona
|
||||
state.
|
||||
- [agent-utterance-state-schema.md](./agent-utterance-state-schema.md):
|
||||
derived speech-conditioning subset for Weks, Aquarium, and other utterance
|
||||
surfaces; it carries identity, trait vectors, mood, and activation without
|
||||
|
|
|
|||
|
|
@ -1,204 +0,0 @@
|
|||
# Canonical Agent State Schema
|
||||
|
||||
Epiphany separates three state layers:
|
||||
|
||||
- `OrganState`: lean, function-shaped state for resident work organs.
|
||||
- `PersonaState`: full Persona/character kernel for public person-shaped agents.
|
||||
- `SwarmState`: scheduler and coordination physiology across organs and
|
||||
Personas.
|
||||
|
||||
The current sub-agent/substrate/protocol split is documented in
|
||||
[epiphany-anatomy.md](../notes/epiphany-anatomy.md). In particular, Body,
|
||||
Mind, Continuity, and Substrate Gate are not role-memory agents.
|
||||
|
||||
The executable shape lives in
|
||||
[agent_memory.rs](/E:/Projects/EpiphanyAgent/epiphany-core/src/agent_memory.rs).
|
||||
The local work-organ store still uses the `EpiphanyAgentMemoryEntry` Rust
|
||||
shape, but the full Ghostlight-style state is no longer required for every
|
||||
organ. The portable light organ contract is
|
||||
[`epiphany.work_organ_state.v0`](./cultnet/epiphany.work_organ_state.v0.schema.json).
|
||||
The portable person-state contract is
|
||||
[`gamecult.persona_state.v0`](./cultnet/gamecult.persona_state.v0.schema.json).
|
||||
Epiphany Persona projects its local organ-state record into the portable Persona
|
||||
contract with:
|
||||
|
||||
```powershell
|
||||
cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-agent-memory-store -- project-persona --store .\state\agents.msgpack --role-id Persona
|
||||
```
|
||||
|
||||
## Objective
|
||||
|
||||
The point of this schema is not theatrical anthropology. It is to give each
|
||||
organ enough durable state to do its job without smuggling project truth into
|
||||
the wrong compartment or making every lane pretend to be a public person.
|
||||
|
||||
Work-organ state stores:
|
||||
|
||||
- who an organ is
|
||||
- what authority boundary it must not cross
|
||||
- what it is trying to protect
|
||||
- what it remembers
|
||||
- what inputs, outputs, constraints, and receipts steer the current turn
|
||||
- how it is currently activated
|
||||
|
||||
It does not store affect, social bonds, status reads, or the whole project map
|
||||
in personality memory. That way lies a small cult of confused blobs.
|
||||
|
||||
## Stored Shape
|
||||
|
||||
Each record in `state/agents.msgpack` is an
|
||||
`EpiphanyAgentMemoryEntry` keyed by role id:
|
||||
|
||||
- `coordinator` -> `epiphany.self`
|
||||
- `Persona` -> `epiphany.Persona`
|
||||
- `imagination` -> `epiphany.imagination`
|
||||
- `research` -> `epiphany.eyes`
|
||||
- `modeling` -> `epiphany.modeling`
|
||||
- `implementation` -> `epiphany.hands`
|
||||
- `verification` -> `epiphany.soul`
|
||||
|
||||
There is intentionally no `reorientation` / `epiphany.continuity` organ-state record.
|
||||
Reorientation is a bounded Continuity worker/procedure, and Continuity records
|
||||
typed protocol receipts rather than self-memory.
|
||||
|
||||
The local top-level shape is:
|
||||
|
||||
- `schema_version`
|
||||
- `role_id`
|
||||
- `world`
|
||||
- `agent`
|
||||
- `relationships`
|
||||
- `events`
|
||||
- `scenes`
|
||||
|
||||
The local `agent` bundle contains:
|
||||
|
||||
- `agent_id`
|
||||
- `identity`
|
||||
- `canonical_state`
|
||||
- `goals`
|
||||
- `memories`
|
||||
- `perceived_state_overlays`
|
||||
|
||||
## Canonical State Families
|
||||
|
||||
Epiphany currently uses six canonical trait families plus values:
|
||||
|
||||
- `underlying_organization`
|
||||
- `stable_dispositions`
|
||||
- `behavioral_dimensions`
|
||||
- `presentation_strategy`
|
||||
- `voice_style`
|
||||
- `situational_state`
|
||||
- `values`
|
||||
|
||||
Each scalar-like trait vector is:
|
||||
|
||||
```json
|
||||
{
|
||||
"mean": 0.5,
|
||||
"plasticity": 0.5,
|
||||
"current_activation": 0.5
|
||||
}
|
||||
```
|
||||
|
||||
Mean is baseline tendency. Plasticity is how easily the variable shifts.
|
||||
Current activation is how hot it is right now.
|
||||
|
||||
## Epiphany-Specific Policy
|
||||
|
||||
Epiphany treats local role memory as two explicit profiles:
|
||||
|
||||
1. `work_organ`
|
||||
2. `persona`
|
||||
|
||||
See [organ-state-profiles.md](./organ-state-profiles.md).
|
||||
|
||||
The important distinction is:
|
||||
|
||||
- Self, Imagination, Eyes, Modeling, Hands, and Soul want a lean role
|
||||
lattice with room to grow through memory, rumination, and distillation
|
||||
- Epiphany Persona, VoidBot repo Personas, and Ghostlight-style characters want the
|
||||
denser, more fallible, relationship-heavy `Persona` surface
|
||||
- Heartbeat/swarm state owns scheduling, initiative, cooldown, active-turn
|
||||
freeze, and sleep/rumination physiology
|
||||
|
||||
So the local store remains compatible with existing role memory, but the
|
||||
authority split is explicit: work-organ state is not Persona state.
|
||||
|
||||
Epiphany does **not** require every standing sub-agent to populate the full old
|
||||
Ghostlight trait inventory. That would be fake precision. The dense personality
|
||||
families, affect, relationship pressure, perceived overlays, and social-read
|
||||
machinery belong to `persona`.
|
||||
|
||||
## Mutation Boundaries
|
||||
|
||||
Normal reviewed `selfPatch` writes may mutate only:
|
||||
|
||||
- memories
|
||||
- goals
|
||||
- values
|
||||
- private notes
|
||||
|
||||
They must not rewrite canonical trait bundles directly.
|
||||
|
||||
Canonical trait bundles are provisioned with the role shell. Later Persona
|
||||
memory and social-state changes enter as typed keyed Mind mutations with exact
|
||||
decision context; a startup classifier cannot rewrite them.
|
||||
|
||||
Persona state is expected to evolve through richer event, relationship,
|
||||
appraisal, affect, and distillation flows. The current narrow `selfPatch`
|
||||
contract is enough for work-organ growth, but it is not the end of the story
|
||||
for portable Personas.
|
||||
|
||||
Portable Persona documents must carry provenance and presentation metadata.
|
||||
`provenance` says which source system/document produced the state, when it was
|
||||
updated/exported, and whether the document is canonical, a projection, or an
|
||||
import. Timestamps use JSON Schema `date-time` format. `presentation` is
|
||||
required for public Personas and carries the public surface: voice summary,
|
||||
optional avatar/pronouns/renderer, home context or jurisdiction, and public
|
||||
handles.
|
||||
|
||||
The generic forward-pressure field is `candidateActions`. VoidBot-flavored
|
||||
`candidateInterventions` may exist under `voidbotProjection`, but it is a
|
||||
projection of the generic action surface rather than the shared contract's
|
||||
authority. Likewise, `anchoredThought.extensions` may preserve source-specific
|
||||
data, but extension fields are non-authoritative unless a consumer explicitly
|
||||
opts into that source contract.
|
||||
|
||||
Persona affect uses typed subshapes where the semantics matter: social bonds
|
||||
carry subject/object/kind/trust/tension, status reads carry target/kind and
|
||||
confidence, and doctrine stances carry principle/stance/action implication.
|
||||
Needs remain anchored thoughts because they are closer to pressure records than
|
||||
relationship facts.
|
||||
|
||||
Every enum that allows `custom` has a companion custom-label field, so custom
|
||||
does not become a stringly-typed category leak. `candidateActions.actions` are
|
||||
typed candidate-action records with action type, target, optional delivery
|
||||
target, readiness, risk level, urgency, confidence, evidence, and expiry. They
|
||||
may cite anchored thoughts as evidence, but they are not generic thoughts
|
||||
pretending to be actions. `privateNotes` are raw strings for v0 interchange
|
||||
only and must not become portable state authority without being promoted into
|
||||
typed private-note records.
|
||||
|
||||
## Project Truth Versus Self Truth
|
||||
|
||||
These organ-state records are not the same thing as thread state.
|
||||
|
||||
Project truth belongs in typed Epiphany state such as:
|
||||
|
||||
- maps
|
||||
- checkpoints
|
||||
- planning captures
|
||||
- coordinator results
|
||||
- evidence ledgers
|
||||
- runtime-spine receipts
|
||||
|
||||
Organ-state records hold role-local durable judgment, not the whole machine's
|
||||
current world model.
|
||||
|
||||
## Related Contracts
|
||||
|
||||
- [agent-state-variable-glossary.md](./agent-state-variable-glossary.md)
|
||||
- [organ-state-profiles.md](./organ-state-profiles.md)
|
||||
- [heartbeat-state-schema.md](./heartbeat-state-schema.md)
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
# Organ State Profiles
|
||||
|
||||
Epiphany uses one local `state/agents.msgpack` store, but it does not use one
|
||||
personality ontology for every organ. That was the old mush vector.
|
||||
|
||||
The live profile split is:
|
||||
|
||||
- `work_organ`: lean state for resident Epiphany sub-agents.
|
||||
- `persona`: portable person-state for Epiphany Persona, VoidBot repo Personas, and
|
||||
Ghostlight characters.
|
||||
|
||||
`Persona` is the Epiphany organ. `Persona` is the cross-runtime state contract.
|
||||
VoidBot's older "PersonaState" phrasing maps to `persona` here.
|
||||
|
||||
Swarm state is separate again: heartbeat, scheduler, initiative, cooldown,
|
||||
active-turn freeze, lane coordination, and sleep/rumination physiology live in
|
||||
heartbeat/swarm contracts rather than either local work-organ state or Persona
|
||||
state.
|
||||
|
||||
## `work_organ`
|
||||
|
||||
Intended for:
|
||||
|
||||
- Self
|
||||
- Imagination
|
||||
- Eyes
|
||||
- Modeling
|
||||
- Hands
|
||||
- Soul
|
||||
|
||||
Work organs need:
|
||||
|
||||
- identity and role boundary
|
||||
- authority boundary
|
||||
- current task/context
|
||||
- durable mission-relevant memory
|
||||
- inputs and outputs
|
||||
- active constraints
|
||||
- receipts
|
||||
- values and goals that steer judgment
|
||||
- heartbeat activation and timing pressure
|
||||
- narrow private notes for future self-correction
|
||||
- optional skill/capability profile
|
||||
|
||||
They do not need affect, social-bond machinery, dramatic relationship stance,
|
||||
or dense Ghostlight personality maps unless a real use case earns that weight.
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- `identity`
|
||||
- `goals`
|
||||
- `canonical_state.values`
|
||||
- authority/input/output/receipt summaries when projected onto CultNet
|
||||
- sparse trait maps only where they steer work
|
||||
- semantic and episodic memories when they change future judgment
|
||||
- relationship summaries only when they describe organ dependency or working
|
||||
coordination
|
||||
|
||||
The portable light schema is
|
||||
[`epiphany.work_organ_state.v0`](./cultnet/epiphany.work_organ_state.v0.schema.json).
|
||||
|
||||
Forbidden sludge:
|
||||
|
||||
- treating mood as proof
|
||||
- storing repo truth in self-memory
|
||||
- giving Hands, Eyes, Soul, or Modeling Persona-style attention hunger,
|
||||
status reads, social bonds, or performative personality loops
|
||||
|
||||
## `persona`
|
||||
|
||||
Intended for:
|
||||
|
||||
- Epiphany Persona
|
||||
- VoidBot repo Personas
|
||||
- Ghostlight scene characters
|
||||
- future public or dramatic agents that must react as situated people
|
||||
|
||||
Persona state needs:
|
||||
|
||||
- public identity
|
||||
- provenance: source system, source document, export time, and whether the
|
||||
document is canonical, a projection, or an import
|
||||
- public presentation surface: avatar URI, pronouns, voice summary, renderer,
|
||||
home context/jurisdiction, and public handles where available
|
||||
- values and private notes
|
||||
- activation/personality vectors
|
||||
- voice and presentation constraints
|
||||
- thought memory
|
||||
- agency pressure
|
||||
- candidate actions
|
||||
- affect: needs, typed social bonds, typed status reads, mood dimensions,
|
||||
social biases, and typed doctrine stances
|
||||
- perceived-state overlays or equivalent fallible social/world reads
|
||||
|
||||
The portable schema is
|
||||
[`gamecult.persona_state.v0`](./cultnet/gamecult.persona_state.v0.schema.json).
|
||||
It is deliberately close to VoidBot's mature repo Persona state while remaining
|
||||
usable by Ghostlight and Epiphany Persona.
|
||||
|
||||
The generic action surface is `candidateActions`. VoidBot may project those
|
||||
actions back into `voidbotProjection.candidateInterventions` for repo-Persona
|
||||
routine compatibility, but that projection does not own the portable contract.
|
||||
Candidate actions are typed records with action type, target, optional delivery
|
||||
target, readiness, risk level, urgency, confidence, evidence, and expiry. They
|
||||
may point back to anchored thoughts as evidence, but they are not themselves
|
||||
generic thoughts wearing an action hat.
|
||||
|
||||
`anchoredThought.extensions` is a quarantine bag for source-specific fields.
|
||||
Portable consumers may preserve those fields, but they are not authoritative
|
||||
PersonaState unless the consumer explicitly understands the source contract.
|
||||
Timestamp fields use JSON Schema `date-time` format, public Persona documents
|
||||
must include `presentation`, and any `custom` enum value must carry a companion
|
||||
custom-label field rather than leaving consumers with a shrug in a hat.
|
||||
|
||||
`privateNotes` are still raw strings in v0 for simple interchange. They should
|
||||
not become portable authority; if private notes start needing provenance,
|
||||
lifecycle, or routing semantics, promote them into typed private-note records or
|
||||
drop them from the shared interchange surface.
|
||||
|
||||
The affect surface is not one flat thought bucket. Needs still use
|
||||
`anchoredThought`, but bonds carry subject/object/kind/trust/tension, status
|
||||
reads carry target/kind/confidence, and doctrine stances carry
|
||||
principle/stance/action implication. The schema is still v0, but it no longer
|
||||
pretends every social fact has the same bones.
|
||||
|
||||
Failure mode if underbuilt:
|
||||
|
||||
- Persona becomes a status printer with eyeliner
|
||||
- Ghostlight characters become omniscient utility daemons wearing skin
|
||||
|
||||
Failure mode if over-applied:
|
||||
|
||||
- every Epiphany work lane starts growing fake interiority instead of sharper
|
||||
judgment
|
||||
|
||||
## Wire Visibility
|
||||
|
||||
The distinction is visible to tools through `organStateProfile`:
|
||||
|
||||
- `profileKind = work_organ`
|
||||
- `profileKind = persona`
|
||||
|
||||
Current typed surfaces expose profile classification through:
|
||||
|
||||
- `epiphany-agent-memory-store status`
|
||||
- `epiphany-agent-memory-store project-persona --role-id Persona`
|
||||
- `epiphany-character-loop` packets
|
||||
- `epiphany.agent_utterance_state.v0`
|
||||
|
||||
If a surface consumes local organ state without surfacing the profile kind, it
|
||||
is already drifting back toward folklore.
|
||||
|
||||
`project-persona` is the current Epiphany Persona bridge into
|
||||
`gamecult.persona_state.v0`: it reads the local Persona organ-state record from
|
||||
`state/agents.msgpack` and emits the portable Persona document with provenance,
|
||||
presentation, activation profile, memories, agency pressure, typed candidate
|
||||
actions, typed affect projections, and the VoidBot projection slot. The local
|
||||
store remains Epiphany-owned; the Persona document is the interchange nerve.
|
||||
Binary file not shown.
|
|
@ -48,9 +48,9 @@ current_status:
|
|||
package_baseline: Exact d2ca66301fb6af4e7d2d27fff0b772b0f0fccdf4 remains the admitted production source and current symlink target. Its native serialized full-workspace gate passed with receipt SHA-256 de0fc6b360ce03493b13208d917dc8349801f03364617d966152c85846c47482. Idunn sealed 26 root-owned non-writable binaries plus release-witness.ccmp as release sha256-46407552b4a0937f63d2b7f2bd09a1dacb89d671a6e3807c97209159541aef06; witness-file SHA-256 is 348785ffb0fc3130d3b4538329870c6e6f8442a8da4a01b32fa9b7ffb1f01357. Source 9b9b5c85 also passed Idunn's native test gate under receipt SHA-256 6c6a71359f8c31297419665d2872f6982a89f84e197d71fc5b36a7fc86216093 and remains sealed as release sha256-30a910785a380224bbdbd56a7b742c8d058fa70cb72dab3562e3907903e0d191 with witness-file SHA-256 75bf52d31c56ea3b08e1a702273301c1db12a4b1ffae45e3d948ce8a29b50e1d. A foreign-target helper restarted Idunn during the Epiphany transaction; rollback restored d2ca, removed deployment.env, and left production Epiphany units inactive. OpenRouter model stealth/ox-alpha and its systemd-injected credential are readiness-proven. Idunn is active with zero restarts and its deployment brake is engaged.
|
||||
summary: The keyed RepoModel hard cut, decision-context foundation, Model Atlas code slice, complete Body, Proposal, frontier-verdict Modeling, frontier Research, frontier Verification, frontier Planning/PlanMind, Reorientation, and Persona decision-context lifecycles are landed on codex/epiphany-shakedown-live. Keyed Mind plus exact runtime receipts own coordinator presence, work discovery, deterministic launch, job-bound continuation, review, typed failure, semantic admission, and retained decision reachability. Exact 07d891ba and ec1431ff delete the persisted/global thread aggregate and its generic coordinator/prompt/revision surfaces. Exact f7948795 deletes MindGatewayReview, MindStateCommitReceipt v0, the generic interpreter prompt, runtime registration/read APIs, false thought/state-effect/adoption CultNet mouths, and stale launch proof profiles. CultMesh now projects only reasoning basis, decision context, and exact Mind commit receipt, read-only. Exact 1c9aafd8 seals the terminal decision context before model-backed frontier-Planning failure and binds the typed failure plus generic job result to it. Exact e0e75a30 deletes the residual aggregate role patch/parser/policy surface, gives Research one closed typed decision, and deletes the unowned generic Imagination planning patch. Exact 12b1b285 makes runtime model/tool binding validation the shared owner for execution and decision sealing; hostile provider, receipt, and stored-binding substitutions refuse, while retained basis/context audit survives transcript-delta deletion. Exact 01602fd3 advances Mind/runtime to v2/v1 and refuses historical or split writable identity pairs before registered writers see them. Exact 26b6a5bf proves simultaneous disjoint Modeling writes, simultaneous Verification+Modeling admission, same-identity conflict, exact replay, and reopened graph assembly; raw mutation primitives are crate-private. Exact 7c2ebd81 moves Persona state-note admission from the legacy agent-memory aggregate into keyed Mind documents and proves a concrete Hands consequence can commit simultaneously. Exact 79c0e373 seals the assembled Persona input and observed source versions in Mind before inference. Exact d3300bba authenticates that input through its one exact Mind commit, refuses naked typed documents, re-enters without external observation or model inference, and proves current-work reconstruction is identical and read-only across Launch, Wait, completed, and post-Research boundaries. No compatibility reader, bootstrap aggregate, schema migrator, or dual reader survives. The source-level Decision-Auditable Concurrent Mind migration is accepted; the fresh exact-package capstone remains open. The machine remains a supervised engineering alpha; Model Atlas operational Gate 1 stays paused.
|
||||
public_positioning: docs/positioning.md is the current public claim owner. Epiphany is the organizational layer above frontier AI workers; global coherence collapse is the primary failure; prompts are bounded projections of governed organizational state; humans retain purpose, judgment, authority, disagreement, exceptions, and acceptance. GameCult is the forge and longitudinal evidence program for a target federation of one locally owned Epiphany project organism per living project Body. Model Atlas carries typed offers, claims, evidence, and derived technical relationships; Bifrost is the intended external work, membership, governance, attribution, and identity-linked receipt rail. Public Persona relationships and visitor surfaces are intended social infrastructure, while private Mind and consequence authority remain local. The fleet, real three-repository Gate 1, complete Bifrost attribution, sustained autonomous production, visitor experience, and external design-partner pilots remain gated alpha targets.
|
||||
latest_phase: The fresh-package Ox17 lane is paused for source subtraction. Yggdrasil exposed and then released 96 GiB of dead native Cargo targets. The first causal cut removes 19,252 net non-vendor lines: 21 duplicate core/release binary declarations, 19 generic repo-request smoke executables that cargo test compiled but never executed as tests, the forbidden runtime-store migrator CLI, and the unshipped repo-personality birth subsystem with its parallel agents.msgpack/heartbeat initialization, prompt fields, schemas, and stale contract plans. epiphany-core falls from 75 to 30 binary targets and duplicate executable ownership is zero. Focused core library acceptance remains 494/494; Hands under its sole release-package owner remains 5/5; the CultNet schema catalog and local PowerShell syntax parse. Build-profile and janitor patches remain deferred. The next cut audits the legacy agent-memory aggregate, runtime migration library/tests, and local PowerShell operator control plane against keyed Mind and Eve ownership. Exact 103e8510 remains pushed but is not authorized for another Idunn build. Ox16 remains sealed historical evidence and must never resume.
|
||||
latest_phase: The fresh-package Ox17 lane remains paused for source subtraction. Exact 1d5a1f17 removed 19,252 net non-vendor lines, 21 duplicate executable owners, 19 main-only smoke binaries, and the unshipped repo-personality birth path. The next verified cut deletes the forbidden runtime-store migration module/test, all 29 remaining unshipped core executables, the shipped 7,814-line verse-query parallel control plane, the 2,244-line local PowerShell launcher, and its orphan Rider bridge. epiphany-core now owns only the explicit prepare-compaction maintenance binary; the release bundle owns 25 runtime executables. Focused acceptance passes: core library 493/493, daemon supervisor 32/32, and release construction 21/21. Smoke-only feature exports and supervisor commands that pointed to deleted bodies are gone. The next cut is the surviving agent_memory aggregate and selfPatch/SoA/migration tests against keyed Mind Persona documents. No Idunn build, package, deployment, or Ox root is authorized yet. Ox16 remains sealed historical evidence and must never resume.
|
||||
operational_state:
|
||||
local_worktree: source_head_103e8510_pushed_first_subtraction_cut_verified_uncommitted_ox17_and_deployment_paused
|
||||
local_worktree: source_head_1d5a1f17_pushed_second_subtraction_cut_verified_uncommitted_ox17_and_deployment_paused
|
||||
docker_engine_access: starfire_compiler_retired_all_yggdrasil_epiphany_transient_containers_volumes_package_roots_and_per_run_test_target_removed_future_idunn_epiphany_builds_native_rust_1_95_0_via_gamecult_ops_89f0d78
|
||||
operator_wireguard: restored
|
||||
starfire_persona_permit_firewall: persistent_exact_gamecult_ops_6ee08a4
|
||||
|
|
@ -90,8 +90,8 @@ current_status:
|
|||
starfire_linux_modeling_peak: 109887488_bytes_resident_body_plus_modeling_circuit
|
||||
yggdrasil_epiphany_service_contract: idunn_source_change_owner_eve_and_local_typed_operator_surfaces_no_discord_control_plane
|
||||
resident_physiology: historical_c011_exact_465af24d_zero_zombie_proof_accepted_current_containers_exited_255
|
||||
unresolved_authority: The first executable-ownership cut is coherent, but `agent_memory` and `state/agents.msgpack` still expose a parallel durable role-memory aggregate beside keyed Mind, `runtime_store_migration` still preserves a forbidden writable migration law after its CLI was deleted, and the 2,000-line local PowerShell operator wrapper still keeps unshipped direct Persona/operator paths alive beside Eve and the packaged runtime. Fresh exact-package Ox17 and Model Atlas Gate 1 remain paused until those owners are resolved. Bifrost remains Persona and governed external-consequence transport. Historical c011 and partial Gate roots stay untouched/read-only.
|
||||
next_action: Map and cut the legacy agent-memory aggregate, runtime-store migration module/tests, and local PowerShell operator control plane. A test survives only when it falsifies a named live invariant at its owning boundary; green serialization mirrors, manual main-only smokes, and tests that preserve deleted authorities do not. Do not run a full-workspace compile, package, deployment, or Ox root before this subtraction audit closes.
|
||||
unresolved_authority: `agent_memory` still exposes a parallel durable role-memory aggregate, swarm identity, generation head, SoA mirror, JSON migration/repair path, and generic selfPatch lifecycle beside keyed Mind Persona documents. Several current tests prove those obsolete shapes rather than a live decision-admission invariant. Fresh exact-package Ox17 and Model Atlas Gate 1 remain paused until that owner is resolved. Bifrost remains Persona and governed external-consequence transport. Historical c011 and partial Gate roots stay untouched/read-only.
|
||||
next_action: Split the few live Persona/heartbeat inputs from `agent_memory`, then delete the aggregate identity/generation/SoA/migration/selfPatch authorities and their tests. A test survives only when it falsifies a named live invariant at its owning boundary. Do not run a full-workspace compile, package, deployment, or Ox root before this subtraction audit closes.
|
||||
|
||||
active_subgoals:
|
||||
- Complete the fresh exact-package Decision-Auditable Concurrent Mind capstone before any further operational Gate run.
|
||||
|
|
|
|||
|
|
@ -18,9 +18,10 @@ machine smaller and the canonical map explicitly reopens that lane.
|
|||
388 MiB build-script output. Several individual test harnesses were
|
||||
500--700 MiB.
|
||||
- `epiphany-core` declared 75 binary targets. The root release bundle declared
|
||||
another 26 targets, many by pointing at the same source files. The first cut
|
||||
reduces core to 30 binaries with zero duplicate owners and removes 19,252 net
|
||||
non-vendor lines.
|
||||
another 26 targets, many by pointing at the same source files. Exact
|
||||
`1d5a1f17` reduced core to 30 binaries with zero duplicate owners and removed
|
||||
19,252 net non-vendor lines. The current cut reduces core again to its one
|
||||
explicit maintenance binary and the package to 25 owned runtime binaries.
|
||||
|
||||
The storage failure is evidence of source-shape failure. Build-profile flags
|
||||
and janitors may reduce the symptom but cannot substitute for deleting code.
|
||||
|
|
@ -50,16 +51,14 @@ and janitors may reduce the symptom but cannot substitute for deleting code.
|
|||
|
||||
## Immediate audit order
|
||||
|
||||
1. Map `agent_memory` and every `state/agents.msgpack` consumer against keyed
|
||||
1. Map `agent_memory` and every remaining aggregate consumer against keyed
|
||||
Mind. Delete memories, social state, migrations, projections, and tests whose
|
||||
only purpose is to preserve the parallel aggregate.
|
||||
2. Delete `runtime_store_migration` and its tests unless a live fresh-epoch path
|
||||
proves an owner; the approved migration explicitly has no writable migrator.
|
||||
3. Map the local PowerShell operator wrapper and its direct Persona/operator
|
||||
binaries against Eve, Bifrost, and the packaged runtime. Delete the parallel
|
||||
control plane rather than repairing its modes.
|
||||
4. Continue classifying remaining smoke/fixture/benchmark executables. A test
|
||||
must falsify a named live invariant; compilation or a green mirror assertion
|
||||
is not enough.
|
||||
2. Done: delete `runtime_store_migration`, its receipt registration, and its
|
||||
test. The approved migration has no writable migrator.
|
||||
3. Done: delete the local PowerShell operator wrapper, direct Persona/operator
|
||||
binaries, shipped `verse-query` control plane, and orphan Rider integration.
|
||||
4. Done: reduce `epiphany-core` from 30 binary targets to the one native
|
||||
compaction helper. Runtime executables belong to the release bundle.
|
||||
5. Run focused tests after each cut. Only after source subtraction stabilizes
|
||||
should Idunn's build scratch contract be reconsidered.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Reference in a new issue