Add Epiphany GUI operator shell

This commit is contained in:
Meta Vens 2026-04-29 16:19:37 +01:00
commit a2c44a52a3
23 changed files with 8188 additions and 17 deletions

5
.gitignore vendored
View file

@ -2,5 +2,10 @@ __pycache__/
*.pyc
epiphany-core/target/
epiphany-core/Cargo.lock
apps/epiphany-gui/node_modules/
apps/epiphany-gui/dist/
apps/epiphany-gui/src-tauri/gen/
apps/epiphany-gui/src-tauri/target/
.epiphany-gui/
.epiphany-smoke/
.epiphany-dogfood/

View file

@ -0,0 +1,30 @@
# Epiphany GUI
Local Tauri v2 + React operator console for the Epiphany MVP.
This app is not a source of truth. It reflects the existing app-server and
artifact surfaces so the operator can inspect coordinator, CRRC, role, job, and
artifact state without working through a terminal.
Current slice:
- read-only operator console
- calls `tools/epiphany_mvp_status.py --json` through a Tauri command
- renders coordinator recommendation, pressure, CRRC/reorientation status, role
lanes, role findings, jobs, and artifact bundles
Run from this directory:
```powershell
npm install
npm run dev
npm run tauri dev
```
The status bridge expects the local app-server binary used by the existing
smoke tools. Build it from `vendor/codex/codex-rs` if needed:
```powershell
$env:CARGO_TARGET_DIR='C:\Users\Meta\.cargo-target-codex'
cargo build -p codex-app-server
```

View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Epiphany Operator</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

2139
apps/epiphany-gui/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,26 @@
{
"name": "epiphany-gui",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite --host 127.0.0.1 --port 1420",
"build": "tsc && vite build",
"preview": "vite preview --host 127.0.0.1 --port 1421",
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "^2.0.0",
"lucide-react": "^0.468.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@tauri-apps/cli": "^2.0.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.6.3",
"vite": "^6.0.1"
}
}

4967
apps/epiphany-gui/src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,18 @@
[package]
name = "epiphany-gui"
version = "0.1.0"
description = "Epiphany local operator console"
authors = ["GameCult"]
edition = "2021"
[lib]
name = "epiphany_gui_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri = { version = "2", features = [] }

View file

@ -0,0 +1,3 @@
fn main() {
tauri_build::build();
}

View file

