Add usable GUI lane controls

This commit is contained in:
Meta Vens 2026-04-29 17:22:46 +01:00
commit b328f68352
12 changed files with 690 additions and 61 deletions

View file

@ -8,12 +8,15 @@ artifact state without working through a terminal.
Current slice:
- read-only operator console
- operator console over the existing app-server APIs
- 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
- writes explicit status-snapshot and coordinator-plan artifact bundles through
bounded operator buttons
- prepares durable Epiphany checkpoint state, then launches and reads the fixed
modeling, verification, and reorient lanes from GUI buttons, with reorient
acceptance kept explicit and review-gated
Run from this directory:
@ -43,9 +46,15 @@ $env:CARGO_TARGET_DIR='C:\Users\Meta\.cargo-target-codex'
cargo build -p codex-app-server
```
The first bounded buttons are intentionally narrow:
The bounded buttons are intentionally narrow:
- **Status Snapshot** writes a status JSON/transcript/stderr bundle under
`.epiphany-gui/status-snapshots`.
- **Coordinator Plan** runs `tools/epiphany_mvp_coordinator.py --mode plan` and
writes an auditable bundle under `.epiphany-dogfood`.
- **Prepare Checkpoint** seeds durable Epiphany state for a GUI operator thread
and returns the persisted thread id that unlocks lane actions.
- **Launch/Read Modeling** and **Launch/Read Verification** call the fixed role
lane APIs and write GUI action bundles under `.epiphany-gui/actions`.
- **Launch/Read/Accept Reorient** calls the fixed reorient-worker APIs.
Acceptance stays disabled until a completed reorient result is present.

View file

@ -52,11 +52,25 @@ async function smokeViewport(browser, viewport, screenshotPath) {
await page.getByText("prepareCheckpoint").waitFor();
await page.getByText("Role Lanes").waitFor();
await page.getByText("Artifact Bundles").waitFor();
await page.getByRole("button", { name: "Prepare Checkpoint" }).waitFor();
await page.getByRole("button", { name: "Launch Modeling" }).waitFor();
await page.getByRole("button", { name: "Read Modeling" }).waitFor();
await page.getByRole("button", { name: "Launch Verification" }).waitFor();
await page.getByRole("button", { name: "Read Verification" }).waitFor();
await page.getByRole("button", { name: "Launch Reorient" }).waitFor();
await page.getByRole("button", { name: "Read Reorient" }).waitFor();
await page.getByRole("button", { name: "Accept Reorient" }).waitFor();
if (viewport.width >= 900) {
await page.getByRole("button", { name: "Status Snapshot" }).click();
await page.getByText("statusSnapshot sample completed.").waitFor();
await page.getByRole("button", { name: "Coordinator Plan" }).click();
await page.getByText("coordinatorPlan sample completed.").waitFor();
await page.getByRole("button", { name: "Prepare Checkpoint" }).click();
await page.getByText("prepareCheckpoint sample completed.").waitFor();
await page.getByRole("button", { name: "Read Modeling" }).click();
await page.getByText("readModelingResult sample completed.").waitFor();
await page.getByRole("button", { name: "Read Reorient" }).click();
await page.getByText("readReorientResult sample completed.").waitFor();
}
await page.screenshot({ path: screenshotPath, fullPage: true });

View file

@ -20,6 +20,7 @@ struct OperatorActionResult {
action: String,
artifact_path: String,
summary: String,
thread_id: Option<String>,
}
#[derive(Debug, Serialize)]
@ -66,6 +67,14 @@ fn run_operator_action(
match action.as_str() {
"statusSnapshot" => run_status_snapshot(&repo_root, request),
"coordinatorPlan" => run_coordinator_plan(&repo_root, request),
"launchModeling"
| "readModelingResult"
| "launchVerification"
| "readVerificationResult"
| "launchReorient"
| "readReorientResult"
| "acceptReorient"
| "prepareCheckpoint" => run_gui_action_bridge(&repo_root, request, action),
_ => Err(format!("unknown operator action: {action}")),
}
}
@ -100,7 +109,8 @@ fn load_status(repo_root: &Path, request: StatusRequest) -> Result<Value, String
.arg("--transcript")
.arg(transcript)
.arg("--stderr")
.arg(stderr);
.arg(stderr)
.arg("--no-ephemeral");
if let Some(thread_id) = request.thread_id {
command.arg("--thread-id").arg(thread_id);
@ -161,7 +171,8 @@ fn run_status_snapshot(
.arg("--transcript")
.arg(transcript_path)
.arg("--stderr")
.arg(stderr_path);
.arg(stderr_path)
.arg("--no-ephemeral");
if let Some(thread_id) = request.thread_id {
command.arg("--thread-id").arg(thread_id);
}
@ -173,6 +184,7 @@ fn run_status_snapshot(
action: "statusSnapshot".to_string(),
artifact_path: artifact_root.display().to_string(),
summary: "Status snapshot written.".to_string(),
thread_id: None,
})
}
@ -218,6 +230,54 @@ fn run_coordinator_plan(
action: "coordinatorPlan".to_string(),
artifact_path: artifact_dir.display().to_string(),
summary: "Coordinator plan artifact written.".to_string(),
thread_id: None,
})
}
fn run_gui_action_bridge(
repo_root: &Path,
request: StatusRequest,
action: String,
) -> Result<OperatorActionResult, String> {
let thread_id = request.thread_id.clone();
let python = find_python()?;
let artifact_root = repo_root.join(".epiphany-gui").join("actions");
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 mut command = Command::new(python);
command
.current_dir(repo_root)
.arg(repo_root.join("tools").join("epiphany_gui_action.py"))
.arg("--action")
.arg(&action)
.arg("--cwd")
.arg(workspace)
.arg("--codex-home")
.arg(codex_home)
.arg("--artifact-root")
.arg(artifact_root);
if let Some(thread_id) = 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 value = run_json_command(command, &action)?;
Ok(OperatorActionResult {
action,
artifact_path: json_string(&value, "artifactPath")?,
summary: json_string(&value, "summary")?,
thread_id: value
.get("threadId")
.and_then(Value::as_str)
.map(ToString::to_string),
})
}
@ -232,13 +292,53 @@ fn run_command(mut command: Command, label: &str) -> Result<(), String> {
Err(format!("{label} exited with {}: {}", output.status, stderr))
}
fn run_json_command(mut command: Command, label: &str) -> Result<Value, String> {
let output = command
.output()
.map_err(|err| format!("failed to run {label}: {err}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("{label} exited with {}: {}", output.status, stderr));
}
serde_json::from_slice(&output.stdout)
.map_err(|err| format!("failed to parse {label} JSON: {err}"))
}
fn json_string(value: &Value, key: &str) -> Result<String, String> {
value
.get(key)
.and_then(Value::as_str)
.map(ToString::to_string)
.ok_or_else(|| format!("missing string field in GUI action result: {key}"))
}
fn list_artifacts(repo_root: &Path) -> Result<Vec<ArtifactBundle>, String> {
let root = repo_root.join(".epiphany-dogfood");
let mut bundles = Vec::new();
collect_artifact_root(&mut bundles, &repo_root.join(".epiphany-dogfood"), "")?;
collect_artifact_root(
&mut bundles,
&repo_root.join(".epiphany-gui").join("actions"),
"actions/",
)?;
collect_artifact_root(
&mut bundles,
&repo_root.join(".epiphany-gui").join("status-snapshots"),
"status/",
)?;
bundles.sort_by(|a, b| b.modified_millis.cmp(&a.modified_millis));
Ok(bundles)
}
fn collect_artifact_root(
bundles: &mut Vec<ArtifactBundle>,
root: &Path,
name_prefix: &str,
) -> Result<(), String> {
if !root.exists() {
return Ok(Vec::new());
return Ok(());
}
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();
@ -255,24 +355,27 @@ fn list_artifacts(repo_root: &Path) -> Result<Vec<ArtifactBundle>, String> {
}
}
files.sort();
let raw_name = entry.file_name().to_string_lossy().to_string();
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(),
name: format!("{name_prefix}{raw_name}"),
path: path.display().to_string(),
summary_path: existing_path(&path, "epiphany-dogfood-summary.json"),
final_status_path: existing_path(&path, "epiphany-final-status.json"),
summary_path: existing_path(&path, "epiphany-dogfood-summary.json")
.or_else(|| existing_path(&path, "gui-action-summary.json"))
.or_else(|| existing_path(&path, "status.json")),
final_status_path: existing_path(&path, "epiphany-final-status.json")
.or_else(|| existing_path(&path, "after-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)
Ok(())
}
fn existing_path(root: &Path, name: &str) -> Option<String> {

View file

@ -1,9 +1,113 @@
import { AlertTriangle, BriefcaseBusiness, ClipboardCheck, Database, FileText, GitBranch, RefreshCw } from "lucide-react";
import {
AlertTriangle,
BriefcaseBusiness,
CheckCircle2,
ClipboardCheck,
Database,
Eye,
FileText,
GitBranch,
Play,
RefreshCw,
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { loadOperatorSnapshot, runOperatorAction } from "./operatorApi";
import type { ArtifactBundle, OperatorAction, OperatorActionResult, OperatorSnapshot, StatusRequest } from "./types";
const roleOrder = ["implementation", "modeling", "verification", "reorientation"];
const actionButtons: Array<{
action: OperatorAction;
label: string;
runningLabel: string;
title: string;
requiresThread?: boolean;
requiresReadyState?: boolean;
requiresReorientResult?: boolean;
icon: "file" | "check" | "play" | "eye" | "accept";
}> = [
{
action: "statusSnapshot",
label: "Status Snapshot",
runningLabel: "Writing",
title: "Write an auditable status snapshot",
icon: "file",
},
{
action: "coordinatorPlan",
label: "Coordinator Plan",
runningLabel: "Running",
title: "Run a review-gated coordinator plan",
icon: "check",
},
{
action: "prepareCheckpoint",
label: "Prepare Checkpoint",
runningLabel: "Preparing",
title: "Seed durable Epiphany state for this GUI operator thread",
icon: "accept",
},
{
action: "launchModeling",
label: "Launch Modeling",
runningLabel: "Launching",
title: "Launch the fixed modeling/checkpoint worker for this thread",
requiresThread: true,
requiresReadyState: true,
icon: "play",
},
{
action: "readModelingResult",
label: "Read Modeling",
runningLabel: "Reading",
title: "Read the latest modeling/checkpoint finding",
requiresThread: true,
icon: "eye",
},
{
action: "launchVerification",
label: "Launch Verification",
runningLabel: "Launching",
title: "Launch the fixed verification/review worker for this thread",
requiresThread: true,
requiresReadyState: true,
icon: "play",
},
{
action: "readVerificationResult",
label: "Read Verification",
runningLabel: "Reading",
title: "Read the latest verification/review finding",
requiresThread: true,
icon: "eye",
},
{
action: "launchReorient",
label: "Launch Reorient",
runningLabel: "Launching",
title: "Launch the bounded reorient-worker for this thread",
requiresThread: true,
requiresReadyState: true,
icon: "play",
},
{
action: "readReorientResult",
label: "Read Reorient",
runningLabel: "Reading",
title: "Read the latest reorient-worker finding",
requiresThread: true,
icon: "eye",
},
{
action: "acceptReorient",
label: "Accept Reorient",
runningLabel: "Accepting",
title: "Accept a completed reorient-worker finding into Epiphany state",
requiresThread: true,
requiresReadyState: true,
requiresReorientResult: true,
icon: "accept",
},
];
function text(value: unknown, fallback = "none"): string {
if (value === null || value === undefined || value === "") {
@ -44,6 +148,16 @@ function useSnapshot() {
try {
const result = await loadOperatorSnapshot(nextRequest);
setSnapshot(result);
const loadedThreadId = result.status?.threadId;
const loadedState = result.status?.scene?.scene?.stateStatus;
if (
!nextRequest.threadId &&
loadedState !== "missing" &&
typeof loadedThreadId === "string" &&
loadedThreadId.length > 0
) {
setRequest((current) => (current.threadId ? current : { ...current, threadId: loadedThreadId }));
}
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
@ -58,7 +172,11 @@ function useSnapshot() {
try {
const result = await runOperatorAction(action, request);
setActionResult(result);
await refresh(request);
const nextRequest = result.threadId ? { ...request, threadId: result.threadId } : request;
if (result.threadId) {
setRequest(nextRequest);
}
await refresh(nextRequest);
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
@ -90,6 +208,9 @@ export function App() {
const jobs: any[] = Array.isArray(status?.jobs?.jobs) ? status.jobs.jobs : [];
const roleResults = status?.roleResults ?? {};
const reorientResult = status?.reorientResult ?? {};
const readyState = scene.stateStatus === "ready";
const currentThreadId = request.threadId;
const canAcceptReorient = text(reorientResult?.status).toLowerCase() === "completed";
return (
<main className="shell">
@ -108,7 +229,7 @@ export function App() {
<label>
Thread ID
<input
placeholder="start ephemeral status thread"
placeholder="auto-load persistent status thread"
value={request.threadId ?? ""}
onChange={(event) => setRequest({ ...request, threadId: event.target.value || undefined })}
/>
@ -124,24 +245,31 @@ export function App() {
</section>
<section className="actionStrip" aria-label="Bounded operator actions">
<button
className="secondaryButton"
onClick={() => void runAction("statusSnapshot")}
disabled={runningAction !== null}
title="Write an auditable status snapshot"
>
<FileText size={16} aria-hidden="true" />
{runningAction === "statusSnapshot" ? "Writing" : "Status Snapshot"}
</button>
<button
className="secondaryButton"
onClick={() => void runAction("coordinatorPlan")}
disabled={runningAction !== null}
title="Run a review-gated coordinator plan"
>
<ClipboardCheck size={16} aria-hidden="true" />
{runningAction === "coordinatorPlan" ? "Running" : "Coordinator Plan"}
</button>
{actionButtons.map((button) => {
const needsThread = button.requiresThread && !currentThreadId;
const needsState = button.requiresReadyState && !readyState;
const needsReorient = button.requiresReorientResult && !canAcceptReorient;
const disabled = runningAction !== null || needsThread || needsState || needsReorient;
const title = needsThread
? "Prepare a checkpoint or enter a persisted thread id first"
: needsState
? "Prepare Epiphany state before launching this lane"
: needsReorient
? "Read a completed reorient result before accepting it"
: button.title;
return (
<button
className="secondaryButton"
onClick={() => void runAction(button.action)}
disabled={disabled}
title={title}
key={button.action}
>
<ActionIcon icon={button.icon} />
{runningAction === button.action ? button.runningLabel : button.label}
</button>
);
})}
{actionResult && (
<p className="actionResult">
{actionResult.summary} <code>{actionResult.artifactPath}</code>
@ -264,6 +392,14 @@ function SectionHeader({ title, icon }: { title: string; icon: React.ReactNode }
);
}
function ActionIcon({ icon }: { icon: "file" | "check" | "play" | "eye" | "accept" }) {
if (icon === "file") return <FileText size={16} aria-hidden="true" />;
if (icon === "check") return <ClipboardCheck size={16} aria-hidden="true" />;
if (icon === "play") return <Play size={16} aria-hidden="true" />;
if (icon === "eye") return <Eye size={16} aria-hidden="true" />;
return <CheckCircle2 size={16} aria-hidden="true" />;
}
function Pill({ tone, children }: { tone: string; children: React.ReactNode }) {
return <span className={`pill ${tone}`}>{children}</span>;
}

View file

@ -23,6 +23,7 @@ export async function runOperatorAction(action: OperatorAction, request: StatusR
action,
artifactPath: "E:\\Projects\\EpiphanyAgent\\.epiphany-gui\\sample-action",
summary: `${action} sample completed.`,
threadId: action === "prepareCheckpoint" ? "019dd9d1-045b-7f13-b0e1-38ed89b31495" : request.threadId,
};
}

View file

@ -5,12 +5,23 @@ export interface StatusRequest {
appServer?: string;
}
export type OperatorAction = "statusSnapshot" | "coordinatorPlan";
export type OperatorAction =
| "statusSnapshot"
| "coordinatorPlan"
| "prepareCheckpoint"
| "launchModeling"
| "readModelingResult"
| "launchVerification"
| "readVerificationResult"
| "launchReorient"
| "readReorientResult"
| "acceptReorient";
export interface OperatorActionResult {
action: OperatorAction;
artifactPath: string;
summary: string;
threadId?: string;
}
export interface ArtifactBundle {

View file

@ -83,7 +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 console over the existing status bridge and dogfood artifacts, with bounded status-snapshot and coordinator-plan buttons
- first Phase 6 GUI operator shell under `apps/epiphany-gui`, a Tauri v2 + React console over the existing status bridge, dogfood artifacts, and GUI action artifacts, with durable checkpoint preparation, bounded status/coordinator artifact buttons, fixed modeling/verification/reorient launch and read-back buttons, and explicit review-gated reorient acceptance
- 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`
@ -265,6 +265,7 @@ Initial shape:
- refresh status
- run status snapshot
- run coordinator pass
- prepare a durable checkpoint for a resumable operator thread
- launch fixed modeling/checkpoint role
- launch fixed verification/review role
- launch fixed reorient-worker when recommended
@ -281,9 +282,11 @@ Implementation slices:
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**: status snapshot and coordinator-plan buttons
are landed. Next add buttons for roleLaunch, roleResult, reorientLaunch,
reorientResult, and reorientAccept flows, preserving review gates.
2. **Bounded operator actions**: status snapshot, coordinator-plan, durable
checkpoint preparation, roleLaunch, roleResult, reorientLaunch,
reorientResult, and explicit reorientAccept flows are landed. Review gates
remain explicit; the GUI does not auto-promote evidence or continue
implementation after semantic findings.
3. **Dogfood launcher**: wrap `tools/epiphany_mvp_dogfood.py` and
`tools/epiphany_mvp_coordinator.py` as explicit operator actions that write
artifact bundles and stream progress/status into the GUI.
@ -294,10 +297,13 @@ Implementation slices:
Verification:
- Keep the existing CLI smokes as backend guardrails.
- Add a GUI smoke that starts the app-server against a smoke thread, opens the
Tauri webview, verifies the operator console renders scene/coordinator/roles,
and checks at least one bounded action produces an auditable artifact.
- Use screenshots for layout regressions once the first screen exists.
- Keep `npm run smoke:visual` for browser-layout regressions and bounded
browser-fallback action clicks.
- For native GUI changes, run `npm run build`, `cargo fmt --check`,
`cargo check`, and `npm run tauri -- build --debug --no-bundle`.
- Use live bridge probes when action lifecycle changes; the current
`prepareCheckpoint -> readModelingResult` probe proved a checkpoint-created
thread can be resumed by a later process.
## Later Phases

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 first MVP GUI shell now exists under `apps/epiphany-gui`. It is a Tauri v2 + React operator console over the existing status bridge and dogfood artifact surfaces, not a new source of truth. It has bounded buttons for status snapshots and coordinator-plan artifacts.
- The first MVP GUI shell now exists under `apps/epiphany-gui`. It is a Tauri v2 + React operator console over the existing status bridge, dogfood artifacts, and GUI action artifacts, not a new source of truth. It can prepare a durable Epiphany checkpoint for a resumable operator thread, run status snapshots and coordinator-plan artifacts, launch/read the fixed modeling and verification lanes, launch/read the fixed reorient-worker, and explicitly accept completed reorient findings after review.
- 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,7 +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 GUI operator shell under `apps/epiphany-gui`, including visual smoke and first bounded artifact-writing buttons
- Tauri v2 + React GUI operator shell under `apps/epiphany-gui`, including visual smoke, durable checkpoint preparation, fixed lane launch/read-back controls, explicit review-gated reorient acceptance, and bounded artifact-writing buttons
The exact current control flow is documented in
`notes/epiphany-current-algorithmic-map.md`.
@ -301,16 +301,20 @@ 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 first Tauri v2 +
React 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 GUI visual smoke covers desktop and mobile screenshots and clicks the first
bounded buttons: status snapshot and coordinator plan. The next real move is
adding the fixed role/reorient launch and explicit reorientAccept buttons while
preserving review gates. 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.
React operator console is now usable enough to dogfood: it renders the same
status, coordinator, role, reorient, job, and artifact surfaces through the
existing MVP status bridge; it has a Prepare Checkpoint button that creates
durable resumable Epiphany state; and it can launch/read fixed modeling,
verification, and reorient lanes plus accept completed reorient findings after
review. GUI visual smoke covers desktop and mobile screenshots and clicks the
bounded browser-fallback controls. A live bridge probe also proved
`prepareCheckpoint` creates a resumable thread and a later process can
`readModelingResult` from it. The next real move is dogfooding the built GUI on
a real Epiphany thread, fixing only concrete operator blockers. 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

