diff --git a/epiphany-core/src/bin/epiphany-mvp-status.rs b/epiphany-core/src/bin/epiphany-mvp-status.rs new file mode 100644 index 00000000..fb02db1d --- /dev/null +++ b/epiphany-core/src/bin/epiphany-mvp-status.rs @@ -0,0 +1,767 @@ +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::fs::File; +use std::io::BufRead; +use std::io::BufReader; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::{Child, ChildStdin, Command, Stdio}; +use std::sync::Arc; +use std::sync::Mutex; +use std::sync::mpsc; +use std::thread; +use std::time::{Duration, Instant}; + +const DEFAULT_APP_SERVER: &str = r"C:\Users\Meta\.cargo-target-codex\debug\codex-app-server.exe"; +const DEFAULT_CARGO_TARGET_DIR: &str = r"C:\Users\Meta\.cargo-target-codex"; +const SEALED_DIRECT_THOUGHT_KEYS: &[&str] = &[ + "rawResult", + "turns", + "items", + "inputTranscript", + "activeTranscript", +]; + +fn main() -> Result<()> { + let args = Args::parse()?; + let status = run_status(&args)?; + if let Some(result) = &args.result { + if let Some(parent) = result.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("failed to create {}", parent.display()))?; + } + fs::write( + result, + format!("{}\n", serde_json::to_string_pretty(&status)?), + ) + .with_context(|| format!("failed to write {}", result.display()))?; + write_transcript_telemetry(&args.transcript, &result.with_extension("telemetry.json"))?; + } + write_transcript_telemetry( + &args.transcript, + &args.transcript.with_extension("telemetry.json"), + )?; + if args.json { + println!("{}", serde_json::to_string_pretty(&status)?); + } else { + print!("{}", render_status(&status)); + } + Ok(()) +} + +#[derive(Debug)] +struct Args { + app_server: PathBuf, + codex_home: PathBuf, + thread_id: Option, + cwd: PathBuf, + ephemeral: bool, + json: bool, + result: Option, + transcript: PathBuf, + stderr: PathBuf, +} + +impl Args { + fn parse() -> Result { + let root = env::current_dir().context("failed to resolve current directory")?; + let mut args = env::args().skip(1); + let mut parsed = Args { + app_server: PathBuf::from(DEFAULT_APP_SERVER), + codex_home: env::var_os("CODEX_HOME") + .map(PathBuf::from) + .unwrap_or_else(|| home_dir().join(".codex")), + thread_id: None, + cwd: root.clone(), + ephemeral: true, + json: false, + result: None, + transcript: root + .join(".epiphany-status") + .join("epiphany-mvp-status-transcript.jsonl"), + stderr: root + .join(".epiphany-status") + .join("epiphany-mvp-status-server.stderr.log"), + }; + while let Some(arg) = args.next() { + match arg.as_str() { + "--app-server" => parsed.app_server = take_path(&mut args, "--app-server")?, + "--codex-home" => parsed.codex_home = take_path(&mut args, "--codex-home")?, + "--thread-id" => parsed.thread_id = Some(take_string(&mut args, "--thread-id")?), + "--cwd" => parsed.cwd = take_path(&mut args, "--cwd")?, + "--ephemeral" => parsed.ephemeral = true, + "--no-ephemeral" => parsed.ephemeral = false, + "--json" => parsed.json = true, + "--result" => parsed.result = Some(take_path(&mut args, "--result")?), + "--transcript" => parsed.transcript = take_path(&mut args, "--transcript")?, + "--stderr" => parsed.stderr = take_path(&mut args, "--stderr")?, + _ => return Err(anyhow!("unknown argument: {arg}")), + } + } + Ok(parsed) + } +} + +fn run_status(args: &Args) -> Result { + let app_server = absolute_path(&args.app_server)?; + let codex_home = absolute_path(&args.codex_home)?; + let cwd = absolute_path(&args.cwd)?; + let transcript = absolute_path(&args.transcript)?; + let stderr = absolute_path(&args.stderr)?; + + if !app_server.exists() { + return Err(anyhow!( + "codex app-server binary not found: {}", + app_server.display() + )); + } + fs::create_dir_all(&codex_home) + .with_context(|| format!("failed to create {}", codex_home.display()))?; + let mut client = AppServerClient::start(&app_server, &codex_home, &transcript, &stderr)?; + client.send( + "initialize", + Some(json!({ + "clientInfo": { + "name": "epiphany-mvp-status", + "title": "Epiphany MVP Status", + "version": "0.1.0", + }, + "capabilities": {"experimentalApi": true}, + })), + true, + )?; + client.send("initialized", None, false)?; + + let thread_id = if let Some(thread_id) = &args.thread_id { + if !args.ephemeral { + client.send("thread/resume", Some(json!({"threadId": thread_id})), true)?; + } + thread_id.clone() + } else { + let started = client.send( + "thread/start", + Some(json!({"cwd": cwd, "ephemeral": args.ephemeral})), + true, + )?; + started["thread"]["id"] + .as_str() + .ok_or_else(|| anyhow!("thread/start returned no thread id"))? + .to_string() + }; + + let read = client.send( + "thread/read", + Some(json!({"threadId": thread_id, "includeTurns": false})), + true, + )?; + let scene = client.send( + "thread/epiphany/scene", + Some(json!({"threadId": thread_id})), + true, + )?; + let pressure = client.send( + "thread/epiphany/pressure", + Some(json!({"threadId": thread_id})), + true, + )?; + let reorient = client.send( + "thread/epiphany/reorient", + Some(json!({"threadId": thread_id})), + true, + )?; + let jobs = client.send( + "thread/epiphany/jobs", + Some(json!({"threadId": thread_id})), + true, + )?; + let roles = client.send( + "thread/epiphany/roles", + Some(json!({"threadId": thread_id})), + true, + )?; + let planning = client.send( + "thread/epiphany/planning", + Some(json!({"threadId": thread_id})), + true, + )?; + let role_results = json!({ + "imagination": client.send("thread/epiphany/roleResult", Some(json!({"threadId": thread_id, "roleId": "imagination"})), true)?, + "modeling": client.send("thread/epiphany/roleResult", Some(json!({"threadId": thread_id, "roleId": "modeling"})), true)?, + "verification": client.send("thread/epiphany/roleResult", Some(json!({"threadId": thread_id, "roleId": "verification"})), true)?, + }); + let reorient_result = client.send( + "thread/epiphany/reorientResult", + Some(json!({"threadId": thread_id})), + true, + )?; + let crrc = client.send( + "thread/epiphany/crrc", + Some(json!({"threadId": thread_id})), + true, + )?; + let coordinator = client.send( + "thread/epiphany/coordinator", + Some(json!({"threadId": thread_id})), + true, + )?; + let root = env::current_dir().context("failed to resolve current directory")?; + let heartbeat_dir = root.join(".epiphany-heartbeats"); + let face_dir = root.join(".epiphany-face"); + let heartbeat = native_json( + "epiphany-heartbeat-store", + &[ + "status", + "--store", + "state/agent-heartbeats.msgpack", + "--artifact-dir", + &heartbeat_dir.to_string_lossy(), + "--limit", + "8", + ], + )?; + let latest_face = native_json( + "epiphany-face-discord", + &[ + "latest", + "--artifact-dir", + &face_dir.to_string_lossy(), + "--limit", + "8", + ], + ) + .unwrap_or_else( + |error| json!({"status": "error", "error": error.to_string(), "latestArtifacts": []}), + ); + let face = json!({ + "status": "ready", + "artifactDir": face_dir, + "latestArtifacts": latest_face.get("latestArtifacts").cloned().unwrap_or_else(|| json!([])), + "availableActions": ["faceBubble"], + }); + let status = json!({ + "threadId": thread_id, + "read": read, + "scene": scene, + "pressure": pressure, + "reorient": reorient, + "jobs": jobs, + "roles": roles, + "planning": planning, + "roleResults": role_results, + "reorientResult": reorient_result, + "crrc": crrc, + "coordinator": coordinator, + "heartbeat": heartbeat, + "face": face, + }); + Ok(sanitize_for_operator(status)) +} + +struct AppServerClient { + child: Child, + stdin: ChildStdin, + rx: mpsc::Receiver, + transcript: Arc>, + next_id: u64, +} + +impl AppServerClient { + fn start( + app_server: &Path, + codex_home: &Path, + transcript_path: &Path, + stderr_path: &Path, + ) -> Result { + if let Some(parent) = transcript_path.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("failed to create {}", parent.display()))?; + } + if let Some(parent) = stderr_path.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("failed to create {}", parent.display()))?; + } + let transcript = Arc::new(Mutex::new( + File::create(transcript_path) + .with_context(|| format!("failed to create {}", transcript_path.display()))?, + )); + let stderr_file = Arc::new(Mutex::new( + File::create(stderr_path) + .with_context(|| format!("failed to create {}", stderr_path.display()))?, + )); + let mut command = Command::new(app_server); + command + .current_dir( + env::current_dir()? + .join("vendor") + .join("codex") + .join("codex-rs"), + ) + .env("CODEX_HOME", codex_home) + .env( + "CARGO_TARGET_DIR", + env::var("CARGO_TARGET_DIR") + .unwrap_or_else(|_| DEFAULT_CARGO_TARGET_DIR.to_string()), + ) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + let mut child = command + .spawn() + .context("failed to spawn codex app-server")?; + let stdin = child + .stdin + .take() + .ok_or_else(|| anyhow!("app-server stdin unavailable"))?; + let stdout = child + .stdout + .take() + .ok_or_else(|| anyhow!("app-server stdout unavailable"))?; + let stderr = child + .stderr + .take() + .ok_or_else(|| anyhow!("app-server stderr unavailable"))?; + let (tx, rx) = mpsc::channel(); + let transcript_for_stdout = Arc::clone(&transcript); + thread::spawn(move || { + for line in BufReader::new(stdout).lines().map_while(Result::ok) { + if line.trim().is_empty() { + continue; + } + let message = serde_json::from_str::(&line).unwrap_or_else( + |error| json!({"_decode_error": error.to_string(), "raw": line}), + ); + record(&transcript_for_stdout, "received", &message); + let _ = tx.send(message); + } + }); + thread::spawn(move || { + for line in BufReader::new(stderr).lines().map_while(Result::ok) { + if let Ok(mut file) = stderr_file.lock() { + let _ = writeln!(file, "{line}"); + } + } + }); + Ok(Self { + child, + stdin, + rx, + transcript, + next_id: 1, + }) + } + + fn send( + &mut self, + method: &str, + params: Option, + expect_response: bool, + ) -> Result { + let mut message = serde_json::Map::new(); + message.insert("method".to_string(), json!(method)); + let request_id = if expect_response { + let id = self.next_id; + self.next_id += 1; + message.insert("id".to_string(), json!(id)); + Some(id) + } else { + None + }; + if let Some(params) = params { + message.insert("params".to_string(), params); + } + let message = Value::Object(message); + record(&self.transcript, "sent", &message); + writeln!( + self.stdin, + "{}", + serde_json::to_string(&message).context("failed to encode request")? + ) + .context("failed to write app-server request")?; + self.stdin + .flush() + .context("failed to flush app-server stdin")?; + let Some(request_id) = request_id else { + return Ok(Value::Null); + }; + self.wait_for(request_id) + } + + fn wait_for(&mut self, request_id: u64) -> Result { + let deadline = Instant::now() + Duration::from_secs(45); + while Instant::now() < deadline { + if let Some(status) = self.child.try_wait()? { + return Err(anyhow!( + "app-server exited with {} before response {}", + status, + request_id + )); + } + match self.rx.recv_timeout(Duration::from_millis(500)) { + Ok(message) => { + if message.get("id").and_then(Value::as_u64) != Some(request_id) { + continue; + } + if let Some(error) = message.get("error") { + return Err(anyhow!("request {request_id} failed: {error}")); + } + let result = message + .get("result") + .cloned() + .ok_or_else(|| anyhow!("request {request_id} returned no result"))?; + if !result.is_object() { + return Err(anyhow!( + "request {request_id} returned non-object result: {result}" + )); + } + return Ok(result); + } + Err(mpsc::RecvTimeoutError::Timeout) => {} + Err(error) => return Err(anyhow!("app-server response channel closed: {error}")), + } + } + Err(anyhow!("timed out waiting for response {request_id}")) + } +} + +impl Drop for AppServerClient { + fn drop(&mut self) { + let _ = self.stdin.flush(); + let _ = self.child.kill(); + let _ = self.child.wait(); + } +} + +fn render_status(status: &Value) -> String { + let scene = &status["scene"]["scene"]; + let pressure = &status["pressure"]["pressure"]; + let reorient = &status["reorient"]["decision"]; + let result = &status["reorientResult"]; + let recommendation = &status["crrc"]["recommendation"]; + let coordinator = &status["coordinator"]; + let heartbeat = &status["heartbeat"]; + let face = &status["face"]; + let planning_response = &status["planning"]; + let planning_summary = &planning_response["summary"]; + let checkpoint = &scene["investigationCheckpoint"]; + let latest_heartbeat = heartbeat["latestEvent"].clone(); + let latest_face = face["latestArtifacts"] + .as_array() + .and_then(|items| items.first()) + .cloned() + .unwrap_or_else(|| json!({})); + + let mut lines = vec![ + "Epiphany MVP Status".to_string(), + format!("Thread: {}", text(&status["threadId"])), + format!( + "State: {} rev {} ({})", + text(&scene["stateStatus"]), + maybe(&scene["revision"], "none"), + text(&scene["source"]) + ), + String::new(), + "Recommendation".to_string(), + format!("- action: {}", text(&recommendation["action"])), + format!( + "- scene action: {}", + maybe(&recommendation["recommendedSceneAction"], "none") + ), + format!("- reason: {}", text(&recommendation["reason"])), + format!( + "- coordinator: {} ({})", + maybe(&coordinator["action"], "none"), + maybe(&coordinator["targetRole"], "none") + ), + String::new(), + "Continuity".to_string(), + format!( + "- pressure: {} ({}, prepare={})", + text(&pressure["level"]), + text(&pressure["status"]), + bool_text(&pressure["shouldPrepareCompaction"]) + ), + format!( + "- reorient: {} via {}", + text(&reorient["action"]), + list_text(&reorient["reasons"], "none") + ), + format!("- next: {}", text(&reorient["nextAction"])), + format!( + "- result: {} for {}", + text(&result["status"]), + text(&result["bindingId"]) + ), + String::new(), + "Heartbeat".to_string(), + format!( + "- status: {}; clock {}; rate {}", + maybe(&heartbeat["status"], "none"), + maybe(&heartbeat["sceneClock"], "none"), + maybe(&heartbeat["targetHeartbeatRate"], "none") + ), + format!( + "- latest: {} / {} / {}", + maybe(&latest_heartbeat["selectedRole"], "none"), + maybe(&latest_heartbeat["actionType"], "none"), + maybe(&latest_heartbeat["coordinatorAction"], "none") + ), + String::new(), + "Face".to_string(), + format!("- latest artifact: {}", maybe(&latest_face["name"], "none")), + format!( + "- latest content: {}", + maybe(&latest_face["content"], "none") + ), + String::new(), + "Planning".to_string(), + format!( + "- state: {} rev {}", + maybe(&planning_response["stateStatus"], "none"), + maybe(&planning_response["stateRevision"], "none") + ), + format!( + "- captures: {} (pending {}, github {})", + maybe(&planning_summary["captureCount"], "0"), + maybe(&planning_summary["pendingCaptureCount"], "0"), + maybe(&planning_summary["githubIssueCaptureCount"], "0") + ), + format!( + "- backlog: {} (ready {})", + maybe(&planning_summary["backlogItemCount"], "0"), + maybe(&planning_summary["readyBacklogItemCount"], "0") + ), + format!( + "- roadmap streams: {}; objective drafts: {} (draft {})", + maybe(&planning_summary["roadmapStreamCount"], "0"), + maybe(&planning_summary["objectiveDraftCount"], "0"), + maybe(&planning_summary["draftObjectiveCount"], "0") + ), + format!( + "- active objective: {}", + maybe(&planning_summary["activeObjective"], "none") + ), + format!("- note: {}", maybe(&planning_summary["note"], "none")), + String::new(), + "Role Lanes".to_string(), + ]; + if let Some(roles) = status["roles"]["roles"].as_array() { + for lane in roles { + lines.push(format!( + "- {}: {} ({}) - {}", + text(&lane["title"]), + text(&lane["status"]), + text(&lane["ownerRole"]), + text(&lane["note"]) + )); + } + } + lines.extend([String::new(), "Role Findings".to_string()]); + for (role_id, label) in [ + ("imagination", "Imagination / Planning"), + ("modeling", "Modeling / Checkpoint"), + ("verification", "Verification / Review"), + ] { + let role_result = &status["roleResults"][role_id]; + lines.push(format!( + "- {}: {} for {}", + label, + maybe(&role_result["status"], "none"), + maybe(&role_result["bindingId"], "none") + )); + } + lines.extend([ + String::new(), + "Checkpoint".to_string(), + format!("- id: {}", maybe(&checkpoint["checkpointId"], "none")), + format!( + "- disposition: {}", + maybe(&checkpoint["disposition"], "none") + ), + format!("- focus: {}", maybe(&checkpoint["focus"], "none")), + format!("- next: {}", maybe(&checkpoint["nextAction"], "none")), + String::new(), + "Jobs".to_string(), + ]); + if let Some(jobs) = status["jobs"]["jobs"].as_array() { + if jobs.is_empty() { + lines.push("- none".to_string()); + } else { + for job in jobs { + lines.push(format!( + "- {}: {} {}, {} [{}]", + text(&job["id"]), + text(&job["status"]), + text(&job["kind"]), + text(&job["ownerRole"]), + text(&job["scope"]) + )); + } + } + } + lines.extend([ + String::new(), + "Available Actions".to_string(), + format!("- {}", list_text(&scene["availableActions"], "none")), + ]); + format!("{}\n", lines.join("\n")) +} + +fn sanitize_for_operator(value: Value) -> Value { + match value { + Value::Object(map) => { + let mut sanitized = serde_json::Map::new(); + for (key, item) in map { + if SEALED_DIRECT_THOUGHT_KEYS + .iter() + .any(|candidate| candidate == &key) + { + sanitized.insert(key.clone(), sealed_direct_thought(&key, &item)); + } else { + sanitized.insert(key, sanitize_for_operator(item)); + } + } + Value::Object(sanitized) + } + Value::Array(items) => Value::Array(items.into_iter().map(sanitize_for_operator).collect()), + other => other, + } +} + +fn sealed_direct_thought(key: &str, value: &Value) -> Value { + let size = match value { + Value::String(text) => Some(text.chars().count()), + Value::Array(items) => Some(items.len()), + Value::Object(map) => Some(map.len()), + _ => None, + }; + let mut sealed = json!({ + "sealed": true, + "key": key, + "reason": "Operator-safe dogfood views use projected findings and audit receipts; direct agent transcript/thought payloads stay sealed unless the user explicitly requests forensic debugging.", + }); + if let Some(size) = size { + sealed["size"] = json!(size); + } + sealed +} + +fn write_transcript_telemetry(transcript: &Path, output: &Path) -> Result<()> { + let _ = native_json( + "epiphany-agent-telemetry", + &[ + &transcript.to_string_lossy(), + "--output", + &output.to_string_lossy(), + ], + )?; + Ok(()) +} + +fn native_json(bin_name: &str, args: &[&str]) -> Result { + let exe = PathBuf::from( + env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| DEFAULT_CARGO_TARGET_DIR.to_string()), + ) + .join("debug") + .join(format!("{bin_name}.exe")); + let output = if exe.exists() { + Command::new(&exe).args(args).output() + } else { + let mut command = Command::new("cargo"); + command + .arg("run") + .arg("--quiet") + .arg("--manifest-path") + .arg("epiphany-core/Cargo.toml") + .arg("--bin") + .arg(bin_name) + .arg("--") + .args(args) + .output() + } + .with_context(|| format!("failed to run {bin_name}"))?; + if !output.status.success() { + return Err(anyhow!( + "{} failed: {}{}", + bin_name, + String::from_utf8_lossy(&output.stderr), + String::from_utf8_lossy(&output.stdout) + )); + } + serde_json::from_slice(&output.stdout) + .with_context(|| format!("{bin_name} returned invalid JSON")) +} + +fn record(transcript: &Arc>, kind: &str, payload: &Value) { + if let Ok(mut file) = transcript.lock() { + let _ = writeln!(file, "{}", json!({kind: payload})); + } +} + +fn take_string(args: &mut impl Iterator, name: &str) -> Result { + args.next() + .ok_or_else(|| anyhow!("{name} requires a value")) +} + +fn take_path(args: &mut impl Iterator, name: &str) -> Result { + Ok(PathBuf::from(take_string(args, name)?)) +} + +fn home_dir() -> PathBuf { + env::var_os("USERPROFILE") + .or_else(|| env::var_os("HOME")) + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(".")) +} + +fn absolute_path(path: &Path) -> Result { + if path.is_absolute() { + Ok(path.to_path_buf()) + } else { + Ok(env::current_dir() + .context("failed to resolve current directory")? + .join(path)) + } +} + +fn text(value: &Value) -> String { + value + .as_str() + .map(ToString::to_string) + .unwrap_or_else(|| value.to_string()) +} + +fn maybe(value: &Value, fallback: &str) -> String { + if value.is_null() { + return fallback.to_string(); + } + if let Some(text) = value.as_str() { + if text.is_empty() { + return fallback.to_string(); + } + return text.to_string(); + } + value.to_string() +} + +fn bool_text(value: &Value) -> &'static str { + if value.as_bool().unwrap_or(false) { + "true" + } else { + "false" + } +} + +fn list_text(value: &Value, fallback: &str) -> String { + let Some(items) = value.as_array() else { + return fallback.to_string(); + }; + if items.is_empty() { + return fallback.to_string(); + } + items.iter().map(text).collect::>().join(", ") +} diff --git a/notes/epiphany-core-harness-surfaces.md b/notes/epiphany-core-harness-surfaces.md index 4be16c27..0ac4ebb6 100644 --- a/notes/epiphany-core-harness-surfaces.md +++ b/notes/epiphany-core-harness-surfaces.md @@ -61,7 +61,7 @@ The rule is: | `thread/epiphany/pressure` | read-only reflection | landed, live-smoked | Context-pressure gauge derived from token telemetry and recorded auto-compact/context limits. | | `thread/epiphany/reorient` | read-only policy reflection | landed, live-smoked | Bounded CRRC reorientation verdict derived from checkpoint, freshness, watcher, and pressure signals; returns resume vs regather without acting on the decision. | | `thread/epiphany/crrc` | read-only coordinator reflection | landed, live-smoked | Composes pressure, reorientation verdict, bound worker status/result, and available actions into one recommendation without launching, accepting, compacting, or mutating state. | -| `tools/epiphany_mvp_status.py` | operator CLI | landed, live-smoked | Starts or reads a thread through app-server and renders scene, pressure, reorient, jobs, result, CRRC recommendation, and MVP role lanes as a compact dogfood view. | +| `epiphany-mvp-status` | operator CLI | landed, live-smoked | Starts or reads a thread through app-server and renders scene, planning, pressure, reorient, jobs, result, CRRC recommendation, heartbeat, Face bubbles, and MVP role lanes as a compact dogfood view. | ## Write Authority @@ -240,7 +240,7 @@ worker, wait for worker, review result, accept result, or regather manually. It is not a scheduler, a hidden launch, a hidden accept, a compactor, or a second source of truth. The little tyrant remains theoretical, for now. -The first dogfood operator view is landed as `tools/epiphany_mvp_status.py`. +The first dogfood operator view is landed as native `epiphany-mvp-status`. It starts or reads one thread through the app-server and prints the scene, pressure, reorientation verdict, jobs, reorient result, and CRRC recommendation as a compact status report, with raw JSON available when needed. It also derives diff --git a/notes/epiphany-current-algorithmic-map.md b/notes/epiphany-current-algorithmic-map.md index c238535d..4930b7bd 100644 --- a/notes/epiphany-current-algorithmic-map.md +++ b/notes/epiphany-current-algorithmic-map.md @@ -38,7 +38,7 @@ The spine exists in more than twenty-five live paths: - role specialist launch/read-back/acceptance: app-server declares experimental `thread/epiphany/roleLaunch`, read-only `thread/epiphany/roleResult`, and write-gated `thread/epiphany/roleAccept`. `roleLaunch` accepts fixed Imagination/planning, modeling/checkpoint, and verification/review role ids, builds bounded specialist payloads from authoritative Epiphany state, routes them through the existing `epiphany_launch_job` job-control seam, and emits the normal `jobLaunch` state update for `jobBindings` only. `roleResult` resolves the fixed or requested binding through the current `agent_jobs` backend and projects structured output as a reviewable finding with verdict, summary, next safe move, inspected files, frontier/evidence ids, optional `statePatch`, optional `selfPatch`, coordinator `selfPersistence` review, raw result, and errors without mutation, promotion, scheduling, or hidden continuation. `selfPatch` is a Ghostlight-shaped lane-memory request, not project truth: the coordinator accepts only role-matched, bounded, evidence-grounded memory/goal/value/private-note mutations and refuses objectives, graphs, checkpoints, scratch, planning, jobs, authority, code edits, or raw transcripts with reasons. `roleAccept` accepts completed Imagination findings only when they contain a valid planning-only patch with draft Objective Draft review gates, accepts completed modeling/checkpoint findings only with a modeling-safe `statePatch`, applies accepted project patches through the existing Epiphany state validator, appends audit observation/evidence, and carries the same `selfPersistence` review. GUI/coordinator accept paths then apply accepted `selfPatch` requests to typed CultCache role dossiers in `state/agents.msgpack` through the native `epiphany-agent-memory-store` binary. Implementation stays owned by the main coding agent, verification output remains review-only, and reorientation stays on the CRRC-specific `reorientLaunch` / `reorientResult` / `reorientAccept` path. - shared prompt memory projection and role dossiers: the prompt config now carries `[shared].persistent_memory` in [epiphany_specialists.toml](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/prompts/epiphany_specialists.toml:2). App-server parses that block, prepends it through `epiphany_agent_prompt_with_memory`, and therefore projects the same Ghostlight-derived memory into fixed role specialist instructions, reorientation-worker instructions, coordinator notes, and CRRC checkpoint steering in [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:13260), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:13349), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:13594), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:13603), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:16119). The GUI-launched Hands implementation lane prepends the same memory in [epiphany_gui_action.py](E:/Projects/EpiphanyAgent/tools/epiphany_gui_action.py:52), while the base `` doctrine reinforces the Perfect Machine rule through [prompt.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/prompt.rs:31) and [epiphany_doctrine.md](E:/Projects/EpiphanyAgent/epiphany-core/src/prompts/epiphany_doctrine.md:1). Individual Ghostlight-shaped role dossiers now live in typed CultCache `state/agents.msgpack`, and the native `epiphany-agent-memory-store` binary validates, reviews, and applies bounded self-memory patches through the Rust store. - fixed-lane coordinator: app-server declares experimental read-only `thread/epiphany/coordinator`, composes role, pressure, reorient, CRRC, role result, and reorient result signals, and returns one current action without mutation. Review acceptance and implementation clearance are intentionally different: an accepted verification finding clears implementation only when the verifier verdict is `pass`; accepted `needs-evidence`, `needs-review`, or `fail` findings route back toward modeling/checkpoint strengthening before code work resumes. `regatherManually` is now fallback, not a supervisor-puppeteering excuse: completed unaccepted modeling findings stop at `reviewModelingResult`, modeling acceptance precedes verification, and fixed lane actions are considered before manual regather. -- MVP operator view: `tools/epiphany_mvp_status.py` launches app-server over stdio, starts or reads one thread, gathers scene, pressure, reorient, jobs, roles, Imagination/modeling/verification role results, reorient result, and CRRC responses, and renders them as text or JSON so the loop can be dogfooded without reading Rust. +- MVP operator view: native `epiphany-mvp-status` launches app-server over stdio, starts or reads one thread, gathers scene, planning, pressure, reorient, jobs, roles, Imagination/modeling/verification role results, reorient result, CRRC responses, heartbeat, and Face bubble artifacts, and renders them as text or JSON so the loop can be dogfooded without Python owning the product surface. - MVP dogfood runner: `tools/epiphany_mvp_dogfood.py` launches app-server over stdio, drives a bounded state/role/CRRC/reorientation loop, and writes operator-safe JSON/text artifacts plus sealed transcript diagnostics under `.epiphany-dogfood/mvp-loop` so a human can audit the run through projections and compare it against a vanilla Codex reference. - Agent function/API telemetry: native `epiphany-agent-telemetry` mechanically parses sealed AppServerClient JSONL transcripts into operator-safe telemetry with method counts, request shape, job/status/path fields, and visible function/tool names while sealing text, turn items, raw results, and direct agent messages. - reorientation launch: app-server now declares experimental `thread/epiphany/reorientLaunch`, requires a loaded thread plus durable investigation checkpoint, consumes the live reorientation verdict, and launches one fixed `reorient-worker` binding through the existing job-control seam with explicit resume-versus-regather scope, checkpoint-derived payload, and structured-output expectations instead of pretending CRRC suddenly grew a scheduler in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:370), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4575), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4519). diff --git a/notes/epiphany-fork-implementation-plan.md b/notes/epiphany-fork-implementation-plan.md index 37f367c8..bcb86402 100644 --- a/notes/epiphany-fork-implementation-plan.md +++ b/notes/epiphany-fork-implementation-plan.md @@ -82,7 +82,7 @@ The landed machine now has: - read-only Phase 6 role ownership through `thread/epiphany/roles`, projecting implementation, modeling/checkpoint, verification/review, and reorientation lanes from typed state plus jobs/CRRC/result signals - explicit Phase 6 role launch/read-back through `thread/epiphany/roleLaunch` and read-only `thread/epiphany/roleResult`, limited to fixed modeling/checkpoint and verification/review templates over the existing job-control seam - review-gated Phase 6 modeling acceptance through `thread/epiphany/roleAccept`, limited to completed modeling/checkpoint `statePatch` findings that apply graph/frontier/checkpoint/scratch/investigation-checkpoint changes through the existing state validator -- first Phase 6 dogfood operator view through `tools/epiphany_mvp_status.py` +- first Phase 6 dogfood operator view through native `epiphany-mvp-status` - first auditable Phase 6 dogfood runner through `tools/epiphany_mvp_dogfood.py`, producing local status snapshots, sealed app-server transcript diagnostics, operator-safe final status artifacts, truthful vanilla-reference output, and comparison notes - first auditable Phase 6 fixed-lane coordinator runner through `tools/epiphany_mvp_coordinator.py`, producing coordinator summary, JSONL steps, rendered snapshots, transcript, stderr, and final next-action artifacts while keeping semantic findings review-gated by default - first auditable Phase 6 live-specialist runner through `tools/epiphany_mvp_live_specialist.py`, proving `roleLaunch -> agent_jobs worker -> report_agent_job_result -> roleResult` without manual backend completion @@ -211,7 +211,7 @@ coordinator, first harness-native role ownership, and first fixed role specialist launch/result blockers are now landed as `thread/epiphany/reorientResult`, `thread/epiphany/reorientAccept`, -`thread/epiphany/crrc`, `tools/epiphany_mvp_status.py`, +`thread/epiphany/crrc`, native `epiphany-mvp-status`, `thread/epiphany/coordinator`, `tools/epiphany_mvp_coordinator.py`, and `thread/epiphany/roles`, `thread/epiphany/roleLaunch`, `thread/epiphany/roleResult`, and `thread/epiphany/roleAccept`. A human can now ask the harness what it believes, @@ -363,7 +363,7 @@ Implementation slices: 1. **Read-only shell**: scaffold Tauri/React, connect to app-server through the existing MVP status bridge, render the same data as - `tools/epiphany_mvp_status.py`, and provide artifact bundle links. This + native `epiphany-mvp-status`, and provide artifact bundle links. This slice landed first under `apps/epiphany-gui` and is now extracted to `E:\Projects\EpiphanyAquarium`. 2. **Bounded operator actions**: status snapshot, coordinator-plan, Unity diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 61679e62..7fa073f0 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -32,7 +32,7 @@ remembers doctrine; the branch remembers the blade. - Do not copy exact branch or HEAD from this note. Run `git status --short --branch` and `git log --oneline -5`. - Phase 1 through Phase 5 are complete enough. - Phase 6 has read-only `thread/epiphany/scene`, `thread/epiphany/jobs`, `thread/epiphany/roles`, `thread/epiphany/freshness`, `thread/epiphany/context`, `thread/epiphany/graphQuery`, `thread/epiphany/planning`, `thread/epiphany/pressure`, `thread/epiphany/reorient`, `thread/epiphany/crrc`, `thread/epiphany/coordinator`, `thread/epiphany/reorientResult`, and `thread/epiphany/roleResult`; durable `jobBindings` now act as a thin Epiphany-owned launcher seam with launcher id, authority scope, and backend kind/job id, `thread/epiphany/jobLaunch` / `thread/epiphany/jobInterrupt` / `thread/epiphany/roleLaunch` now provide explicit bounded authority over that seam, jobs can overlay it onto live runtime `agent_jobs` progress for loaded threads, and `thread/epiphany/jobsUpdated` now translates live `agent_job_progress:{json}` background events into changed launcher-bound notifications without polling or scheduling. Freshness carries watcher-backed invalidation inputs, graphQuery traverses authoritative typed graph neighborhoods and path/symbol matches without mutation, planning projects typed captures/backlog/roadmap/objective drafts without adopting work, roles project implementation/imagination/modeling/verification/reorientation ownership from existing signals without becoming a scheduler, `roleLaunch` can launch fixed Imagination/planning, modeling/checkpoint, or verification/review specialists, `roleResult` reads those outputs back as reviewable findings without mutation, `thread/epiphany/roleAccept` can apply reviewed Imagination planning-only `statePatch` findings and reviewed modeling/checkpoint `statePatch` findings through the existing state validator, reorient turns checkpoint plus freshness/pressure/watcher signals into a read-only resume-versus-regather verdict, `thread/epiphany/reorientLaunch` is the explicit runtime consumer over that verdict, `thread/epiphany/reorientResult` reads the launched worker output back as a reviewable finding without mutation or promotion, `thread/epiphany/reorientAccept` explicitly banks completed findings into accepted observation/evidence plus optional scratch/checkpoint state, `thread/epiphany/crrc` recommends the next explicit CRRC action without launching, accepting, compacting, scheduling, or mutating, and `thread/epiphany/coordinator` composes those signals into a fixed-lane MVP action recommendation without becoming a writer. -- `tools/epiphany_mvp_status.py` is the first dogfood operator view. It starts or reads a thread through app-server and prints scene, planning, pressure, reorient, jobs, roles, Imagination/modeling/verification role result read-backs, reorient result, and CRRC recommendation as text or JSON. +- Native `epiphany-mvp-status` is the first dogfood operator view. It starts or reads a thread through app-server and prints scene, planning, pressure, reorient, jobs, roles, Imagination/modeling/verification role result read-backs, reorient result, heartbeat, Face bubbles, and CRRC recommendation as text or JSON. The Python status module is now compatibility tissue for still-Python coordinator/GUI drivers, not the smoked product path. - `tools/epiphany_mvp_coordinator.py` is the first auditable fixed-lane coordinator runner. It starts or reads a thread through app-server, follows the harness-native coordinator action, can auto-launch modeling, verification, or reorient-worker jobs, keeps semantic findings review-gated by default, and writes summary, steps, rendered snapshots, transcript, stderr, and final next-action artifacts under `.epiphany-dogfood/coordinator` or a caller-provided artifact directory. - Native CRRC automation is now landed only at turn-complete safe boundaries. It may submit `Op::Compact` for coordinator-approved `compactRehydrateReorient` or for a successful pre-compaction checkpoint intervention's pending compact handoff, and it may launch the fixed `reorient-worker` for coordinator-approved `launchReorientWorker`. It does not auto-launch Imagination/modeling/verification, accept findings, promote evidence, edit implementation code, or keep going after reviewable semantic output. - Pre-compaction checkpoint intervention is now landed. On token-count events for loaded Epiphany threads, when current context usage reaches 80% of the active auto-compact/context limit, the harness steers the active turn once with a CRRC checkpoint directive so the agent banks working context before compaction/reorientation. Pressure ignores cumulative token spend; cumulative-only telemetry reports unknown instead of yelling. A successful steer now latches a turn-scoped compact handoff that is consumed at clean turn completion, preventing the brake from decaying into another implementation turn. This is still bounded steering plus compaction handoff, not automatic semantic acceptance, a broad scheduler, or implementation continuation. @@ -46,7 +46,7 @@ remembers doctrine; the branch remembers the blade. - The role self-memory persistence pass is native now. Each lane has a Ghostlight-shaped typed dossier in `state/agents.msgpack`, and specialists may return optional `selfPatch` requests beside their normal role result. `roleResult`/`roleAccept` project coordinator review as `selfPersistence`: accepted requests are role-matched, bounded lane memory/goal/value/private-note mutations; refused requests explain wrong role, project-truth smuggling, authority grabs, bloat, missing reason, or malformed records. GUI/coordinator accept paths apply accepted `selfPatch` requests through the native `epiphany-agent-memory-store` binary; project truth still belongs only in `EpiphanyThreadState`. - The heartbeat initiative pass is landed as a bounded tool seam. `state/agent-heartbeats.msgpack` tracks Self, Face, Imagination, Eyes, Body, Hands, Soul, and Life as Ghostlight-style initiative participants with speed, next-ready time, reaction bias, interrupt threshold, load, status, constraints, history, and pending turns through `epiphany-core::EpiphanyHeartbeatStateEntry` and the native `epiphany-heartbeat-store` binary. Tick/status/complete and idle rumination selfPatch application now run through Rust/CultCache directly. JSON heartbeat state and Python wrapper state are gone; general CultCache schema sync, polyglot domain loading, and debug display tools belong in CultLib. This is a callable scheduler seam, not yet an always-on daemon; a heart valve, not a whole circulatory god. - The Face public-surface pass is landed as a bounded lane. Face's role dossier lives in `state/agents.msgpack`; `epiphany_specialists.toml` gives it a VoidBot-heartbeat-derived prompt stripped of moderation authority; `state/face-discord.toml` and the native `epiphany-face-discord` binary enforce that Face may interact only through #aquarium. Missing channel id or token writes candidate chat artifacts instead of posting elsewhere. -- The heartbeat/Face operator API pass is landed. Native `epiphany-heartbeat-store status` returns machine-readable initiative state plus CultCache store presence, native `epiphany-face-discord bubble` writes Discord-independent `epiphany.face_bubble.v0` artifacts, `tools/epiphany_mvp_status.py` includes `heartbeat` and `face` blocks, and `tools/epiphany_gui_action.py` exposes `heartbeatStatus`, `runHeartbeat`, and `faceBubble`. `runHeartbeat` advances one slot and emits a Face bubble so Aquarium can show chat even before Discord posting is configured. +- The heartbeat/Face operator API pass is landed. Native `epiphany-heartbeat-store status` returns machine-readable initiative state plus CultCache store presence, native `epiphany-face-discord bubble` writes Discord-independent `epiphany.face_bubble.v0` artifacts, native `epiphany-mvp-status` includes `heartbeat` and `face` blocks, and `tools/epiphany_gui_action.py` exposes `heartbeatStatus`, `runHeartbeat`, and `faceBubble`. `runHeartbeat` advances one slot and emits a Face bubble so Aquarium can show chat even before Discord posting is configured. - The Aetheria dogfood run has a contamination scar. The supervising Codex session directly edited and committed target-repo work on `E:\Projects\Aetheria-Economy` instead of only driving Epiphany lanes. Treat those Aetheria commits as supervisor-seeded implementation, not clean evidence that Epiphany coordinated the work. Future dogfood must run through the GUI/coordinator/fixed role lanes with auditable artifacts unless the user explicitly authorizes an operator intervention. Remember the sunburn: do not stare into the worker's objective until you become it. - The dogfood quarantine now has a direct-thought boundary. The supervisor may read coordinator actions, role/reorient statuses, structured finding summaries, reviewed state patches, rendered status snapshots, and artifact manifests. It must not read raw worker transcripts, direct worker messages, full turn logs, or `rawResult` payloads during normal dogfood. Those artifacts are sealed black reliquaries for explicit forensic debugging only. - Native `epiphany-agent-telemetry` is the safe instrument panel for sealed runs. Status/coordinator/GUI/dogfood/live-specialist tools generate telemetry JSON from sealed transcripts that preserves method names, call shape, job/status/path counts, and any visible function/tool names while sealing text, direct messages, and raw results. @@ -467,7 +467,7 @@ Live `thread/epiphany/scene`, `thread/epiphany/jobs`, `thread/epiphany/roles`, `thread/epiphany/freshness`, `thread/epiphany/context`, `thread/epiphany/graphQuery`, `thread/epiphany/planning`, `thread/epiphany/pressure`, `thread/epiphany/reorient`, `thread/epiphany/crrc`, `thread/epiphany/coordinator`, -`tools/epiphany_mvp_status.py`, `tools/epiphany_mvp_coordinator.py`, and +native `epiphany-mvp-status`, `tools/epiphany_mvp_coordinator.py`, and `tools/epiphany_phase6_graph_query_smoke.py` / `tools/epiphany_phase6_planning_smoke.py` / `tools/epiphany_gui_planning_smoke.py` smokes are now guardrails, not the next organs. diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index c8272a27..7b3683b8 100644 Binary files a/state/ledgers.msgpack and b/state/ledgers.msgpack differ diff --git a/state/map.yaml b/state/map.yaml index 7704cd29..c42f0995 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -110,7 +110,7 @@ accepted_design: - `tools/epiphany_mvp_coordinator.py` is the first auditable coordinator runner: it drives the fixed single-user lanes through app-server APIs, may auto-launch modeling, verification, or reorient-worker jobs, keeps semantic findings review-gated by default, and writes coordinator summaries, JSONL steps, rendered snapshots, transcript, stderr, and final next-action artifacts. - Limited native CRRC automation is now wired only at turn-complete safe boundaries: it can submit a compact operation when the read-only coordinator recommends `compactRehydrateReorient` or when a successful pre-compaction checkpoint intervention left a pending compact handoff for the completed turn, and it can launch the fixed `reorient-worker` when the coordinator recommends `launchReorientWorker`. It does not auto-launch modeling or verification, auto-accept findings, promote evidence, edit implementation code, or continue after reviewable semantic results; the rite may carry the stretcher, not declare the patient healed. - Pre-compaction checkpoint intervention is now wired at token-count boundaries: when a loaded Epiphany thread crosses the existing `shouldPrepareCompaction` pressure threshold during an active turn, the harness steers the active agent once for that turn with a bounded CRRC directive to bank scratch/checkpoint/map/evidence before compaction/reorientation. A successful steer marks a turn-scoped pending compact handoff consumed at clean turn completion; this is banking the fire before the dark, not semantic acceptance, broad scheduling, or automatic implementation continuation. - - `tools/epiphany_mvp_status.py` is the first dogfood operator view: it starts or reads a thread through app-server and renders scene, planning, pressure, reorient, jobs, role result read-backs, reorient result, CRRC recommendation, and MVP role lanes as text or JSON without becoming a state store. + - Native `epiphany-mvp-status` is the first dogfood operator view: it starts or reads a thread through app-server and renders scene, planning, pressure, reorient, jobs, role result read-backs, reorient result, CRRC recommendation, heartbeat, Face bubbles, and MVP role lanes as text or JSON without becoming a state store. The Python status module remains compatibility tissue for still-Python coordinator/GUI drivers until those organs are cut over. - `thread/epiphany/roles` is the first harness-native role ownership surface: it projects implementation, Imagination/planning, modeling/checkpoint, verification/review, and reorientation lanes from authoritative typed state, bound jobs, pressure/reorient/CRRC, and worker result status without launching, scheduling, accepting, compacting, or mutating state. - `thread/epiphany/roleLaunch` and `thread/epiphany/roleResult` are the first explicit role-scoped specialist templates: Imagination/planning, modeling/checkpoint, and verification/review can be launched through fixed bindings over the existing job-control seam and read back as reviewable findings, while implementation stays owned by the main coding agent and reorientation stays on the CRRC-specific launch/result path. - `thread/epiphany/roleAccept` is the first review-gated role finding acceptance path: it accepts completed Imagination/planning findings only when they return a valid planning-only `statePatch` with draft Objective Draft review gates, accepts completed modeling/checkpoint findings only with a modeling-safe `statePatch`, appends audit observation/evidence, and rejects verification, implementation, reorientation, job binding, objective adoption, or broad authority fields. @@ -138,8 +138,8 @@ accepted_design: - VoidBot's current routine is the reference for the next Epiphany heartbeat/state substrate: work, rumination, and sleep turns; completion-gated cooldown; typed CultCache state; Qdrant-backed memory vectors; resonance/incubation/dream state; and Aquarium-visible internals. The first MessagePack canonical state slice is now the heartbeat store through CultCache-compatible typed Rust state. See `notes/voidbot-routine-adoption-plan.md`. - The Rust CultCache-compatible contract now lives in dedicated upstream `https://github.com/GameCult/cultcache-rs` and is vendored as `vendor/cultcache-rs`. Epiphany should consume it as the canonical typed MessagePack persistence seam instead of growing an embedded local copy like an unsanctioned organ. - `state/face-discord.toml` and the native `epiphany-face-discord` binary are the first Face Discord boundary. Face may interact only through #aquarium; missing or mismatched channel configuration produces candidate chat artifacts instead of posting elsewhere. - - The native `epiphany-heartbeat-store status`, the `heartbeat` block in `tools/epiphany_mvp_status.py`, and GUI actions `heartbeatStatus` / `runHeartbeat` are the first operator API surface for heartbeat initiative state and outputs. `runHeartbeat` advances one slot and emits a Discord-independent Face bubble artifact. Status now reflects the backing CultCache store so Aquarium can see whether the typed heartbeat memory is present. - - The native `epiphany-face-discord bubble`, the `face` block in `tools/epiphany_mvp_status.py`, and GUI action `faceBubble` are the first Aquarium bubble surface. Face can open a local chat bubble artifact even when Discord channel id/token configuration is absent. + - The native `epiphany-heartbeat-store status`, the `heartbeat` block in native `epiphany-mvp-status`, and GUI actions `heartbeatStatus` / `runHeartbeat` are the first operator API surface for heartbeat initiative state and outputs. `runHeartbeat` advances one slot and emits a Discord-independent Face bubble artifact. Status now reflects the backing CultCache store so Aquarium can see whether the typed heartbeat memory is present. + - The native `epiphany-face-discord bubble`, the `face` block in native `epiphany-mvp-status`, and GUI action `faceBubble` are the first Aquarium bubble surface. Face can open a local chat bubble artifact even when Discord channel id/token configuration is absent. - `thread/epiphany/roleResult` and `thread/epiphany/roleAccept` now project optional specialist `selfPatch` requests with coordinator review through `selfPersistence`: good mutations are role-matched, bounded, evidence-grounded lane memory/goal/value/private-note updates; project truth, authority, code edits, graphs, checkpoints, planning, scratch, jobs, and raw transcripts are refused with reasons. GUI/coordinator acceptance paths apply accepted `selfPatch` requests through the native `epiphany-agent-memory-store` binary. - The base Epiphany prompt now keeps Codex's useful harness DNA explicitly: AGENTS scope, concise progress updates, plan discipline, scoped edits, non-destructive filesystem/git behavior, focused validation, and concise honest finals remain first-class rather than overwritten by Epiphany metaphor. Good old scars are holy when they still protect the machine. - Greenspun-shaped invention is now a named prompt-level guardrail. Before building a parser, scheduler, renderer, protocol, storage layer, security mechanism, workflow engine, or algorithm, agents must perform or request a bounded Eyes/research scout rite and record why existing work does or does not fit. diff --git a/tools/epiphany_mvp_status_smoke.py b/tools/epiphany_mvp_status_smoke.py index 2350cf8c..e4505232 100644 --- a/tools/epiphany_mvp_status_smoke.py +++ b/tools/epiphany_mvp_status_smoke.py @@ -2,12 +2,12 @@ from __future__ import annotations import argparse import json +import os from pathlib import Path +import subprocess from epiphany_mvp_status import DEFAULT_APP_SERVER from epiphany_mvp_status import ROOT -from epiphany_mvp_status import render_status -from epiphany_mvp_status import run from epiphany_phase5_smoke import require from epiphany_phase5_smoke import reset_smoke_paths @@ -16,6 +16,27 @@ DEFAULT_CODEX_HOME = ROOT / ".epiphany-smoke" / "mvp-status-codex-home" DEFAULT_RESULT = ROOT / ".epiphany-smoke" / "mvp-status-smoke-result.json" DEFAULT_TRANSCRIPT = ROOT / ".epiphany-smoke" / "mvp-status-smoke-transcript.jsonl" DEFAULT_STDERR = ROOT / ".epiphany-smoke" / "mvp-status-smoke-server.stderr.log" +DEFAULT_RENDERED = ROOT / ".epiphany-smoke" / "mvp-status-smoke-rendered.txt" + + +def native_status_exe() -> Path: + exe = Path(os.environ.get("CARGO_TARGET_DIR", r"C:\Users\Meta\.cargo-target-codex")) / "debug" / "epiphany-mvp-status.exe" + if exe.exists(): + return exe + subprocess.run( + [ + "cargo", + "build", + "--manifest-path", + str(ROOT / "epiphany-core" / "Cargo.toml"), + "--bin", + "epiphany-mvp-status", + ], + cwd=ROOT, + check=True, + ) + require(exe.exists(), f"native status binary was not built: {exe}") + return exe def run_smoke(args: argparse.Namespace) -> dict[str, object]: @@ -28,20 +49,57 @@ def run_smoke(args: argparse.Namespace) -> dict[str, object]: transcript_path = args.transcript.resolve() stderr_path = args.stderr.resolve() reset_smoke_paths(codex_home, result_path, transcript_path, stderr_path) + rendered_path = args.rendered.resolve() + if rendered_path.exists(): + rendered_path.unlink() - status = run( - argparse.Namespace( - app_server=app_server, - codex_home=codex_home, - thread_id=None, - cwd=ROOT, - ephemeral=True, - result=None, - transcript=transcript_path, - stderr=stderr_path, - ) + exe = native_status_exe() + completed = subprocess.run( + [ + str(exe), + "--app-server", + str(app_server), + "--codex-home", + str(codex_home), + "--cwd", + str(ROOT), + "--transcript", + str(transcript_path), + "--stderr", + str(stderr_path), + "--result", + str(result_path), + "--json", + ], + cwd=ROOT, + text=True, + capture_output=True, + check=False, ) - rendered = render_status(status) + require(completed.returncode == 0, completed.stderr or completed.stdout) + status = json.loads(completed.stdout) + rendered_completed = subprocess.run( + [ + str(exe), + "--app-server", + str(app_server), + "--codex-home", + str(codex_home), + "--cwd", + str(ROOT), + "--transcript", + str(transcript_path.with_name("mvp-status-smoke-render-transcript.jsonl")), + "--stderr", + str(stderr_path.with_name("mvp-status-smoke-render-server.stderr.log")), + ], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + require(rendered_completed.returncode == 0, rendered_completed.stderr or rendered_completed.stdout) + rendered = rendered_completed.stdout + rendered_path.write_text(rendered, encoding="utf-8") require( status["scene"]["scene"]["stateStatus"] == "missing", @@ -139,6 +197,7 @@ def main() -> int: parser.add_argument("--result", type=Path, default=DEFAULT_RESULT) parser.add_argument("--transcript", type=Path, default=DEFAULT_TRANSCRIPT) parser.add_argument("--stderr", type=Path, default=DEFAULT_STDERR) + parser.add_argument("--rendered", type=Path, default=DEFAULT_RENDERED) args = parser.parse_args() result = run_smoke(args) print(json.dumps(result, indent=2, ensure_ascii=False))