@ -0,0 +1,7 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default permissions for the Epiphany operator console.",
"windows": ["main"],
"permissions": ["core:default"]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,207 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct StatusRequest {
thread_id: Option<String>,
cwd: Option<String>,
codex_home: Option<String>,
app_server: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct ArtifactBundle {
name: String,
path: String,
files: Vec<String>,
summary_path: Option<String>,
final_status_path: Option<String>,
comparison_path: Option<String>,
modified_millis: Option<u128>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct OperatorSnapshot {
generated_at: String,
repo_root: String,
status: Value,
artifacts: Vec<ArtifactBundle>,
}
#[tauri::command]
fn load_operator_snapshot(request: Option<StatusRequest>) -> Result<OperatorSnapshot, String> {
let repo_root = repo_root()?;
let status = load_status(&repo_root, request.unwrap_or_default())?;
let artifacts = list_artifacts(&repo_root)?;
Ok(OperatorSnapshot {
generated_at: unix_millis().to_string(),
repo_root: repo_root.display().to_string(),
status,
artifacts,
})
}
impl Default for StatusRequest {
fn default() -> Self {
Self {
thread_id: None,
cwd: None,
codex_home: None,
app_server: None,
}
}
}
fn load_status(repo_root: &Path, request: StatusRequest) -> Result<Value, String> {
let python = find_python()?;
let status_script = repo_root.join("tools").join("epiphany_mvp_status.py");
let workspace = request
.cwd
.map(PathBuf::from)
.unwrap_or_else(|| repo_root.to_path_buf());
let codex_home = request
.codex_home
.map(PathBuf::from)
.unwrap_or_else(|| repo_root.join(".epiphany-gui").join("codex-home"));
let transcript = repo_root
.join(".epiphany-gui")
.join("status-transcript.jsonl");
let stderr = repo_root
.join(".epiphany-gui")
.join("status-server.stderr.log");
let mut command = Command::new(python);
command
.current_dir(repo_root)
.arg(status_script)
.arg("--json")
.arg("--cwd")
.arg(workspace)
.arg("--codex-home")
.arg(codex_home)
.arg("--transcript")
.arg(transcript)
.arg("--stderr")
.arg(stderr);
if let Some(thread_id) = request.thread_id {
command.arg("--thread-id").arg(thread_id);
}
if let Some(app_server) = request.app_server {
command.arg("--app-server").arg(app_server);
}
let output = command
.output()
.map_err(|err| format!("failed to run Epiphany status bridge: {err}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!(
"Epiphany status bridge exited with {}: {}",
output.status, stderr
));
}
serde_json::from_slice(&output.stdout)
.map_err(|err| format!("failed to parse Epiphany status JSON: {err}"))
}
fn list_artifacts(repo_root: &Path) -> Result<Vec<ArtifactBundle>, String> {
let root = repo_root.join(".epiphany-dogfood");
if !root.exists() {
return Ok(Vec::new());
}
let mut bundles = Vec::new();
for entry in fs::read_dir(&root).map_err(|err| format!("failed to read artifacts: {err}"))? {
let entry = entry.map_err(|err| format!("failed to read artifact entry: {err}"))?;
let path = entry.path();
if !path.is_dir() {
continue;
}
let mut files = Vec::new();
for file in
fs::read_dir(&path).map_err(|err| format!("failed to read artifact bundle: {err}"))?
{
let file = file.map_err(|err| format!("failed to read artifact file: {err}"))?;
if file.path().is_file() {
files.push(file.file_name().to_string_lossy().to_string());
}
}
files.sort();
let modified_millis = entry
.metadata()
.ok()
.and_then(|metadata| metadata.modified().ok())
.and_then(system_time_millis);
bundles.push(ArtifactBundle {
name: entry.file_name().to_string_lossy().to_string(),
path: path.display().to_string(),
summary_path: existing_path(&path, "epiphany-dogfood-summary.json"),
final_status_path: existing_path(&path, "epiphany-final-status.json"),
comparison_path: existing_path(&path, "comparison.md"),
files,
modified_millis,
});
}
bundles.sort_by(|a, b| b.modified_millis.cmp(&a.modified_millis));
Ok(bundles)
}
fn existing_path(root: &Path, name: &str) -> Option<String> {
let path = root.join(name);
path.exists().then(|| path.display().to_string())
}
fn repo_root() -> Result<PathBuf, String> {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
manifest_dir
.parent()
.and_then(Path::parent)
.and_then(Path::parent)
.map(Path::to_path_buf)
.ok_or_else(|| "failed to derive repository root from CARGO_MANIFEST_DIR".to_string())
}
fn find_python() -> Result<PathBuf, String> {
if let Ok(value) = std::env::var("EPIPHANY_PYTHON") {
let path = PathBuf::from(value);
if path.exists() {
return Ok(path);
}
}
let bundled = PathBuf::from(
r"C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe",
);
if bundled.exists() {
return Ok(bundled);
}
Ok(PathBuf::from("python"))
}
fn unix_millis() -> u128 {
system_time_millis(SystemTime::now()).unwrap_or_default()
}
fn system_time_millis(value: SystemTime) -> Option<u128> {
value
.duration_since(UNIX_EPOCH)
.ok()
.map(|duration| duration.as_millis())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![load_operator_snapshot])
.run(tauri::generate_context!())
.expect("error while running Epiphany operator");
}

View file

@ -0,0 +1,3 @@
fn main() {
epiphany_gui_lib::run();
}

View file

@ -0,0 +1,30 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Epiphany",
"version": "0.1.0",
"identifier": "dev.epiphany.operator",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://127.0.0.1:1420",
"beforeBuildCommand": "npm run build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "Epiphany Operator",
"width": 1280,
"height": 860,
"minWidth": 960,
"minHeight": 640
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": false,
"targets": "all"
}
}

View file