@ -67,3 +67,4 @@
{"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."}
{"ts": "2026-04-29T15:48:44+00:00", "type": "implementation", "status": "ok", "note": "Extended the GUI MVP shell with repeatable visual smoke and first bounded operator actions. apps/epiphany-gui now has a browser fallback sample snapshot, Playwright desktop/mobile visual smoke with screenshots, and Tauri commands/buttons for statusSnapshot and coordinatorPlan artifact bundles. The buttons write auditable artifacts but do not accept findings, promote evidence, or launch arbitrary specialists."}
{"ts": "2026-04-29T16:22:19+00:00", "type": "verification", "status": "ok", "note": "GUI operator MVP now prepares durable checkpoint state and exposes fixed modeling, verification, and reorient launch/read controls plus review-gated reorient acceptance. Verified with py_compile, npm build, browser visual smoke, cargo fmt/check, Tauri debug build, and a live prepareCheckpoint -> readModelingResult bridge probe."}

View file

@ -12,9 +12,9 @@ objective:
explicit typed models of the codebase, active goal, evidence, and change
machinery they are working on.
current: >
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.
Dogfood the local Tauri v2 + React operator GUI now that it can prepare a
durable Epiphany checkpoint, run coordinator/status artifacts, and drive the
fixed modeling, verification, and reorient lane controls over app-server.
constraints:
- No frontier-scale training budget.
@ -76,7 +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 GUI shell: a Tauri v2 + React app that invokes the existing MVP status bridge, renders coordinator/CRRC/pressure/reorient/role/job/finding status, lists dogfood artifact bundles, and exposes bounded artifact-writing buttons for status snapshot and coordinator plan without accepting semantic findings.
- `apps/epiphany-gui` is the first GUI shell: a Tauri v2 + React app that invokes the existing MVP status bridge, renders coordinator/CRRC/pressure/reorient/role/job/finding status, lists dogfood and GUI action artifact bundles, prepares a durable Epiphany checkpoint for a resumable operator thread, and exposes bounded buttons for status snapshots, coordinator plans, fixed modeling/verification/reorient launch and read-back, and explicit review-gated reorient acceptance.
- 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.
@ -128,7 +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 console under `apps/epiphany-gui` over the existing status bridge and artifact bundles, with visual smoke and first bounded artifact-writing buttons.
- Phase 6 GUI operator shell: Tauri v2 + React console under `apps/epiphany-gui` over the existing status bridge and artifact bundles, with visual smoke, durable checkpoint preparation, fixed lane launch/read-back controls, and explicit review-gated reorient acceptance.
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.
@ -146,6 +146,7 @@ landed_baseline:
- Use `tools/epiphany_mvp_coordinator_smoke.py` before modifying the fixed-lane MVP coordinator endpoint or runner.
- Use `tools/epiphany_phase6_role_smoke.py` before modifying explicit role launch/read-back behavior.
- Use `tools/epiphany_mvp_live_specialist.py` before claiming live specialist execution still works end to end.
- Use `npm run build`, `npm run smoke:visual`, `cargo fmt --check`, `cargo check`, and `npm run tauri -- build --debug --no-bundle` under `apps/epiphany-gui` before claiming GUI operator changes are usable.
- Use focused scene/jobs/context/pressure/reorient/protocol/schema checks for reflection-only changes.
- Use `tools/epiphany_prepare_compaction.py` before and after imminent-compaction persistence passes.
- Use `CARGO_TARGET_DIR=C:\Users\Meta\.cargo-target-codex` for Codex Rust work on this Windows machine.
@ -164,5 +165,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, pre-compaction checkpoint intervention, and the first Tauri/React GUI operator 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. The GUI now has desktop/mobile visual smoke plus bounded status-snapshot and coordinator-plan buttons. 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: Add fixed role/reorient operator buttons to `apps/epiphany-gui`: launch modeling/checkpoint, launch verification/review, launch fixed reorient-worker when recommended, read role/reorient results, and explicit reorientAccept after review. Preserve review gates and do not auto-promote evidence or continue implementation.
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 Tauri/React GUI operator 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. The GUI now has desktop/mobile visual smoke, durable checkpoint preparation, bounded status/coordinator artifact buttons, fixed role/reorient launch and read-back buttons, and explicit reorientAccept after a completed result. 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: Dogfood the built GUI on a real Epiphany thread: open the Tauri debug app, prepare or enter a persisted thread, run coordinator guidance, launch/read the recommended fixed lane, inspect the generated artifact bundle, and fix only concrete operator blockers.

