Add usable GUI lane controls
This commit is contained in:
parent
21a05368d3
commit
b328f68352
12 changed files with 690 additions and 61 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
341
tools/epiphany_gui_action.py
Normal file
341
tools/epiphany_gui_action.py
Normal 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())
|
||||
|
|
@ -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})
|
||||
|
|
|
|||
Reference in a new issue