@ -0,0 +1,253 @@
import { invoke } from "@tauri-apps/api/core";
import { AlertTriangle, BriefcaseBusiness, ClipboardCheck, Database, FileText, GitBranch, RefreshCw } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import type { ArtifactBundle, OperatorSnapshot, StatusRequest } from "./types";
const roleOrder = ["implementation", "modeling", "verification", "reorientation"];
function text(value: unknown, fallback = "none"): string {
if (value === null || value === undefined || value === "") {
return fallback;
}
if (typeof value === "string") {
return value;
}
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
return fallback;
}
function listText(value: unknown, fallback = "none"): string {
return Array.isArray(value) && value.length > 0 ? value.map(String).join(", ") : fallback;
}
function statusClass(value: unknown): string {
const lower = text(value).toLowerCase();
if (lower.includes("blocked") || lower.includes("critical") || lower.includes("regather")) return "danger";
if (lower.includes("needed") || lower.includes("review") || lower.includes("prepare") || lower.includes("high")) return "warn";
if (lower.includes("completed") || lower.includes("ready") || lower.includes("continue") || lower.includes("pass")) return "ok";
return "neutral";
}
function useSnapshot() {
const [snapshot, setSnapshot] = useState<OperatorSnapshot | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [request, setRequest] = useState<StatusRequest>({});
async function refresh(nextRequest = request) {
setLoading(true);
setError(null);
try {
const result = await invoke<OperatorSnapshot>("load_operator_snapshot", { request: nextRequest });
setSnapshot(result);
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setLoading(false);
}
}
useEffect(() => {
void refresh({});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return { snapshot, loading, error, request, setRequest, refresh };
}
export function App() {
const { snapshot, loading, error, request, setRequest, refresh } = useSnapshot();
const status = snapshot?.status;
const scene = status?.scene?.scene ?? {};
const pressure = status?.pressure?.pressure ?? {};
const reorient = status?.reorient?.decision ?? {};
const crrc = status?.crrc?.recommendation ?? {};
const coordinator = status?.coordinator ?? {};
const roles = useMemo(() => {
const lanes = status?.roles?.roles;
if (!Array.isArray(lanes)) return [];
return [...lanes].sort((a, b) => roleOrder.indexOf(text(a.id)) - roleOrder.indexOf(text(b.id)));
}, [status]);
const jobs: any[] = Array.isArray(status?.jobs?.jobs) ? status.jobs.jobs : [];
const roleResults = status?.roleResults ?? {};
const reorientResult = status?.reorientResult ?? {};
return (
<main className="shell">
<header className="topbar">
<div>
<p className="eyebrow">Epiphany MVP</p>
<h1>Operator Console</h1>
</div>
<button className="primaryButton" onClick={() => void refresh()} disabled={loading} title="Refresh status">
<RefreshCw size={16} aria-hidden="true" />
{loading ? "Refreshing" : "Refresh"}
</button>
</header>
<section className="controls" aria-label="Connection">
<label>
Thread ID
<input
placeholder="start ephemeral status thread"
value={request.threadId ?? ""}
onChange={(event) => setRequest({ ...request, threadId: event.target.value || undefined })}
/>
</label>
<label>
Workspace
<input
placeholder={snapshot?.repoRoot ?? "repo root"}
value={request.cwd ?? ""}
onChange={(event) => setRequest({ ...request, cwd: event.target.value || undefined })}
/>
</label>
</section>
{error && (
<section className="notice dangerNotice" role="alert">
<AlertTriangle size={18} aria-hidden="true" />
<span>{error}</span>
</section>
)}
<section className="statusGrid" aria-label="Coordinator summary">
<Panel title="Recommendation" icon={<ClipboardCheck size={18} />}>
<div className={`actionBanner ${statusClass(coordinator.action ?? crrc.action)}`}>
<strong>{text(coordinator.action ?? crrc.action, "unknown")}</strong>
<span>{text(coordinator.targetRole ?? crrc.recommendedSceneAction)}</span>
</div>
<p className="reason">{text(coordinator.reason ?? crrc.reason, "No recommendation loaded yet.")}</p>
<dl className="facts">
<div><dt>Thread</dt><dd>{text(status?.threadId)}</dd></div>
<div><dt>State</dt><dd>{text(scene.stateStatus)} rev {text(scene.revision)}</dd></div>
<div><dt>Requires review</dt><dd>{text(coordinator.requiresReview)}</dd></div>
</dl>
</Panel>
<Panel title="Continuity" icon={<GitBranch size={18} />}>
<dl className="facts">
<div><dt>Pressure</dt><dd><Pill tone={statusClass(pressure.level)}>{text(pressure.level)}</Pill></dd></div>
<div><dt>Prepare compaction</dt><dd>{text(pressure.shouldPrepareCompaction)}</dd></div>
<div><dt>Reorient</dt><dd><Pill tone={statusClass(reorient.action)}>{text(reorient.action)}</Pill></dd></div>
<div><dt>Reasons</dt><dd>{listText(reorient.reasons)}</dd></div>
</dl>
<p className="reason">{text(reorient.nextAction)}</p>
</Panel>
</section>
<section className="sectionBand">
<SectionHeader title="Role Lanes" icon={<BriefcaseBusiness size={18} />} />
<div className="cardGrid">
{roles.map((role) => (
<article className="laneCard" key={text(role.id)}>
<div className="cardTopline">
<h3>{text(role.title)}</h3>
<Pill tone={statusClass(role.status)}>{text(role.status)}</Pill>
</div>
<p>{text(role.note)}</p>
<span className="owner">{text(role.ownerRole)}</span>
</article>
))}
{roles.length === 0 && <EmptyState label="No role lanes loaded." />}
</div>
</section>
<section className="sectionBand twoColumn">
<div>
<SectionHeader title="Findings" icon={<FileText size={18} />} />
<div className="stack">
<Finding title="Modeling / Checkpoint" result={roleResults.modeling} />
<Finding title="Verification / Review" result={roleResults.verification} />
<Finding title="Reorientation" result={reorientResult} findingKey="finding" />
</div>
</div>
<div>
<SectionHeader title="Jobs" icon={<Database size={18} />} />
<div className="stack">
{jobs.map((job) => (
<article className="jobRow" key={text(job.id)}>
<div>
<strong>{text(job.id)}</strong>
<span>{text(job.kind)} · {text(job.ownerRole)}</span>
</div>
<Pill tone={statusClass(job.status)}>{text(job.status)}</Pill>
</article>
))}
{jobs.length === 0 && <EmptyState label="No jobs loaded." />}
</div>
</div>
</section>
<section className="sectionBand">
<SectionHeader title="Artifact Bundles" icon={<FileText size={18} />} />
<div className="artifactTable" role="table" aria-label="Artifact bundles">
<div className="artifactHeader" role="row">
<span>Name</span>
<span>Files</span>
<span>Path</span>
</div>
{(snapshot?.artifacts ?? []).map((artifact: ArtifactBundle) => (
<div className="artifactRow" role="row" key={artifact.path}>
<strong>{artifact.name}</strong>
<span>{artifact.files.length}</span>
<code title={artifact.path}>{artifact.path}</code>
</div>
))}
{(snapshot?.artifacts ?? []).length === 0 && <EmptyState label="No dogfood artifact bundles found." />}
</div>
</section>
</main>
);
}
function Panel({ title, icon, children }: { title: string; icon: React.ReactNode; children: React.ReactNode }) {
return (
<section className="panel">
<SectionHeader title={title} icon={icon} />
{children}
</section>
);
}
function SectionHeader({ title, icon }: { title: string; icon: React.ReactNode }) {
return (
<div className="sectionHeader">
{icon}
<h2>{title}</h2>
</div>
);
}
function Pill({ tone, children }: { tone: string; children: React.ReactNode }) {
return <span className={`pill ${tone}`}>{children}</span>;
}
function Finding({ title, result, findingKey = "finding" }: { title: string; result: any; findingKey?: string }) {
const finding = result?.[findingKey];
return (
<article className="findingCard">
<div className="cardTopline">
<h3>{title}</h3>
<Pill tone={statusClass(result?.status)}>{text(result?.status)}</Pill>
</div>
{finding ? (
<>
<p>{text(finding.summary ?? finding.nextSafeMove ?? finding.mode ?? finding.verdict)}</p>
<dl className="facts compact">
<div><dt>Verdict</dt><dd>{text(finding.verdict ?? finding.mode)}</dd></div>
<div><dt>Next</dt><dd>{text(finding.nextSafeMove)}</dd></div>
</dl>
</>
) : (
<p>{text(result?.note, "No finding available.")}</p>
)}
</article>
);
}
function EmptyState({ label }: { label: string }) {
return <p className="emptyState">{label}</p>;
}

View file

@ -0,0 +1,10 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./App";
import "./styles.css";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

View file

@ -0,0 +1,397 @@
:root {
color: #172018;
background: #f7f8f5;
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
font-synthesis: none;
text-rendering: optimizeLegibility;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
background:
linear-gradient(180deg, rgba(240, 243, 237, 0.88), rgba(248, 249, 246, 1)),
#f7f8f5;
}
button,
input {
font: inherit;
}
.shell {
width: min(1440px, calc(100vw - 32px));
margin: 0 auto;
padding: 24px 0 36px;
}
.topbar,
.controls,
.statusGrid,
.twoColumn {
display: grid;
gap: 16px;
}
.topbar {
grid-template-columns: 1fr auto;
align-items: center;
margin-bottom: 18px;
}
.eyebrow {
margin: 0 0 4px;
color: #5f6b61;
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
}
h1,
h2,
h3,
p {
margin: 0;
}
h1 {
font-size: clamp(1.6rem, 3vw, 2.35rem);
line-height: 1.08;
}
h2 {
font-size: 0.94rem;
}
h3 {
font-size: 0.92rem;
line-height: 1.2;
}
.primaryButton {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 38px;
border: 1px solid #244d3a;
border-radius: 6px;
padding: 0 14px;
color: #f8fff8;
background: #244d3a;
cursor: pointer;
}
.primaryButton:disabled {
cursor: wait;
opacity: 0.68;
}
.controls {
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-bottom: 16px;
}
label {
display: grid;
gap: 6px;
color: #465047;
font-size: 0.78rem;
font-weight: 700;
}
input {
width: 100%;
min-height: 38px;
border: 1px solid #ccd4ca;
border-radius: 6px;
padding: 0 11px;
color: #172018;
background: #ffffff;
}
.notice,
.panel,
.laneCard,
.findingCard,
.jobRow,
.artifactTable {
border: 1px solid #d9ded5;
border-radius: 8px;
background: #ffffff;
box-shadow: 0 1px 2px rgba(23, 32, 24, 0.04);
}
.notice {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
padding: 12px;
}
.dangerNotice {
border-color: #d9b1a4;
color: #6b2418;
background: #fff7f4;
}
.statusGrid {
grid-template-columns: minmax(0, 1.35fr) minmax(320px, 0.65fr);
margin-bottom: 22px;
}
.panel,
.sectionBand {
padding: 16px;
}
.sectionBand {
margin-top: 18px;
}
.sectionHeader {
display: flex;
align-items: center;
gap: 8px;
min-height: 24px;
margin-bottom: 12px;
color: #263229;
}
.actionBanner {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 10px;
align-items: center;
min-height: 46px;
border-radius: 8px;
padding: 10px 12px;
margin-bottom: 12px;
}
.actionBanner strong {
overflow-wrap: anywhere;
}
.actionBanner span {
color: rgba(23, 32, 24, 0.68);
font-size: 0.84rem;
}
.ok {
background: #e7f3eb;
color: #1d5d39;
}
.warn {
background: #fff4d9;
color: #76520b;
}
.danger {
background: #fde8e1;
color: #8d2d1c;
}
.neutral {
background: #edf0f2;
color: #39454b;
}
.reason {
color: #3e493f;
line-height: 1.45;
}
.facts {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin: 14px 0 0;
}
.facts.compact {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.facts div {
min-width: 0;
}
dt {
color: #667168;
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
}
dd {
margin: 4px 0 0;
overflow-wrap: anywhere;
color: #172018;
}
.pill {
display: inline-flex;
align-items: center;
min-height: 24px;
max-width: 100%;
border-radius: 999px;
padding: 2px 9px;
font-size: 0.78rem;
font-weight: 700;
overflow-wrap: anywhere;
}
.cardGrid {
display: grid;
grid-template-columns: repeat(4, minmax(180px, 1fr));
gap: 12px;
}
.laneCard,
.findingCard,
.jobRow {
padding: 12px;
}
.laneCard p,
.findingCard p {
min-height: 42px;
margin-top: 10px;
color: #435045;
font-size: 0.9rem;
line-height: 1.42;
}
.owner {
display: block;
margin-top: 12px;
color: #68726a;
font-size: 0.78rem;
}
.cardTopline,
.jobRow,
.artifactHeader,
.artifactRow {
display: grid;
align-items: center;
gap: 12px;
}
.cardTopline {
grid-template-columns: minmax(0, 1fr) auto;
}
.twoColumn {
grid-template-columns: minmax(0, 1fr) minmax(360px, 0.86fr);
}
.stack {
display: grid;
gap: 10px;
}
.jobRow {
grid-template-columns: minmax(0, 1fr) auto;
}
.jobRow div {
display: grid;
gap: 3px;
min-width: 0;
}
.jobRow span {
color: #657069;
font-size: 0.82rem;
overflow-wrap: anywhere;
}
.artifactTable {
overflow: hidden;
}
.artifactHeader,
.artifactRow {
grid-template-columns: minmax(180px, 0.6fr) 72px minmax(220px, 1fr);
min-height: 42px;
padding: 0 12px;
}
.artifactHeader {
color: #606a62;
background: #f0f3ef;
font-size: 0.74rem;
font-weight: 800;
text-transform: uppercase;
}
.artifactRow + .artifactRow {
border-top: 1px solid #edf0eb;
}
code {
display: block;
overflow: hidden;
color: #36503f;
text-overflow: ellipsis;
white-space: nowrap;
}
.emptyState {
border: 1px dashed #cbd3c8;
border-radius: 8px;
padding: 14px;
color: #69736b;
background: #fbfcfa;
}
@media (max-width: 980px) {
.statusGrid,
.twoColumn,
.controls {
grid-template-columns: 1fr;
}
.cardGrid {
grid-template-columns: repeat(2, minmax(180px, 1fr));
}
}
@media (max-width: 640px) {
.shell {
width: min(100vw - 20px, 1440px);
padding-top: 14px;
}
.topbar {
grid-template-columns: 1fr;
}
.cardGrid,
.facts,
.facts.compact,
.artifactHeader,
.artifactRow {
grid-template-columns: 1fr;
}
.artifactHeader {
display: none;
}
.artifactRow {
gap: 4px;
padding: 10px 12px;
}
}

View file

@ -0,0 +1,23 @@
export interface StatusRequest {
threadId?: string;
cwd?: string;
codexHome?: string;
appServer?: string;
}
export interface ArtifactBundle {
name: string;
path: string;
files: string[];
summaryPath?: string;
finalStatusPath?: string;
comparisonPath?: string;
modifiedMillis?: number;
}
export interface OperatorSnapshot {
generatedAt: string;
repoRoot: string;
status: any;
artifacts: ArtifactBundle[];
}

View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ES2020"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"references": []
}

View file

@ -0,0 +1,13 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
clearScreen: false,
server: {
host: "127.0.0.1",
port: 1420,
strictPort: true,
},
envPrefix: ["VITE_", "TAURI_"],
});