View file

@ -0,0 +1,341 @@
from __future__ import annotations
import argparse
from datetime import datetime
from datetime import timezone
import json
import os
import time
from pathlib import Path
from typing import Any
from epiphany_mvp_status import DEFAULT_APP_SERVER
from epiphany_mvp_status import collect_status
from epiphany_mvp_status import render_status
from epiphany_phase5_smoke import AppServerClient
from epiphany_phase5_smoke import ROOT
from epiphany_phase6_reorient_launch_smoke import BINDING_ID as REORIENT_BINDING_ID
DEFAULT_CODEX_HOME = ROOT / ".epiphany-gui" / "codex-home"
DEFAULT_ARTIFACT_ROOT = ROOT / ".epiphany-gui" / "actions"
def checkpoint_patch(cwd: Path) -> dict[str, Any]:
generated_at = datetime.now(timezone.utc).isoformat()
return {
"objective": "Reach a testable Epiphany MVP through the local GUI/operator workflow.",
"activeSubgoalId": "epiphany-gui-mvp",
"subgoals": [
{
"id": "epiphany-gui-mvp",
"title": "Make the Epiphany GUI usable for local operator testing",
"status": "active",
"summary": "The operator can prepare durable state, inspect coordinator guidance, and launch/read fixed specialist lanes without terminal work.",
}
],
"graphs": {
"architecture": {
"nodes": [
{
"id": "gui-operator-console",
"title": "Tauri/React operator console",
"purpose": "Human-facing surface for status, artifacts, and bounded lane actions.",
"code_refs": [
{
"path": "apps/epiphany-gui/src/App.tsx",
"start_line": 1,
"end_line": 1,
"symbol": "App",
}
],
},
{
"id": "gui-action-bridge",
"title": "GUI action bridge",
"purpose": "Python bridge that invokes fixed app-server Epiphany lane APIs and writes auditable artifacts.",
"code_refs": [
{
"path": "tools/epiphany_gui_action.py",
"start_line": 1,
"end_line": 1,
"symbol": "run_action",
}
],
},
{
"id": "app-server-epiphany-control-plane",
"title": "App-server Epiphany control plane",
"purpose": "Typed state, coordinator, CRRC, role, reorient, and job surfaces used by the GUI.",
"code_refs": [
{
"path": "vendor/codex/codex-rs/app-server/src/codex_message_processor.rs",
"start_line": 1,
"end_line": 1,
"symbol": "thread/epiphany",
}
],
},
]
},
"dataflow": {
"nodes": [
{
"id": "operator-action-flow",
"title": "Operator action flow",
"purpose": "GUI button -> Tauri command -> Python bridge -> app-server API -> artifact bundle -> refreshed GUI status.",
}
]
},
"links": [
{
"dataflow_node_id": "operator-action-flow",
"architecture_node_id": "gui-operator-console",
"relationship": "operator-surface",
},
{
"dataflow_node_id": "operator-action-flow",
"architecture_node_id": "gui-action-bridge",
"relationship": "bridge",
},
{
"dataflow_node_id": "operator-action-flow",
"architecture_node_id": "app-server-epiphany-control-plane",
"relationship": "control-plane",
},
],
},
"graphFrontier": {
"active_node_ids": [
"gui-operator-console",
"gui-action-bridge",
"app-server-epiphany-control-plane",
],
"dirty_paths": [],
},
"graphCheckpoint": {
"checkpoint_id": f"gui-operator-{int(time.time())}",
"graph_revision": 1,
"summary": "GUI-prepared checkpoint for local Epiphany MVP operator testing.",
"frontier_node_ids": [
"gui-operator-console",
"gui-action-bridge",
"app-server-epiphany-control-plane",
],
},
"investigationCheckpoint": {
"checkpoint_id": f"ix-gui-operator-{int(time.time())}",
"kind": "source_gathering",
"disposition": "resume_ready",
"focus": "Validate the GUI operator loop against the fixed Epiphany lane APIs.",
"summary": "The GUI is the operator surface; app-server typed state remains the source of truth.",
"next_action": "Use the GUI to run coordinator plan, launch/read specialist lanes, and inspect written artifacts.",
"captured_at_turn_id": f"gui-prepare-{int(time.time())}",
"code_refs": [
{
"path": "apps/epiphany-gui/src/App.tsx",
"start_line": 1,
"end_line": 1,
"symbol": "App",
},
{
"path": "tools/epiphany_gui_action.py",
"start_line": 1,
"end_line": 1,
"symbol": "run_action",
},
],
},
"scratch": {
"summary": "Prepared from the GUI so the operator has a durable thread before launching fixed lanes.",
"current_focus": "Test the local GUI MVP workflow end to end.",
"next_action": "Run coordinator guidance, then launch/read modeling, verification, or reorient lanes as recommended.",
"updated_at": generated_at,
"cwd": str(cwd),
},
"churn": {
"understanding_status": "ready",
"diff_pressure": "low",
"graph_freshness": "fresh",
"unexplained_writes": 0,
},
}
def write_json(path: Path, value: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(value, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
def write_text(path: Path, value: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(value, encoding="utf-8")
def state_revision(status: dict[str, Any]) -> int | None:
state = status.get("read", {}).get("thread", {}).get("epiphanyState")
if isinstance(state, dict):
revision = state.get("revision")
if isinstance(revision, int):
return revision
scene_revision = status.get("scene", {}).get("scene", {}).get("revision")
return scene_revision if isinstance(scene_revision, int) else None
def require_thread_id(thread_id: str | None, action: str) -> str:
if not thread_id:
raise ValueError(f"{action} requires a persistent thread id")
return thread_id
def is_unresumable_empty_thread_error(error: RuntimeError) -> bool:
message = str(error)
return "no rollout found for thread id" in message or "thread not loaded" in message
def run_action(args: argparse.Namespace) -> dict[str, Any]:
thread_id = args.thread_id or ""
if args.action != "prepareCheckpoint":
thread_id = require_thread_id(args.thread_id, args.action)
codex_home = args.codex_home.resolve()
artifact_dir = args.artifact_root.resolve() / f"{args.action}-{time.time_ns()}-{os.getpid()}"
transcript_path = artifact_dir / "transcript.jsonl"
stderr_path = artifact_dir / "server.stderr.log"
cwd = args.cwd.resolve()
codex_home.mkdir(parents=True, exist_ok=True)
artifact_dir.mkdir(parents=True, exist_ok=True)
with AppServerClient(args.app_server.resolve(), codex_home, transcript_path, stderr_path) as client:
client.send(
"initialize",
{
"clientInfo": {
"name": "epiphany-gui-action",
"title": "Epiphany GUI Action",
"version": "0.1.0",
},
"capabilities": {"experimentalApi": True},
},
)
client.send("initialized", expect_response=False)
if args.action == "prepareCheckpoint":
if args.thread_id:
try:
client.send("thread/resume", {"threadId": args.thread_id})
except RuntimeError as error:
if not is_unresumable_empty_thread_error(error):
raise
thread_id = ""
if not thread_id:
started = client.send("thread/start", {"cwd": str(cwd), "ephemeral": False})
if started is None:
raise RuntimeError("thread/start returned no response")
thread_id = started["thread"]["id"]
before = collect_status(client, thread_id=thread_id, cwd=cwd, ephemeral=True)
revision = state_revision(before)
response = client.send(
"thread/epiphany/update",
{
"threadId": thread_id,
"expectedRevision": revision if revision is not None else 0,
"patch": checkpoint_patch(cwd),
},
)
summary = "Prepared durable Epiphany checkpoint."
else:
before = collect_status(client, thread_id=thread_id, cwd=cwd, ephemeral=False)
revision = state_revision(before)
if args.action in {"launchModeling", "launchVerification"}:
role_id = "modeling" if args.action == "launchModeling" else "verification"
payload: dict[str, Any] = {
"threadId": thread_id,
"roleId": role_id,
"maxRuntimeSeconds": args.max_runtime_seconds,
}
if revision is not None:
payload["expectedRevision"] = revision
response = client.send("thread/epiphany/roleLaunch", payload)
summary = f"Launched {role_id} role worker."
elif args.action in {"readModelingResult", "readVerificationResult"}:
role_id = "modeling" if args.action == "readModelingResult" else "verification"
response = client.send(
"thread/epiphany/roleResult", {"threadId": thread_id, "roleId": role_id}
)
summary = f"Read {role_id} role result."
elif args.action == "launchReorient":
payload = {"threadId": thread_id, "maxRuntimeSeconds": args.max_runtime_seconds}
if revision is not None:
payload["expectedRevision"] = revision
response = client.send("thread/epiphany/reorientLaunch", payload)
summary = "Launched fixed reorient-worker."
elif args.action == "readReorientResult":
response = client.send(
"thread/epiphany/reorientResult",
{"threadId": thread_id, "bindingId": REORIENT_BINDING_ID},
)
summary = "Read reorient-worker result."
elif args.action == "acceptReorient":
if revision is None:
raise ValueError("acceptReorient requires ready Epiphany state with a revision")
response = client.send(
"thread/epiphany/reorientAccept",
{
"threadId": thread_id,
"expectedRevision": revision,
"bindingId": REORIENT_BINDING_ID,
"updateScratch": True,
"updateInvestigationCheckpoint": True,
},
)
summary = "Accepted reviewed reorientation finding."
else:
raise ValueError(f"unsupported GUI action: {args.action}")
assert response is not None
after = collect_status(client, thread_id=thread_id, cwd=cwd, ephemeral=True)
write_json(artifact_dir / "before-status.json", before)
write_json(artifact_dir / "action-response.json", response)
write_json(artifact_dir / "after-status.json", after)
write_text(artifact_dir / "after-status.txt", render_status(after))
result = {
"action": args.action,
"artifactPath": str(artifact_dir),
"summary": summary,
"threadId": thread_id,
"response": response,
}
write_json(artifact_dir / "gui-action-summary.json", result)
return result
def main() -> int:
parser = argparse.ArgumentParser(description="Run one bounded Epiphany GUI action.")
parser.add_argument("--app-server", type=Path, default=DEFAULT_APP_SERVER)
parser.add_argument("--codex-home", type=Path, default=DEFAULT_CODEX_HOME)
parser.add_argument("--cwd", type=Path, default=ROOT)
parser.add_argument("--thread-id")
parser.add_argument("--artifact-root", type=Path, default=DEFAULT_ARTIFACT_ROOT)
parser.add_argument("--max-runtime-seconds", type=int, default=180)
parser.add_argument(
"--action",
required=True,
choices=[
"launchModeling",
"readModelingResult",
"launchVerification",
"readVerificationResult",
"launchReorient",
"readReorientResult",
"acceptReorient",
"prepareCheckpoint",
],
)
args = parser.parse_args()
print(json.dumps(run_action(args), indent=2, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -66,6 +66,8 @@ def collect_status(
if started is None:
raise RuntimeError("thread/start returned no response")
thread_id = started["thread"]["id"]
elif not ephemeral:
client.send("thread/resume", {"threadId": thread_id})
read = client.send("thread/read", {"threadId": thread_id, "includeTurns": False})
scene = client.send("thread/epiphany/scene", {"threadId": thread_id})