View file

@ -83,6 +83,7 @@ The landed machine now has:
- first auditable Phase 6 dogfood runner through `tools/epiphany_mvp_dogfood.py`, producing local status snapshots, raw app-server transcript, 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
- first Phase 6 GUI operator shell under `apps/epiphany-gui`, a Tauri v2 + React read-only console over the existing status bridge and dogfood artifacts
- live Phase 6 reorientation app-server smoke coverage in `tools/epiphany_phase6_reorient_smoke.py`
- live Phase 6 reorient-launch app-server smoke coverage in `tools/epiphany_phase6_reorient_launch_smoke.py`
- live Phase 6 MVP status smoke coverage in `tools/epiphany_mvp_status_smoke.py`
@ -276,9 +277,10 @@ Initial shape:
Implementation slices:
1. **Read-only shell**: scaffold Tauri/React, connect to a running app-server,
render the same data as `tools/epiphany_mvp_status.py`, and provide artifact
file links.
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
slice is landed under `apps/epiphany-gui`.
2. **Bounded operator actions**: add buttons for the existing coordinator,
roleLaunch, roleResult, reorientLaunch, reorientResult, and reorientAccept
flows, preserving review gates.

View file

@ -35,7 +35,7 @@ Do not trust this file for the exact live HEAD. Always check git.
- Pre-compaction checkpoint intervention is now landed. On token-count events for loaded Epiphany threads, when pressure reaches the existing `shouldPrepareCompaction` threshold, the harness steers the active turn once with a CRRC checkpoint directive so the agent banks working context before compaction/reorientation. This is still bounded steering, not automatic semantic acceptance, a broad scheduler, or implementation continuation.
- `tools/epiphany_mvp_dogfood.py` is the first auditable dogfood runner. It drives a bounded state/role/CRRC/reorientation loop and writes local artifacts under `.epiphany-dogfood/mvp-loop`, including raw app-server transcript, rendered snapshots, final status, vanilla-reference prompt/output, and comparison notes. The runner now writes truthful vanilla/comparison artifacts whether the optional vanilla reference is skipped, fails, or completes.
- `tools/epiphany_mvp_live_specialist.py` is the first auditable live-specialist runner. It launches a real role specialist through `thread/epiphany/roleLaunch`, lets the spawned worker report through `report_agent_job_result`, reads it back through `thread/epiphany/roleResult`, and writes local artifacts under `.epiphany-dogfood/live-specialist`.
- The MVP GUI plan is now Tauri v2 + React under `apps/epiphany-gui`, talking to existing app-server APIs. It is an operator console over typed state and artifact surfaces, not a new source of truth.
- The first MVP GUI shell now exists under `apps/epiphany-gui`. It is a Tauri v2 + React read-only operator console over the existing status bridge and dogfood artifact surfaces, not a new source of truth.
- Internal `agent_job:` workers now get the reporting tool independent of the user-facing CSV spawn feature, and ephemeral worker sessions can initialize the sqlite state runtime on demand so specialist reports land in the shared backend.
- Durable in-flight investigation checkpointing is now landed in authoritative typed state, writable through `thread/epiphany/update` or accepted `thread/epiphany/promote`, rendered into the prompt, and reflected through scene/context.
- The repo is an Epiphany fork of Codex, not a Codex preset.
@ -104,6 +104,7 @@ The current spine:
- live MVP coordinator smoke through `tools/epiphany_mvp_coordinator_smoke.py`
- live job-control app-server smoke through `tools/epiphany_phase6_job_control_smoke.py`
- live specialist MVP pass through `tools/epiphany_mvp_live_specialist.py`
- Tauri v2 + React read-only GUI operator shell under `apps/epiphany-gui`
The exact current control flow is documented in
`notes/epiphany-current-algorithmic-map.md`.
@ -299,14 +300,15 @@ The fixed-lane coordinator MVP is testable, and the first pre-compaction CRRC
intervention is now wired. Limited safe-boundary CRRC execution still handles
compact and fixed reorient-worker launch actions after a turn ends; the
token-count hook now handles the earlier danger zone by steering active loaded
turns once when `shouldPrepareCompaction` is reached. The next real move is the
first Tauri v2 + React operator console slice over the same status,
coordinator, role, reorient, job, and artifact surfaces so the user does not
have to operate the machine from a terminal. Start read-only, then add bounded
buttons for already-existing authority surfaces. CRRC is not a specialist-agent
persona; the reorient-worker it may launch is the specialist. Do not turn the
coordinator into a broad hidden dispatcher, arbitrary marketplace, alternate
job backend, automatic semantic acceptance path, or GUI-as-source-of-truth.
turns once when `shouldPrepareCompaction` is reached. The first Tauri v2 +
React read-only operator console is also scaffolded. It renders the same status,
coordinator, role, reorient, job, and artifact surfaces through the existing MVP
status bridge so the user does not have to operate the machine from a terminal.
The next real move is a visual/operator smoke, then bounded buttons for
already-existing authority surfaces. CRRC is not a specialist-agent persona;
the reorient-worker it may launch is the specialist. Do not turn the coordinator
into a broad hidden dispatcher, arbitrary marketplace, alternate job backend,
automatic semantic acceptance path, or GUI-as-source-of-truth.
Live `thread/epiphany/scene`, `thread/epiphany/jobs`, `thread/epiphany/roles`,
`thread/epiphany/freshness`, `thread/epiphany/context`,

View file

@ -65,3 +65,4 @@
{"ts": "2026-04-29T15:15:06+01:00", "type": "implementation", "status": "ok", "note": "Landed the smallest pre-compaction checkpoint intervention: TokenCount handling maps pressure through the existing gauge and, for loaded Epiphany threads at shouldPrepareCompaction, steers the active turn once with a bounded CRRC directive to bank checkpoint/scratch/map/evidence before compaction or reorientation. It does not mutate state directly, accept findings, launch arbitrary specialists, or continue implementation. Focused app-server tests plus coordinator/status/reorient/role smokes passed; a stale scene expected-actions assertion was corrected to include the already-landed coordinator action."}
{"ts": "2026-04-29T14:35:07+00:00", "type": "implementation", "status": "ok", "note": "Self-dogfood exposed an artifact-honesty blocker in tools/epiphany_mvp_dogfood.py: the manifest listed vanilla comparison artifacts that were not written. The runner now writes truthful vanilla-reference and comparison artifacts for skipped, failed, or completed reference runs; an explicit --run-vanilla-reference pass completed with a persisted vanilla transcript under .epiphany-dogfood/mvp-loop-selfdogfood-vanilla2-20260429."}
{"ts": "2026-04-29T14:55:37+00:00", "type": "design", "status": "ok", "note": "Chose the MVP GUI path: scaffold a local Tauri v2 + React operator console over existing app-server status/coordinator/role/reorient/job/artifact surfaces. The GUI reflects and steers typed Epiphany state but does not become a source of truth; first slice should be read-only, with bounded action buttons added only for already-existing authority surfaces after rendering is proved."}
{"ts": "2026-04-29T15:18:49+00:00", "type": "implementation", "status": "ok", "note": "Landed the first MVP GUI shell under apps/epiphany-gui: Tauri v2 + React read-only operator console invoking the existing epiphany_mvp_status.py app-server bridge and listing dogfood artifact bundles. Verified npm install, npm run build, cargo check with CARGO_TARGET_DIR, tauri info, tauri build --debug --no-bundle, and a live status-bridge JSON check."}

View file

@ -12,9 +12,9 @@ objective:
explicit typed models of the codebase, active goal, evidence, and change
machinery they are working on.
current: >
Turn the landed Phase 6 coordinator/status/dogfood artifact surfaces into
the smallest local Tauri v2 + React operator GUI, while continuing
agent-run dogfood of CRRC and pre-compaction behavior.
Harden the first local Tauri v2 + React operator GUI over the landed Phase
6 coordinator/status/dogfood artifact surfaces, while continuing agent-run
dogfood of CRRC and pre-compaction behavior.
constraints:
- No frontier-scale training budget.
@ -76,6 +76,7 @@ accepted_design:
- The harness now has an in-flight investigation checkpoint packet in durable typed state; automatic CRRC still needs runtime logic that reads the packet and decides whether to resume or force reorientation and re-gathering after compaction.
- GUI/client work must reflect or steer typed state rather than manufacture a second source of truth.
- The MVP GUI target is a local Tauri v2 + React operator console over existing app-server APIs. Tauri owns windowing and local lifecycle; React renders the operator surface; app-server and typed Epiphany state remain authoritative.
- `apps/epiphany-gui` is the first read-only GUI shell: a Tauri v2 + React app that invokes the existing MVP status bridge, renders coordinator/CRRC/pressure/reorient/role/job/finding status, and lists dogfood artifact bundles without writing Epiphany state.
- Durable `job_bindings` now form a thin Epiphany-owned launcher seam: bindings can carry launcher identity, authority scope, backend kind, and backend job id while still supporting the older runtime-agent-job pointer as a compatibility alias.
- `thread/epiphany/jobs` is a read-only job/progress reflection: it derives indexing, remap, verification, and specialist slots from authoritative state and retrieval summaries, and can overlay durable launcher bindings onto live runtime `agent_jobs` snapshots through the current backend adapter when a real owner exists; it is not a scheduler, notification stream, or durable runtime job table.
- `thread/epiphany/jobsUpdated` is the first live bound-job progress notification: it translates runtime `agent_job_progress:{json}` background events into changed launcher-bound payloads by resolving durable `job_bindings` against live `agent_jobs` snapshots through the current backend adapter, without polling in a loop, mutating state, or scheduling work.
@ -127,6 +128,7 @@ landed_baseline:
- Phase 6 coordinator: read-only `thread/epiphany/coordinator` plus `tools/epiphany_mvp_coordinator.py` and smoke coverage for the fixed-lane MVP loop.
- Phase 6 safe-boundary CRRC automation: turn-complete automation limited to coordinator-approved compact and fixed reorient-worker launch actions.
- Phase 6 pre-compaction checkpoint intervention: token-count pressure hook that steers active turns once when `shouldPrepareCompaction` is reached so working context is banked before compaction.
- Phase 6 GUI operator shell: Tauri v2 + React read-only console under `apps/epiphany-gui` over the existing status bridge and artifact bundles.
guardrails:
- Use `tools/epiphany_phase5_smoke.py` before modifying update, distill, propose, promote, read-projection, or notification behavior.
- Use `tools/epiphany_phase6_scene_smoke.py` before modifying scene projection behavior.
@ -162,5 +164,5 @@ rejected_paths:
reason: persistent state that accumulates every local truth becomes memory cruft and weakens rehydration
current_status:
summary: Phase 1 durable state, Phase 2 prompt integration, Phase 3 typed read, Phase 4 retrieval/indexing, Phase 5 update/distill/propose/promote/read/notification, and Phase 6 read-only scene, jobs, roles, freshness, targeted context, pressure, reorient, CRRC, coordinator, reorient launch/result/accept, explicit modeling/checkpoint and verification/review role launch/result templates, MVP status, dogfood runner, coordinator runner, live specialist runner, limited safe-boundary CRRC automation, and pre-compaction checkpoint intervention are landed. Native CRRC can steer active loaded Epiphany turns once at `shouldPrepareCompaction`; turn-complete automation can still compact or launch the fixed reorient-worker when the coordinator says it can auto-run. The latest self-dogfood pass exposed and fixed an artifact-honesty blocker in the MVP dogfood runner: comparison and vanilla-reference artifacts are now actually written and the optional real vanilla run produces a persisted read-back transcript. Implementation remains main-agent work; CRRC remains workflow automation, not a specialist persona.
next_action: Scaffold `apps/epiphany-gui` as a Tauri v2 + React local operator console over the existing app-server status/coordinator/role/reorient/artifact surfaces. First slice is read-only: render active thread, coordinator recommendation, pressure/CRRC/reorient status, role lanes, job progress, reviewable findings, and artifact bundle links without becoming a source of truth.
summary: Phase 1 durable state, Phase 2 prompt integration, Phase 3 typed read, Phase 4 retrieval/indexing, Phase 5 update/distill/propose/promote/read/notification, and Phase 6 read-only scene, jobs, roles, freshness, targeted context, pressure, reorient, CRRC, coordinator, reorient launch/result/accept, explicit modeling/checkpoint and verification/review role launch/result templates, MVP status, dogfood runner, coordinator runner, live specialist runner, limited safe-boundary CRRC automation, pre-compaction checkpoint intervention, and the first read-only Tauri/React GUI shell are landed. Native CRRC can steer active loaded Epiphany turns once at `shouldPrepareCompaction`; turn-complete automation can still compact or launch the fixed reorient-worker when the coordinator says it can auto-run. Implementation remains main-agent work; CRRC remains workflow automation, not a specialist persona; GUI remains an operator surface over typed state, not a source of truth.
next_action: Run a visual/operator smoke for `apps/epiphany-gui`, then add bounded action buttons for already-existing authority surfaces: refresh/status snapshot, coordinator pass, fixed modeling/verification launches, fixed reorient-worker launch when recommended, and explicit reorientAccept after review.