Move Aquarium UI to sibling repo
This commit is contained in:
parent
4c2943ddff
commit
96f04b2a74
28 changed files with 52 additions and 16443 deletions
8
apps/README.md
Normal file
8
apps/README.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Apps
|
||||
|
||||
The Aquarium operator UI moved to its own sibling repository:
|
||||
|
||||
`E:\Projects\EpiphanyAquarium`
|
||||
|
||||
EpiphanyAgent remains the harness, typed-state, coordinator, bridge, and backend
|
||||
repo. Aquarium is the Tauri/React/WebGL interface organism over those surfaces.
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
# Epiphany GUI
|
||||
|
||||
Local Tauri v2 + React operator console for the Epiphany MVP.
|
||||
|
||||
This app is not a source of truth. It reflects the existing app-server and
|
||||
artifact surfaces so the operator can inspect coordinator, CRRC, role, job, and
|
||||
artifact state without working through a terminal.
|
||||
|
||||
Current slice:
|
||||
|
||||
- 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
|
||||
- resolves project-pinned Unity editors through the runtime bridge and writes
|
||||
audit artifacts instead of letting workers launch PATH/default editors
|
||||
- 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:
|
||||
|
||||
```powershell
|
||||
npm install
|
||||
npm run dev
|
||||
npm run tauri dev
|
||||
```
|
||||
|
||||
Visual smoke:
|
||||
|
||||
```powershell
|
||||
npm run smoke:visual
|
||||
```
|
||||
|
||||
The smoke runs the React shell in a browser with the bundled sample operator
|
||||
snapshot, clicks the bounded operator buttons, and writes:
|
||||
|
||||
- `.epiphany-gui/operator-console-smoke-desktop.png`
|
||||
- `.epiphany-gui/operator-console-smoke-mobile.png`
|
||||
|
||||
The status bridge expects the local app-server binary used by the existing
|
||||
smoke tools. Build it from `vendor/codex/codex-rs` if needed:
|
||||
|
||||
```powershell
|
||||
$env:CARGO_TARGET_DIR='C:\Users\Meta\.cargo-target-codex'
|
||||
cargo build -p codex-app-server
|
||||
```
|
||||
|
||||
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`.
|
||||
- **Inspect Unity** runs `tools/epiphany_unity_bridge.py inspect` and writes a
|
||||
pinned-editor inspection bundle under `.epiphany-gui/runtime`.
|
||||
- **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.
|
||||
|
||||
Raw transcripts, stderr logs, full turn logs, direct worker messages, and
|
||||
`rawResult` payloads are sealed forensic artifacts. The operator console should
|
||||
use projected status, finding summaries, state patches, coordinator
|
||||
recommendations, and manifests by default; open sealed artifacts only for
|
||||
explicit debugging.
|
||||
|
||||
For more detail without reading the worker's direct stream, use the generated
|
||||
`agent-function-telemetry.json` files. They summarize API/function-call shape,
|
||||
job/status/path counts, and visible tool names while sealing text and raw
|
||||
results.
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Epiphany Operator</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
2220
apps/epiphany-gui/package-lock.json
generated
2220
apps/epiphany-gui/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"name": "epiphany-gui",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 127.0.0.1 --port 1420",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview --host 127.0.0.1 --port 1421",
|
||||
"smoke:visual": "node scripts/visual-smoke.mjs",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@epiphanygraph/epiphany-graph-viewer": "file:../../../EpiphanyGraph/web/epiphany-graph-viewer",
|
||||
"@tauri-apps/api": "^2.0.0",
|
||||
"lucide-react": "^0.468.0",
|
||||
"midi-file": "^1.2.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.0.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"playwright": "^1.59.1",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^6.0.1"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,487 +0,0 @@
|
|||
{
|
||||
"generatedAt": "1777475847000",
|
||||
"repoRoot": "E:\\Projects\\EpiphanyAgent",
|
||||
"status": {
|
||||
"threadId": "019dd9d1-045b-7f13-b0e1-38ed89b31495",
|
||||
"read": {
|
||||
"thread": {
|
||||
"id": "019dd9d1-045b-7f13-b0e1-38ed89b31495",
|
||||
"epiphanyState": {
|
||||
"graphs": {
|
||||
"architecture": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "gui-operator-console",
|
||||
"title": "Tauri/React operator console",
|
||||
"purpose": "Human-facing surface for status, artifacts, graph state, and bounded lane actions.",
|
||||
"code_refs": [
|
||||
{
|
||||
"path": "apps/epiphany-gui/src/App.tsx",
|
||||
"start_line": 1,
|
||||
"symbol": "App"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rider-context-bridge",
|
||||
"title": "Rider context bridge",
|
||||
"purpose": "Operator-safe source, solution, VCS, and selected-code context receipts for modeling and review.",
|
||||
"code_refs": [
|
||||
{
|
||||
"path": "tools/epiphany_rider_bridge.py",
|
||||
"start_line": 1,
|
||||
"symbol": "status_rider"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "unity-runtime-bridge",
|
||||
"title": "Unity runtime bridge",
|
||||
"purpose": "Pinned-editor runtime, package, compilation, probe, and test receipts for Unity projects.",
|
||||
"code_refs": [
|
||||
{
|
||||
"path": "tools/epiphany_unity_bridge.py",
|
||||
"start_line": 1,
|
||||
"symbol": "probe_unity"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source_id": "gui-operator-console",
|
||||
"target_id": "rider-context-bridge",
|
||||
"kind": "requests-source-context",
|
||||
"label": "Rider facts"
|
||||
},
|
||||
{
|
||||
"source_id": "gui-operator-console",
|
||||
"target_id": "unity-runtime-bridge",
|
||||
"kind": "requests-runtime-facts",
|
||||
"label": "Unity facts"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dataflow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "operator-bridge-flow",
|
||||
"title": "Operator bridge flow",
|
||||
"purpose": "GUI button or Rider plugin action writes an artifact, then Epiphany reads the projected receipt."
|
||||
},
|
||||
{
|
||||
"id": "graph-review-flow",
|
||||
"title": "Graph review flow",
|
||||
"purpose": "Modeling state renders into a graph viewer, code refs become Rider-friendly navigation targets."
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source_id": "operator-bridge-flow",
|
||||
"target_id": "graph-review-flow",
|
||||
"kind": "feeds-review"
|
||||
}
|
||||
]
|
||||
},
|
||||
"links": [
|
||||
{
|
||||
"dataflow_node_id": "operator-bridge-flow",
|
||||
"architecture_node_id": "gui-operator-console",
|
||||
"relationship": "operator-surface"
|
||||
},
|
||||
{
|
||||
"dataflow_node_id": "operator-bridge-flow",
|
||||
"architecture_node_id": "rider-context-bridge",
|
||||
"relationship": "source-organ"
|
||||
},
|
||||
{
|
||||
"dataflow_node_id": "operator-bridge-flow",
|
||||
"architecture_node_id": "unity-runtime-bridge",
|
||||
"relationship": "runtime-organ"
|
||||
},
|
||||
{
|
||||
"dataflow_node_id": "graph-review-flow",
|
||||
"architecture_node_id": "gui-operator-console",
|
||||
"relationship": "graph-dashboard"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scene": {
|
||||
"scene": {
|
||||
"availableActions": [
|
||||
"index",
|
||||
"retrieve",
|
||||
"distill",
|
||||
"context",
|
||||
"jobs",
|
||||
"roles",
|
||||
"planning",
|
||||
"coordinator",
|
||||
"roleLaunch",
|
||||
"roleResult",
|
||||
"jobLaunch",
|
||||
"freshness",
|
||||
"pressure",
|
||||
"reorient",
|
||||
"crrc",
|
||||
"update"
|
||||
],
|
||||
"source": "live",
|
||||
"stateStatus": "missing"
|
||||
}
|
||||
},
|
||||
"pressure": {
|
||||
"pressure": {
|
||||
"basis": "unknown",
|
||||
"level": "unknown",
|
||||
"note": "No token usage telemetry has been recorded for this thread yet.",
|
||||
"shouldPrepareCompaction": false,
|
||||
"status": "unknown"
|
||||
}
|
||||
},
|
||||
"reorient": {
|
||||
"decision": {
|
||||
"action": "regather",
|
||||
"checkpointStatus": "missing",
|
||||
"graphStatus": "missing",
|
||||
"nextAction": "Re-gather source context before editing.",
|
||||
"note": "No Epiphany state survived, so there is no authoritative checkpoint to resume.",
|
||||
"pressureLevel": "unknown",
|
||||
"reasons": ["missingState", "missingCheckpoint"],
|
||||
"retrievalStatus": "ready",
|
||||
"watcherStatus": "clean"
|
||||
}
|
||||
},
|
||||
"jobs": {
|
||||
"jobs": [
|
||||
{
|
||||
"id": "retrieval-index",
|
||||
"kind": "indexing",
|
||||
"ownerRole": "epiphany-core",
|
||||
"progressNote": "Retrieval catalog is ready.",
|
||||
"scope": "E:\\Projects\\EpiphanyAgent",
|
||||
"status": "idle"
|
||||
},
|
||||
{
|
||||
"blockingReason": "Epiphany state is missing, so there is no graph to remap.",
|
||||
"id": "graph-remap",
|
||||
"kind": "remap",
|
||||
"ownerRole": "epiphany-harness",
|
||||
"scope": "architecture/dataflow graphs",
|
||||
"status": "blocked"
|
||||
},
|
||||
{
|
||||
"blockingReason": "Epiphany state is missing, so there are no invariants to verify.",
|
||||
"id": "verification",
|
||||
"kind": "verification",
|
||||
"ownerRole": "epiphany-harness",
|
||||
"scope": "invariants/evidence",
|
||||
"status": "blocked"
|
||||
}
|
||||
]
|
||||
},
|
||||
"roles": {
|
||||
"roles": [
|
||||
{
|
||||
"id": "implementation",
|
||||
"note": "Wait for CRRC action: RegatherManually.",
|
||||
"ownerRole": "coding-agent",
|
||||
"status": "blocked",
|
||||
"title": "Implementation"
|
||||
},
|
||||
{
|
||||
"id": "imagination",
|
||||
"note": "Planning material ready: 2 captures, 1 backlog items, 1 roadmap streams, 1 objective drafts.",
|
||||
"ownerRole": "epiphany-imagination",
|
||||
"recommendedAction": "roleLaunch",
|
||||
"status": "ready",
|
||||
"title": "Imagination / Planning"
|
||||
},
|
||||
{
|
||||
"id": "modeling",
|
||||
"note": "Checkpoint missing: Re-gather source context before editing.",
|
||||
"ownerRole": "epiphany-modeler",
|
||||
"recommendedAction": "update",
|
||||
"status": "needed",
|
||||
"title": "Modeling / Checkpoint"
|
||||
},
|
||||
{
|
||||
"id": "verification",
|
||||
"note": "Epiphany state is missing, so there are no invariants to verify.",
|
||||
"ownerRole": "epiphany-verifier",
|
||||
"status": "blocked",
|
||||
"title": "Verification / Review"
|
||||
},
|
||||
{
|
||||
"id": "reorientation",
|
||||
"note": "Regather verdict, result MissingState, pressure Unknown. No authoritative Epiphany state exists; re-gather source context before editing.",
|
||||
"ownerRole": "epiphany-reorient",
|
||||
"status": "needed",
|
||||
"title": "Reorientation"
|
||||
}
|
||||
]
|
||||
},
|
||||
"planning": {
|
||||
"threadId": "019dd9d1-045b-7f13-b0e1-38ed89b31495",
|
||||
"source": "live",
|
||||
"stateStatus": "ready",
|
||||
"stateRevision": 3,
|
||||
"planning": {
|
||||
"workspace_root": "E:\\Projects\\EpiphanyAgent",
|
||||
"captures": [
|
||||
{
|
||||
"id": "capture-github-planning",
|
||||
"title": "Import GitHub issue into planning inbox",
|
||||
"body": "Issues should become planning captures before becoming objectives.",
|
||||
"confidence": "observed",
|
||||
"status": "inbox",
|
||||
"speaker": "human",
|
||||
"tags": ["github", "backlog"],
|
||||
"source": {
|
||||
"kind": "github_issue",
|
||||
"provider": "github",
|
||||
"repo": "GameCult/EpiphanyAgent",
|
||||
"issue_number": 42,
|
||||
"url": "https://github.com/GameCult/EpiphanyAgent/issues/42",
|
||||
"state": "open",
|
||||
"labels": ["planning", "mvp"],
|
||||
"assignees": ["Meta"],
|
||||
"author": "Meta",
|
||||
"created_at": "2026-05-01T10:00:00Z",
|
||||
"updated_at": "2026-05-01T10:05:00Z",
|
||||
"imported_at": "2026-05-01T10:10:00Z"
|
||||
},
|
||||
"created_at": "2026-05-01T10:10:00Z",
|
||||
"updated_at": "2026-05-01T10:10:00Z"
|
||||
},
|
||||
{
|
||||
"id": "capture-chat-planning",
|
||||
"title": "Draft objectives from chat after bounding them",
|
||||
"body": "Discussion remains planning material until it becomes a firm artifact.",
|
||||
"confidence": "medium",
|
||||
"status": "triaged",
|
||||
"speaker": "human",
|
||||
"tags": ["chat", "objective"],
|
||||
"source": {
|
||||
"kind": "chat",
|
||||
"uri": "codex://threads/planning-smoke",
|
||||
"external_id": "turn-planning-smoke"
|
||||
},
|
||||
"created_at": "2026-05-01T10:12:00Z",
|
||||
"updated_at": "2026-05-01T10:12:00Z"
|
||||
}
|
||||
],
|
||||
"backlog_items": [
|
||||
{
|
||||
"id": "backlog-planning-dashboard",
|
||||
"title": "Expose planning state in the Epiphany dashboard",
|
||||
"kind": "feature",
|
||||
"summary": "Show captures, backlog, roadmap streams, and objective drafts.",
|
||||
"status": "ready",
|
||||
"horizon": "near",
|
||||
"priority": {
|
||||
"value": "high",
|
||||
"rationale": "Planning needs a visible user review loop before automation.",
|
||||
"impact": "user-steering",
|
||||
"urgency": "soon",
|
||||
"confidence": "medium",
|
||||
"effort": "small",
|
||||
"unblocks": ["objective-adoption"]
|
||||
},
|
||||
"confidence": "medium",
|
||||
"product_area": "epiphany-gui",
|
||||
"lane_hints": ["imagination", "hands", "soul"],
|
||||
"acceptance_sketch": ["Dashboard can render typed planning records."],
|
||||
"source_refs": [],
|
||||
"updated_at": "2026-05-01T10:15:00Z"
|
||||
}
|
||||
],
|
||||
"roadmap_streams": [
|
||||
{
|
||||
"id": "stream-user-steering",
|
||||
"title": "Human-steered planning",
|
||||
"purpose": "Keep future work visible and bounded before implementation starts.",
|
||||
"status": "active",
|
||||
"item_ids": ["backlog-planning-dashboard"],
|
||||
"near_term_focus": "backlog-planning-dashboard",
|
||||
"review_cadence": "per objective"
|
||||
}
|
||||
],
|
||||
"objective_drafts": [
|
||||
{
|
||||
"id": "draft-planning-dashboard",
|
||||
"title": "Build the planning dashboard slice",
|
||||
"summary": "Render planning records and let the user adopt one bounded objective.",
|
||||
"source_item_ids": ["backlog-planning-dashboard"],
|
||||
"scope": {
|
||||
"includes": ["read-only planning projection", "objective draft review"],
|
||||
"excludes": ["automatic objective adoption"]
|
||||
},
|
||||
"acceptance_criteria": [
|
||||
"Planning records render without changing thread state.",
|
||||
"A draft objective remains review-gated."
|
||||
],
|
||||
"evidence_required": ["live smoke", "GUI screenshot"],
|
||||
"lane_plan": {
|
||||
"imagination": "organize planning records into a bounded objective candidate",
|
||||
"eyes": "check prior art and GitHub issue metadata shape",
|
||||
"hands": "wire dashboard controls after the read-only surface is stable",
|
||||
"soul": "verify no planning record silently becomes active objective"
|
||||
},
|
||||
"risks": ["accidental objective adoption"],
|
||||
"review_gates": ["human adoption"],
|
||||
"status": "draft"
|
||||
}
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"captureCount": 2,
|
||||
"pendingCaptureCount": 1,
|
||||
"githubIssueCaptureCount": 1,
|
||||
"backlogItemCount": 1,
|
||||
"readyBacklogItemCount": 1,
|
||||
"roadmapStreamCount": 1,
|
||||
"objectiveDraftCount": 1,
|
||||
"draftObjectiveCount": 1,
|
||||
"activeObjective": "Reach a testable Epiphany MVP through the local GUI/operator workflow.",
|
||||
"note": "Planning substrate is available. These records are planning state only until a human explicitly adopts an objective."
|
||||
}
|
||||
},
|
||||
"roleResults": {
|
||||
"imagination": {
|
||||
"bindingId": "planning-synthesis-worker",
|
||||
"note": "No authoritative Epiphany state exists for this thread.",
|
||||
"roleId": "imagination",
|
||||
"status": "missingState"
|
||||
},
|
||||
"modeling": {
|
||||
"bindingId": "modeling-checkpoint-worker",
|
||||
"note": "No authoritative Epiphany state exists for this thread.",
|
||||
"roleId": "modeling",
|
||||
"status": "missingState"
|
||||
},
|
||||
"verification": {
|
||||
"bindingId": "verification-review-worker",
|
||||
"note": "No authoritative Epiphany state exists for this thread.",
|
||||
"roleId": "verification",
|
||||
"status": "missingState"
|
||||
}
|
||||
},
|
||||
"reorientResult": {
|
||||
"bindingId": "reorient-worker",
|
||||
"note": "No authoritative Epiphany state exists for this thread.",
|
||||
"status": "missingState"
|
||||
},
|
||||
"crrc": {
|
||||
"recommendation": {
|
||||
"action": "regatherManually",
|
||||
"reason": "No authoritative Epiphany state exists; re-gather source context before editing."
|
||||
}
|
||||
},
|
||||
"coordinator": {
|
||||
"action": "prepareCheckpoint",
|
||||
"canAutoRun": false,
|
||||
"reason": "Authoritative state or investigation checkpoint is missing; prepare a checkpoint before coordination can continue.",
|
||||
"recommendedSceneAction": "update",
|
||||
"requiresReview": false,
|
||||
"targetRole": "modeling"
|
||||
}
|
||||
},
|
||||
"artifacts": [
|
||||
{
|
||||
"name": "runtime/unity-inspect-sample",
|
||||
"path": "E:\\Projects\\EpiphanyAgent\\.epiphany-gui\\runtime\\unity-inspect-sample",
|
||||
"files": [
|
||||
"unity-bridge-inspection.md",
|
||||
"unity-bridge-summary.json"
|
||||
],
|
||||
"summaryPath": "E:\\Projects\\EpiphanyAgent\\.epiphany-gui\\runtime\\unity-inspect-sample\\unity-bridge-summary.json",
|
||||
"runtimeAudit": {
|
||||
"resultPath": "E:\\Projects\\EpiphanyAgent\\.epiphany-gui\\runtime\\unity-inspect-sample\\unity-bridge-summary.json",
|
||||
"status": "missingEditor",
|
||||
"projectPath": "E:\\Projects\\Aetheria-Economy",
|
||||
"projectVersion": "6000.1.10f1",
|
||||
"note": "Project pins Unity 6000.1.10f1, but no exact editor was found.",
|
||||
"editorBridge": {
|
||||
"exists": true,
|
||||
"path": "E:\\Projects\\Aetheria-Economy\\Assets\\Editor\\Epiphany\\EpiphanyEditorBridge.cs",
|
||||
"relativePath": "Assets/Editor/Epiphany/EpiphanyEditorBridge.cs",
|
||||
"executeMethod": "GameCult.Epiphany.Unity.EpiphanyEditorBridge.RunProbe"
|
||||
},
|
||||
"installedEditors": [
|
||||
{
|
||||
"version": "6000.4.2f1",
|
||||
"editorPath": "C:\\Program Files\\Unity\\Hub\\Editor\\6000.4.2f1\\Editor\\Unity.exe"
|
||||
}
|
||||
],
|
||||
"candidatePaths": [
|
||||
"C:\\Program Files\\Unity\\Hub\\Editor\\6000.1.10f1\\Editor\\Unity.exe",
|
||||
"C:\\Program Files (x86)\\Unity\\Hub\\Editor\\6000.1.10f1\\Editor\\Unity.exe"
|
||||
],
|
||||
"searchRoots": [
|
||||
"C:\\Program Files\\Unity\\Hub\\Editor",
|
||||
"C:\\Program Files (x86)\\Unity\\Hub\\Editor"
|
||||
]
|
||||
},
|
||||
"modifiedMillis": 1777475848000
|
||||
},
|
||||
{
|
||||
"name": "rider/rider-inspect-sample",
|
||||
"path": "E:\\Projects\\EpiphanyAgent\\.epiphany-gui\\rider\\rider-inspect-sample",
|
||||
"files": [
|
||||
"rider-bridge-status.md",
|
||||
"rider-bridge-summary.json"
|
||||
],
|
||||
"summaryPath": "E:\\Projects\\EpiphanyAgent\\.epiphany-gui\\rider\\rider-inspect-sample\\rider-bridge-summary.json",
|
||||
"riderAudit": {
|
||||
"resultPath": "E:\\Projects\\EpiphanyAgent\\.epiphany-gui\\rider\\rider-inspect-sample\\rider-bridge-summary.json",
|
||||
"status": "ready",
|
||||
"workspace": "E:\\Projects\\Aetheria-Economy",
|
||||
"solutionPath": "E:\\Projects\\Aetheria-Economy\\Aetheria.sln",
|
||||
"solutionStatus": "ready",
|
||||
"riderPath": "C:\\Program Files\\JetBrains\\JetBrains Rider 2026.1.0.1\\bin\\rider64.exe",
|
||||
"installationCount": 1,
|
||||
"note": "Rider executable is available for explicit source navigation and context capture.",
|
||||
"vcs": {
|
||||
"status": "ready",
|
||||
"branch": "codex/epiphany-unity-editor-bridge",
|
||||
"dirty": false,
|
||||
"changedFiles": [
|
||||
"Assets/Editor/Epiphany/EpiphanyEditorBridge.cs"
|
||||
],
|
||||
"stagedFiles": [],
|
||||
"changedRangesKnown": true
|
||||
},
|
||||
"installations": [
|
||||
{
|
||||
"path": "C:\\Program Files\\JetBrains\\JetBrains Rider 2026.1.0.1\\bin\\rider64.exe",
|
||||
"versionHint": "JetBrains Rider 2026.1.0.1"
|
||||
}
|
||||
],
|
||||
"searchRoots": [
|
||||
"C:\\Program Files\\JetBrains"
|
||||
]
|
||||
},
|
||||
"modifiedMillis": 1777475847500
|
||||
},
|
||||
{
|
||||
"name": "mvp-loop-selfdogfood-vanilla2-20260429",
|
||||
"path": "E:\\Projects\\EpiphanyAgent\\.epiphany-dogfood\\mvp-loop-selfdogfood-vanilla2-20260429",
|
||||
"files": [
|
||||
"artifact-manifest.json",
|
||||
"comparison.md",
|
||||
"epiphany-dogfood-summary.json",
|
||||
"epiphany-final-status.json",
|
||||
"epiphany-transcript.jsonl",
|
||||
"vanilla-reference.md"
|
||||
],
|
||||
"summaryPath": "E:\\Projects\\EpiphanyAgent\\.epiphany-dogfood\\mvp-loop-selfdogfood-vanilla2-20260429\\epiphany-dogfood-summary.json",
|
||||
"finalStatusPath": "E:\\Projects\\EpiphanyAgent\\.epiphany-dogfood\\mvp-loop-selfdogfood-vanilla2-20260429\\epiphany-final-status.json",
|
||||
"comparisonPath": "E:\\Projects\\EpiphanyAgent\\.epiphany-dogfood\\mvp-loop-selfdogfood-vanilla2-20260429\\comparison.md",
|
||||
"modifiedMillis": 1777475847000
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,285 +0,0 @@
|
|||
import { chromium } from "playwright";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { createServer } from "vite";
|
||||
|
||||
const root = resolve(import.meta.dirname, "..", "..", "..");
|
||||
const artifactDir = resolve(root, ".epiphany-gui");
|
||||
const desktopScreenshotPath = resolve(artifactDir, "operator-console-smoke-desktop.png");
|
||||
const wideScreenshotPath = resolve(artifactDir, "operator-console-smoke-wide.png");
|
||||
const mobileScreenshotPath = resolve(artifactDir, "operator-console-smoke-mobile.png");
|
||||
const url = "http://127.0.0.1:1420";
|
||||
const fluidStorageKey = "epiphany:aquarium-fluid-params:v3";
|
||||
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
|
||||
const server = await createServer({
|
||||
root: resolve(root, "apps", "epiphany-gui"),
|
||||
logLevel: "silent",
|
||||
server: {
|
||||
host: "127.0.0.1",
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await server.listen();
|
||||
await waitForServer(url);
|
||||
const browser = await launchSmokeBrowser();
|
||||
const wide = await smokeViewport(browser, { width: 2048, height: 1024 }, wideScreenshotPath, false);
|
||||
const desktop = await smokeViewport(browser, { width: 1366, height: 900 }, desktopScreenshotPath, true);
|
||||
const mobile = await smokeViewport(browser, { width: 390, height: 844 }, mobileScreenshotPath, false);
|
||||
await browser.close();
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
screenshots: [wideScreenshotPath, desktopScreenshotPath, mobileScreenshotPath],
|
||||
probes: { wide, desktop, mobile },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
await server.close();
|
||||
}
|
||||
|
||||
async function launchSmokeBrowser() {
|
||||
const requestedChannel = process.env.EPIPHANY_SMOKE_BROWSER;
|
||||
const channels = requestedChannel ? [requestedChannel] : ["chrome", "msedge"];
|
||||
let lastError = null;
|
||||
for (const channel of channels) {
|
||||
try {
|
||||
return await chromium.launch({ channel, headless: true });
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
async function smokeViewport(browser, viewport, screenshotPath, exerciseFluidPanel) {
|
||||
const page = await browser.newPage({ viewport });
|
||||
await page.goto(url, { waitUntil: "networkidle" });
|
||||
await page.locator(".immersiveShell").waitFor();
|
||||
await page.locator('[data-agent-node="coordinator"]').waitFor({ state: "attached" });
|
||||
await page.locator(".agentSmokeCanvas").waitFor();
|
||||
await page.locator(".agentCrispCanvas").waitFor();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const smokeProbe = await probeCanvas(page, ".agentSmokeCanvas");
|
||||
const crispProbe = await probeCanvas(page, ".agentCrispCanvas");
|
||||
if (!smokeProbe.nonBlank) {
|
||||
throw new Error(`agent smoke canvas did not render: ${smokeProbe.reason}`);
|
||||
}
|
||||
|
||||
const quietSurface = await page.evaluate(() => !document.querySelector(".agentFocusSurface"));
|
||||
if (!quietSurface) {
|
||||
throw new Error("operator surface rendered before an aquarium object was selected");
|
||||
}
|
||||
|
||||
const projectionProbe = await page.evaluate(() => {
|
||||
const node = document.querySelector('[data-agent-node="coordinator"]');
|
||||
if (!(node instanceof HTMLElement)) return { ok: false, reason: "coordinator DOM node missing" };
|
||||
const style = node.style;
|
||||
const x = style.getPropertyValue("--agent-x");
|
||||
const y = style.getPropertyValue("--agent-y");
|
||||
const glow = Number.parseFloat(style.getPropertyValue("--agent-glow-pulse"));
|
||||
return {
|
||||
ok: x.endsWith("%") && y.endsWith("%") && Number.isFinite(glow),
|
||||
reason: `x=${x} y=${y} glow=${glow}`,
|
||||
};
|
||||
});
|
||||
if (!projectionProbe.ok) {
|
||||
throw new Error(`DOM agent projection was not synchronized: ${projectionProbe.reason}`);
|
||||
}
|
||||
|
||||
if (viewport.width >= 720) {
|
||||
const hoverBox = await page.locator('[data-agent-node="research"] .agentGlyph').boundingBox();
|
||||
if (!hoverBox) {
|
||||
throw new Error("research DOM node has no hover bounds");
|
||||
}
|
||||
await page.mouse.move(hoverBox.x + hoverBox.width / 2, hoverBox.y + hoverBox.height / 2);
|
||||
await page.waitForTimeout(600);
|
||||
let hoverProbe = await page.evaluate(() => {
|
||||
const node = document.querySelector('[data-agent-node="research"]');
|
||||
if (!(node instanceof HTMLElement)) return { ok: false, reason: "research DOM node missing" };
|
||||
const hover = Number.parseFloat(node.style.getPropertyValue("--agent-hover"));
|
||||
const acknowledgement = Number.parseFloat(node.style.getPropertyValue("--agent-ack"));
|
||||
return {
|
||||
ok: hover > 0.7 && acknowledgement >= 0,
|
||||
reason: `hover=${hover} ack=${acknowledgement}`,
|
||||
};
|
||||
});
|
||||
if (!hoverProbe.ok) {
|
||||
await page.locator('[data-agent-node="research"]').dispatchEvent("pointerenter", { bubbles: true });
|
||||
await page.locator('[data-agent-node="research"]').dispatchEvent("mouseenter", { bubbles: true });
|
||||
await page.waitForTimeout(600);
|
||||
hoverProbe = await page.evaluate(() => {
|
||||
const node = document.querySelector('[data-agent-node="research"]');
|
||||
if (!(node instanceof HTMLElement)) return { ok: false, reason: "research DOM node missing" };
|
||||
const hover = Number.parseFloat(node.style.getPropertyValue("--agent-hover"));
|
||||
const acknowledgement = Number.parseFloat(node.style.getPropertyValue("--agent-ack"));
|
||||
return {
|
||||
ok: hover > 0.7 && acknowledgement >= 0,
|
||||
reason: `hover=${hover} ack=${acknowledgement}`,
|
||||
};
|
||||
});
|
||||
}
|
||||
if (!hoverProbe.ok) {
|
||||
throw new Error(`DOM hover did not reach aquarium projection at ${viewport.width}x${viewport.height}: ${hoverProbe.reason}`);
|
||||
}
|
||||
const optionProbe = await page.evaluate(() => {
|
||||
const halo = document.querySelector('[data-agent-options="research"]');
|
||||
if (!(halo instanceof HTMLElement)) return { ok: false, reason: "research options missing" };
|
||||
const style = window.getComputedStyle(halo);
|
||||
const buttons = [...halo.querySelectorAll("button")].filter((button) => button instanceof HTMLButtonElement);
|
||||
const labels = buttons.map((button) => button.textContent?.trim()).filter(Boolean);
|
||||
const firstRect = buttons[0]?.getBoundingClientRect();
|
||||
const firstStyle = buttons[0] ? window.getComputedStyle(buttons[0]) : null;
|
||||
return {
|
||||
ok:
|
||||
style.opacity !== "0" &&
|
||||
firstStyle?.pointerEvents !== "none" &&
|
||||
labels.includes("State") &&
|
||||
labels.includes("Artifacts") &&
|
||||
Boolean(firstRect && firstRect.width > 20 && firstRect.height > 16),
|
||||
reason: `opacity=${style.opacity} haloPointer=${style.pointerEvents} buttonPointer=${firstStyle?.pointerEvents} labels=${labels.join(",")}`,
|
||||
};
|
||||
});
|
||||
if (!optionProbe.ok) {
|
||||
throw new Error(`DOM agent options did not open for research: ${optionProbe.reason}`);
|
||||
}
|
||||
}
|
||||
let audioProbe = null;
|
||||
if (viewport.width >= 720) {
|
||||
const researchBox = await page.locator('[data-agent-node="research"] .agentGlyph').boundingBox();
|
||||
if (!researchBox) {
|
||||
throw new Error("research DOM node has no clickable bounds");
|
||||
}
|
||||
await page.mouse.click(researchBox.x + researchBox.width / 2, researchBox.y + researchBox.height / 2);
|
||||
const operatorSurface = await page.evaluate(() => {
|
||||
const focus = document.querySelector('.agentStage [data-agent-focus="research"]');
|
||||
if (!(focus instanceof HTMLElement)) return { ok: false, reason: "research focus surface missing" };
|
||||
const style = window.getComputedStyle(focus);
|
||||
const surfaces = [".immersiveTopbar", ".deckRail", ".diegeticPanel"].every((selector) => {
|
||||
const element = focus.querySelector(selector);
|
||||
if (!(element instanceof HTMLElement)) return false;
|
||||
const rect = element.getBoundingClientRect();
|
||||
return rect.width > 20 && rect.height > 20;
|
||||
});
|
||||
return {
|
||||
ok: style.opacity !== "0" && style.visibility !== "hidden" && surfaces,
|
||||
reason: `opacity=${style.opacity} visibility=${style.visibility} surfaces=${surfaces}`,
|
||||
};
|
||||
});
|
||||
if (!operatorSurface.ok) {
|
||||
throw new Error(`agent-owned operator surface did not open: ${operatorSurface.reason}`);
|
||||
}
|
||||
try {
|
||||
await page.waitForFunction(() => {
|
||||
const audio = window.__epiphanyAquariumAudio;
|
||||
return audio?.state === "running" &&
|
||||
audio.vocalAgentCount >= 7 &&
|
||||
audio.lastBurstChirpDrivers >= 6 &&
|
||||
audio.spectral?.chirpDrivers === 6 &&
|
||||
audio.spectral?.lastBurstChoirVoices >= 3 &&
|
||||
audio.spectral?.reactiveFlushes >= 1 &&
|
||||
audio.spectral?.transientBins >= 24 &&
|
||||
audio.spectral?.vocalAgents >= 7 &&
|
||||
audio.spectral?.queuedFrames >= 2048 &&
|
||||
audio.lastBurst;
|
||||
}, null, { timeout: 5000 });
|
||||
} catch (error) {
|
||||
const audio = await page.evaluate(() => window.__epiphanyAquariumAudio ?? null);
|
||||
throw new Error(`aquarium audio did not wake correctly: ${JSON.stringify(audio)}`, { cause: error });
|
||||
}
|
||||
audioProbe = await page.evaluate(() => window.__epiphanyAquariumAudio ?? null);
|
||||
await page.locator('[data-agent-focus="research"] .deckRail button').nth(1).dispatchEvent("pointerdown", { bubbles: true });
|
||||
await page.waitForFunction(() => {
|
||||
const audio = window.__epiphanyAquariumAudio;
|
||||
return audio?.state === "running" && audio.interfaceHitCount >= 1;
|
||||
}, null, { timeout: 5000 });
|
||||
audioProbe = await page.evaluate(() => window.__epiphanyAquariumAudio ?? null);
|
||||
}
|
||||
|
||||
let persistedParams = null;
|
||||
if (exerciseFluidPanel) {
|
||||
await page.evaluate((key) => window.localStorage.removeItem(key), fluidStorageKey);
|
||||
await page.reload({ waitUntil: "networkidle" });
|
||||
await page.locator(".agentSmokeCanvas").waitFor();
|
||||
await page.waitForTimeout(700);
|
||||
const inspectorGuard = viewport.width >= 720 ? Math.min(230, viewport.height * 0.25) : 0;
|
||||
await page.mouse.click(viewport.width - 48, viewport.height - 48 - inspectorGuard);
|
||||
await page.waitForTimeout(120);
|
||||
const railY = Math.round(viewport.height * 0.47);
|
||||
await page.mouse.move(viewport.width - 226, railY);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(viewport.width - 46, railY, { steps: 8 });
|
||||
await page.mouse.up();
|
||||
persistedParams = await page.evaluate((key) => window.localStorage.getItem(key), fluidStorageKey);
|
||||
if (!persistedParams || !persistedParams.includes("timeScale")) {
|
||||
throw new Error("fluid parameter panel did not persist changed parameters");
|
||||
}
|
||||
}
|
||||
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||
const result = await page.evaluate(() => ({
|
||||
horizontalOverflow: document.documentElement.scrollWidth > document.documentElement.clientWidth + 1,
|
||||
}));
|
||||
await page.close();
|
||||
if (result.horizontalOverflow) {
|
||||
throw new Error(`visual smoke found horizontal overflow at ${viewport.width}x${viewport.height}`);
|
||||
}
|
||||
return { smokeProbe, crispProbe, audioProbe, persistedParams };
|
||||
}
|
||||
|
||||
async function probeCanvas(page, selector) {
|
||||
return page.locator(selector).evaluate((canvas) => {
|
||||
if (!(canvas instanceof HTMLCanvasElement) || canvas.width === 0 || canvas.height === 0) {
|
||||
return { nonBlank: false, reason: "canvas has no drawable dimensions" };
|
||||
}
|
||||
const gl = canvas.getContext("webgl2");
|
||||
if (gl) {
|
||||
const width = Math.min(12, canvas.width);
|
||||
const height = Math.min(12, canvas.height);
|
||||
const pixels = new Uint8Array(width * height * 4);
|
||||
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
||||
return {
|
||||
nonBlank: pixels.some((value) => value !== 0),
|
||||
reason: "webgl2 sample",
|
||||
};
|
||||
}
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) return { nonBlank: false, reason: "no readable canvas context" };
|
||||
const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
|
||||
const stride = Math.max(4, Math.floor(pixels.length / 4096 / 4) * 4);
|
||||
let nonBlank = false;
|
||||
for (let index = 0; index < pixels.length - 3; index += stride) {
|
||||
if (pixels[index] !== 0 || pixels[index + 1] !== 0 || pixels[index + 2] !== 0 || pixels[index + 3] !== 0) {
|
||||
nonBlank = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
nonBlank,
|
||||
reason: "2d whole-canvas sample",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForServer(target) {
|
||||
const deadline = Date.now() + 30000;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const response = await fetch(target);
|
||||
if (response.ok) return;
|
||||
} catch {
|
||||
// Server is still waking up.
|
||||
}
|
||||
await new Promise((resolveWait) => setTimeout(resolveWait, 500));
|
||||
}
|
||||
throw new Error("Vite server did not start.");
|
||||
}
|
||||
4967
apps/epiphany-gui/src-tauri/Cargo.lock
generated
4967
apps/epiphany-gui/src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,18 +0,0 @@
|
|||
[package]
|
||||
name = "epiphany-gui"
|
||||
version = "0.1.0"
|
||||
description = "Epiphany local operator console"
|
||||
authors = ["GameCult"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "epiphany_gui_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tauri = { version = "2", features = [] }
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fn main() {
|
||||
tauri_build::build();
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Default permissions for the Epiphany operator console.",
|
||||
"windows": ["main"],
|
||||
"permissions": ["core:default"]
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
|
|
@ -1,748 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct StatusRequest {
|
||||
thread_id: Option<String>,
|
||||
cwd: Option<String>,
|
||||
codex_home: Option<String>,
|
||||
app_server: Option<String>,
|
||||
planning_draft_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct OperatorActionResult {
|
||||
action: String,
|
||||
artifact_path: String,
|
||||
summary: String,
|
||||
thread_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ArtifactBundle {
|
||||
name: String,
|
||||
path: String,
|
||||
files: Vec<String>,
|
||||
summary_path: Option<String>,
|
||||
final_status_path: Option<String>,
|
||||
comparison_path: Option<String>,
|
||||
implementation_audit: Option<ImplementationAudit>,
|
||||
runtime_audit: Option<RuntimeAudit>,
|
||||
rider_audit: Option<RiderAudit>,
|
||||
modified_millis: Option<u128>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ImplementationAudit {
|
||||
result_path: String,
|
||||
workspace_changed: bool,
|
||||
tracked_diff_present: bool,
|
||||
changed_files: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RuntimeAudit {
|
||||
result_path: String,
|
||||
status: String,
|
||||
project_path: Option<String>,
|
||||
project_version: Option<String>,
|
||||
editor_path: Option<String>,
|
||||
note: Option<String>,
|
||||
editor_bridge: Option<EditorBridgeAudit>,
|
||||
installed_editors: Vec<InstalledUnityEditor>,
|
||||
candidate_paths: Vec<String>,
|
||||
search_roots: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RiderAudit {
|
||||
result_path: String,
|
||||
status: String,
|
||||
workspace: Option<String>,
|
||||
solution_path: Option<String>,
|
||||
solution_status: Option<String>,
|
||||
rider_path: Option<String>,
|
||||
installation_count: Option<u64>,
|
||||
note: Option<String>,
|
||||
vcs: Option<RiderVcsAudit>,
|
||||
installations: Vec<RiderInstallation>,
|
||||
search_roots: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RiderVcsAudit {
|
||||
status: Option<String>,
|
||||
branch: Option<String>,
|
||||
dirty: Option<bool>,
|
||||
changed_files: Vec<String>,
|
||||
staged_files: Vec<String>,
|
||||
changed_ranges_known: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RiderInstallation {
|
||||
path: Option<String>,
|
||||
version_hint: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct EditorBridgeAudit {
|
||||
exists: bool,
|
||||
path: Option<String>,
|
||||
relative_path: Option<String>,
|
||||
execute_method: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct InstalledUnityEditor {
|
||||
version: Option<String>,
|
||||
editor_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct OperatorSnapshot {
|
||||
generated_at: String,
|
||||
repo_root: String,
|
||||
status: Value,
|
||||
artifacts: Vec<ArtifactBundle>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn load_operator_snapshot(request: Option<StatusRequest>) -> Result<OperatorSnapshot, String> {
|
||||
let repo_root = repo_root()?;
|
||||
let status = load_status(&repo_root, request.unwrap_or_default())?;
|
||||
let artifacts = list_artifacts(&repo_root)?;
|
||||
Ok(OperatorSnapshot {
|
||||
generated_at: unix_millis().to_string(),
|
||||
repo_root: repo_root.display().to_string(),
|
||||
status,
|
||||
artifacts,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn run_operator_action(
|
||||
action: String,
|
||||
request: Option<StatusRequest>,
|
||||
) -> Result<OperatorActionResult, String> {
|
||||
let repo_root = repo_root()?;
|
||||
let request = request.unwrap_or_default();
|
||||
match action.as_str() {
|
||||
"statusSnapshot" => run_status_snapshot(&repo_root, request),
|
||||
"coordinatorPlan" => run_coordinator_plan(&repo_root, request),
|
||||
"inspectUnity" => run_unity_inspection(&repo_root, request),
|
||||
"inspectRider" => run_rider_inspection(&repo_root, request),
|
||||
"launchImagination"
|
||||
| "readImaginationResult"
|
||||
| "acceptImagination"
|
||||
| "launchModeling"
|
||||
| "readModelingResult"
|
||||
| "acceptModeling"
|
||||
| "launchVerification"
|
||||
| "readVerificationResult"
|
||||
| "acceptVerification"
|
||||
| "adoptObjectiveDraft"
|
||||
| "launchReorient"
|
||||
| "readReorientResult"
|
||||
| "acceptReorient"
|
||||
| "continueImplementation"
|
||||
| "prepareCheckpoint" => run_gui_action_bridge(&repo_root, request, action),
|
||||
_ => Err(format!("unknown operator action: {action}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_rider_inspection(
|
||||
repo_root: &Path,
|
||||
request: StatusRequest,
|
||||
) -> Result<OperatorActionResult, String> {
|
||||
let python = find_python()?;
|
||||
let artifact_root = repo_root.join(".epiphany-gui").join("rider");
|
||||
let workspace = request
|
||||
.cwd
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| repo_root.to_path_buf());
|
||||
|
||||
let mut command = Command::new(python);
|
||||
command
|
||||
.current_dir(repo_root)
|
||||
.arg(repo_root.join("tools").join("epiphany_rider_bridge.py"))
|
||||
.arg("status")
|
||||
.arg("--project-root")
|
||||
.arg(workspace)
|
||||
.arg("--artifact-root")
|
||||
.arg(artifact_root);
|
||||
let value = run_json_command(command, "rider inspection")?;
|
||||
let status = value
|
||||
.get("status")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("unknown");
|
||||
Ok(OperatorActionResult {
|
||||
action: "inspectRider".to_string(),
|
||||
artifact_path: json_string(&value, "artifactPath")?,
|
||||
summary: format!("Rider bridge inspection: {status}."),
|
||||
thread_id: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn run_unity_inspection(
|
||||
repo_root: &Path,
|
||||
request: StatusRequest,
|
||||
) -> Result<OperatorActionResult, String> {
|
||||
let python = find_python()?;
|
||||
let artifact_root = repo_root.join(".epiphany-gui").join("runtime");
|
||||
let workspace = request
|
||||
.cwd
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| repo_root.to_path_buf());
|
||||
|
||||
let mut command = Command::new(python);
|
||||
command
|
||||
.current_dir(repo_root)
|
||||
.arg(repo_root.join("tools").join("epiphany_unity_bridge.py"))
|
||||
.arg("inspect")
|
||||
.arg("--project-path")
|
||||
.arg(workspace)
|
||||
.arg("--artifact-root")
|
||||
.arg(artifact_root);
|
||||
let value = run_json_command(command, "unity inspection")?;
|
||||
let status = value
|
||||
.get("status")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("unknown");
|
||||
Ok(OperatorActionResult {
|
||||
action: "inspectUnity".to_string(),
|
||||
artifact_path: json_string(&value, "artifactPath")?,
|
||||
summary: format!("Unity bridge inspection: {status}."),
|
||||
thread_id: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_status(repo_root: &Path, request: StatusRequest) -> Result<Value, String> {
|
||||
let python = find_python()?;
|
||||
let status_script = repo_root.join("tools").join("epiphany_mvp_status.py");
|
||||
let workspace = request
|
||||
.cwd
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| repo_root.to_path_buf());
|
||||
let codex_home = request
|
||||
.codex_home
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| repo_root.join(".epiphany-gui").join("codex-home"));
|
||||
let transcript = repo_root
|
||||
.join(".epiphany-gui")
|
||||
.join("status-transcript.jsonl");
|
||||
let stderr = repo_root
|
||||
.join(".epiphany-gui")
|
||||
.join("status-server.stderr.log");
|
||||
|
||||
let mut command = Command::new(python);
|
||||
command
|
||||
.current_dir(repo_root)
|
||||
.arg(status_script)
|
||||
.arg("--json")
|
||||
.arg("--cwd")
|
||||
.arg(workspace)
|
||||
.arg("--codex-home")
|
||||
.arg(codex_home)
|
||||
.arg("--transcript")
|
||||
.arg(transcript)
|
||||
.arg("--stderr")
|
||||
.arg(stderr)
|
||||
.arg("--no-ephemeral");
|
||||
|
||||
if let Some(thread_id) = request.thread_id {
|
||||
command.arg("--thread-id").arg(thread_id);
|
||||
}
|
||||
if let Some(app_server) = request.app_server {
|
||||
command.arg("--app-server").arg(app_server);
|
||||
}
|
||||
|
||||
let output = command
|
||||
.output()
|
||||
.map_err(|err| format!("failed to run Epiphany status bridge: {err}"))?;
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!(
|
||||
"Epiphany status bridge exited with {}: {}",
|
||||
output.status, stderr
|
||||
));
|
||||
}
|
||||
|
||||
serde_json::from_slice(&output.stdout)
|
||||
.map_err(|err| format!("failed to parse Epiphany status JSON: {err}"))
|
||||
}
|
||||
|
||||
fn run_status_snapshot(
|
||||
repo_root: &Path,
|
||||
request: StatusRequest,
|
||||
) -> Result<OperatorActionResult, String> {
|
||||
let python = find_python()?;
|
||||
let artifact_root = repo_root
|
||||
.join(".epiphany-gui")
|
||||
.join("status-snapshots")
|
||||
.join(unix_millis().to_string());
|
||||
fs::create_dir_all(&artifact_root)
|
||||
.map_err(|err| format!("failed to create status artifact dir: {err}"))?;
|
||||
let result_path = artifact_root.join("status.json");
|
||||
let transcript_path = artifact_root.join("transcript.jsonl");
|
||||
let stderr_path = artifact_root.join("server.stderr.log");
|
||||
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_mvp_status.py"))
|
||||
.arg("--json")
|
||||
.arg("--cwd")
|
||||
.arg(workspace)
|
||||
.arg("--codex-home")
|
||||
.arg(codex_home)
|
||||
.arg("--result")
|
||||
.arg(&result_path)
|
||||
.arg("--transcript")
|
||||
.arg(transcript_path)
|
||||
.arg("--stderr")
|
||||
.arg(stderr_path)
|
||||
.arg("--no-ephemeral");
|
||||
if let Some(thread_id) = request.thread_id {
|
||||
command.arg("--thread-id").arg(thread_id);
|
||||
}
|
||||
if let Some(app_server) = request.app_server {
|
||||
command.arg("--app-server").arg(app_server);
|
||||
}
|
||||
run_command(command, "status snapshot")?;
|
||||
Ok(OperatorActionResult {
|
||||
action: "statusSnapshot".to_string(),
|
||||
artifact_path: artifact_root.display().to_string(),
|
||||
summary: "Status snapshot written.".to_string(),
|
||||
thread_id: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn run_coordinator_plan(
|
||||
repo_root: &Path,
|
||||
request: StatusRequest,
|
||||
) -> Result<OperatorActionResult, String> {
|
||||
let python = find_python()?;
|
||||
let artifact_dir = repo_root
|
||||
.join(".epiphany-dogfood")
|
||||
.join(format!("gui-coordinator-plan-{}", unix_millis()));
|
||||
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_mvp_coordinator.py"))
|
||||
.arg("--mode")
|
||||
.arg("plan")
|
||||
.arg("--max-steps")
|
||||
.arg("2")
|
||||
.arg("--cwd")
|
||||
.arg(workspace)
|
||||
.arg("--codex-home")
|
||||
.arg(codex_home)
|
||||
.arg("--artifact-dir")
|
||||
.arg(&artifact_dir);
|
||||
if let Some(thread_id) = request.thread_id {
|
||||
command.arg("--thread-id").arg(thread_id);
|
||||
}
|
||||
if let Some(app_server) = request.app_server {
|
||||
command.arg("--app-server").arg(app_server);
|
||||
}
|
||||
run_command(command, "coordinator plan")?;
|
||||
Ok(OperatorActionResult {
|
||||
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);
|
||||
}
|
||||
if let Some(planning_draft_id) = request.planning_draft_id {
|
||||
command.arg("--planning-draft-id").arg(planning_draft_id);
|
||||
}
|
||||
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),
|
||||
})
|
||||
}
|
||||
|
||||
fn run_command(mut command: Command, label: &str) -> Result<(), String> {
|
||||
let output = command
|
||||
.output()
|
||||
.map_err(|err| format!("failed to run {label}: {err}"))?;
|
||||
if output.status.success() {
|
||||
return Ok(());
|
||||
}
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
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 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/",
|
||||
)?;
|
||||
collect_artifact_root(
|
||||
&mut bundles,
|
||||
&repo_root.join(".epiphany-gui").join("runtime"),
|
||||
"runtime/",
|
||||
)?;
|
||||
collect_artifact_root(
|
||||
&mut bundles,
|
||||
&repo_root.join(".epiphany-gui").join("rider"),
|
||||
"rider/",
|
||||
)?;
|
||||
|
||||
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(());
|
||||
}
|
||||
|
||||
for entry in fs::read_dir(&root).map_err(|err| format!("failed to read artifacts: {err}"))? {
|
||||
let entry = entry.map_err(|err| format!("failed to read artifact entry: {err}"))?;
|
||||
let path = entry.path();
|
||||
if !path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let mut files = Vec::new();
|
||||
for file in
|
||||
fs::read_dir(&path).map_err(|err| format!("failed to read artifact bundle: {err}"))?
|
||||
{
|
||||
let file = file.map_err(|err| format!("failed to read artifact file: {err}"))?;
|
||||
if file.path().is_file() {
|
||||
files.push(file.file_name().to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
files.sort();
|
||||
let 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: format!("{name_prefix}{raw_name}"),
|
||||
path: path.display().to_string(),
|
||||
summary_path: existing_path(&path, "epiphany-dogfood-summary.json")
|
||||
.or_else(|| existing_path(&path, "gui-action-summary.json"))
|
||||
.or_else(|| existing_path(&path, "unity-bridge-summary.json"))
|
||||
.or_else(|| existing_path(&path, "rider-bridge-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"),
|
||||
implementation_audit: read_implementation_audit(&path),
|
||||
runtime_audit: read_runtime_audit(&path),
|
||||
rider_audit: read_rider_audit(&path),
|
||||
files,
|
||||
modified_millis,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_rider_audit(root: &Path) -> Option<RiderAudit> {
|
||||
let result_path = root.join("rider-bridge-summary.json");
|
||||
let text = fs::read_to_string(&result_path).ok()?;
|
||||
let value: Value = serde_json::from_str(&text).ok()?;
|
||||
let status = value.get("status").and_then(Value::as_str)?.to_string();
|
||||
Some(RiderAudit {
|
||||
result_path: result_path.display().to_string(),
|
||||
status,
|
||||
workspace: json_optional_string(&value, "workspace"),
|
||||
solution_path: json_optional_string(&value, "solutionPath"),
|
||||
solution_status: json_optional_string(&value, "solutionStatus"),
|
||||
rider_path: json_optional_string(&value, "riderPath"),
|
||||
installation_count: value.get("installationCount").and_then(Value::as_u64),
|
||||
note: json_optional_string(&value, "note"),
|
||||
vcs: read_rider_vcs(&value),
|
||||
installations: read_rider_installations(&value),
|
||||
search_roots: read_string_array(&value, "searchRoots"),
|
||||
})
|
||||
}
|
||||
|
||||
fn read_rider_vcs(value: &Value) -> Option<RiderVcsAudit> {
|
||||
let vcs = value.get("vcs")?;
|
||||
Some(RiderVcsAudit {
|
||||
status: json_optional_string(vcs, "status"),
|
||||
branch: json_optional_string(vcs, "branch"),
|
||||
dirty: vcs.get("dirty").and_then(Value::as_bool),
|
||||
changed_files: read_string_array(vcs, "changedFiles"),
|
||||
staged_files: read_string_array(vcs, "stagedFiles"),
|
||||
changed_ranges_known: vcs.get("changedRangesKnown").and_then(Value::as_bool),
|
||||
})
|
||||
}
|
||||
|
||||
fn read_rider_installations(value: &Value) -> Vec<RiderInstallation> {
|
||||
value
|
||||
.get("installations")
|
||||
.and_then(Value::as_array)
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.map(|item| RiderInstallation {
|
||||
path: json_optional_string(item, "path"),
|
||||
version_hint: json_optional_string(item, "versionHint"),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn read_runtime_audit(root: &Path) -> Option<RuntimeAudit> {
|
||||
let result_path = root.join("unity-bridge-summary.json");
|
||||
let text = fs::read_to_string(&result_path).ok()?;
|
||||
let value: Value = serde_json::from_str(&text).ok()?;
|
||||
let status = value.get("status").and_then(Value::as_str)?.to_string();
|
||||
Some(RuntimeAudit {
|
||||
result_path: result_path.display().to_string(),
|
||||
status,
|
||||
project_path: json_optional_string(&value, "projectPath"),
|
||||
project_version: json_optional_string(&value, "projectVersion"),
|
||||
editor_path: json_optional_string(&value, "editorPath"),
|
||||
note: json_optional_string(&value, "note"),
|
||||
editor_bridge: read_editor_bridge(&value),
|
||||
installed_editors: read_installed_editors(&value),
|
||||
candidate_paths: read_string_array(&value, "candidatePaths"),
|
||||
search_roots: read_string_array(&value, "searchRoots"),
|
||||
})
|
||||
}
|
||||
|
||||
fn json_optional_string(value: &Value, key: &str) -> Option<String> {
|
||||
value
|
||||
.get(key)
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string)
|
||||
}
|
||||
|
||||
fn read_string_array(value: &Value, key: &str) -> Vec<String> {
|
||||
value
|
||||
.get(key)
|
||||
.and_then(Value::as_array)
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.map(ToString::to_string)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn read_editor_bridge(value: &Value) -> Option<EditorBridgeAudit> {
|
||||
let bridge = value.get("editorBridge")?;
|
||||
Some(EditorBridgeAudit {
|
||||
exists: bridge
|
||||
.get("exists")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false),
|
||||
path: json_optional_string(bridge, "path"),
|
||||
relative_path: json_optional_string(bridge, "relativePath"),
|
||||
execute_method: json_optional_string(bridge, "executeMethod"),
|
||||
})
|
||||
}
|
||||
|
||||
fn read_installed_editors(value: &Value) -> Vec<InstalledUnityEditor> {
|
||||
value
|
||||
.get("installedEditors")
|
||||
.and_then(Value::as_array)
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.map(|item| InstalledUnityEditor {
|
||||
version: json_optional_string(item, "version"),
|
||||
editor_path: json_optional_string(item, "editorPath"),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn existing_path(root: &Path, name: &str) -> Option<String> {
|
||||
let path = root.join(name);
|
||||
path.exists().then(|| path.display().to_string())
|
||||
}
|
||||
|
||||
fn read_implementation_audit(root: &Path) -> Option<ImplementationAudit> {
|
||||
let result_path = root.join("implementation-result.json");
|
||||
let text = fs::read_to_string(&result_path).ok()?;
|
||||
let value: Value = serde_json::from_str(&text).ok()?;
|
||||
Some(ImplementationAudit {
|
||||
result_path: result_path.display().to_string(),
|
||||
workspace_changed: value
|
||||
.get("workspaceChanged")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false),
|
||||
tracked_diff_present: value
|
||||
.get("trackedDiffPresent")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false),
|
||||
changed_files: value
|
||||
.get("changedFiles")
|
||||
.and_then(Value::as_array)
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.map(ToString::to_string)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn repo_root() -> Result<PathBuf, String> {
|
||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
manifest_dir
|
||||
.parent()
|
||||
.and_then(Path::parent)
|
||||
.and_then(Path::parent)
|
||||
.map(Path::to_path_buf)
|
||||
.ok_or_else(|| "failed to derive repository root from CARGO_MANIFEST_DIR".to_string())
|
||||
}
|
||||
|
||||
fn find_python() -> Result<PathBuf, String> {
|
||||
if let Ok(value) = std::env::var("EPIPHANY_PYTHON") {
|
||||
let path = PathBuf::from(value);
|
||||
if path.exists() {
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
let bundled = PathBuf::from(
|
||||
r"C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe",
|
||||
);
|
||||
if bundled.exists() {
|
||||
return Ok(bundled);
|
||||
}
|
||||
Ok(PathBuf::from("python"))
|
||||
}
|
||||
|
||||
fn unix_millis() -> u128 {
|
||||
system_time_millis(SystemTime::now()).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn system_time_millis(value: SystemTime) -> Option<u128> {
|
||||
value
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|duration| duration.as_millis())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
load_operator_snapshot,
|
||||
run_operator_action
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running Epiphany operator");
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fn main() {
|
||||
epiphany_gui_lib::run();
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Epiphany",
|
||||
"version": "0.1.0",
|
||||
"identifier": "dev.epiphany.operator",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"devUrl": "http://127.0.0.1:1420",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Epiphany Operator",
|
||||
"width": 1280,
|
||||
"height": 860,
|
||||
"minWidth": 960,
|
||||
"minHeight": 640
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": false,
|
||||
"targets": "all"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,10 +0,0 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { App } from "./App";
|
||||
import "./styles.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
|
@ -1,319 +0,0 @@
|
|||
import { parseMidi } from "midi-file";
|
||||
|
||||
export type MidiCorpusFile = {
|
||||
name: string;
|
||||
path: string;
|
||||
relativePath: string;
|
||||
size: number;
|
||||
};
|
||||
|
||||
export type AgentHarmonyVoice = {
|
||||
chordDegree: number;
|
||||
frequency: number;
|
||||
instrument: string;
|
||||
midi: number;
|
||||
program: number;
|
||||
};
|
||||
|
||||
export type AquariumHarmonyFrame = {
|
||||
agentVoices: Record<string, AgentHarmonyVoice>;
|
||||
chordLabel: string;
|
||||
corpusLabel: string;
|
||||
sourceName: string;
|
||||
sourcePath: string;
|
||||
};
|
||||
|
||||
type ChordState = {
|
||||
key: string;
|
||||
label: string;
|
||||
pitchClasses: number[];
|
||||
salience: number;
|
||||
};
|
||||
|
||||
export type HarmonySource = {
|
||||
chords: ChordState[];
|
||||
filesSeen: number;
|
||||
instruments: Array<{ count: number; name: string; program: number }>;
|
||||
markov: Map<string, string[]>;
|
||||
sourceName: string;
|
||||
sourcePath: string;
|
||||
};
|
||||
|
||||
export type HarmonyRuntime = {
|
||||
frame: AquariumHarmonyFrame;
|
||||
next: () => AquariumHarmonyFrame;
|
||||
};
|
||||
|
||||
const defaultCorpusLabel = "Classical MIDIRip";
|
||||
const noteNames = ["C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"];
|
||||
const gmProgramNames = [
|
||||
"Acoustic Grand Piano", "Bright Piano", "Electric Grand Piano", "Honky-tonk Piano",
|
||||
"Electric Piano 1", "Electric Piano 2", "Harpsichord", "Clavinet",
|
||||
"Celesta", "Glockenspiel", "Music Box", "Vibraphone", "Marimba", "Xylophone", "Tubular Bells", "Dulcimer",
|
||||
"Drawbar Organ", "Percussive Organ", "Rock Organ", "Church Organ", "Reed Organ", "Accordion", "Harmonica", "Tango Accordion",
|
||||
"Nylon Guitar", "Steel Guitar", "Jazz Guitar", "Clean Guitar", "Muted Guitar", "Overdrive Guitar", "Distortion Guitar", "Guitar Harmonics",
|
||||
"Acoustic Bass", "Finger Bass", "Pick Bass", "Fretless Bass", "Slap Bass 1", "Slap Bass 2", "Synth Bass 1", "Synth Bass 2",
|
||||
"Violin", "Viola", "Cello", "Contrabass", "Tremolo Strings", "Pizzicato Strings", "Orchestral Harp", "Timpani",
|
||||
"String Ensemble 1", "String Ensemble 2", "Synth Strings 1", "Synth Strings 2", "Choir Aahs", "Voice Oohs", "Synth Voice", "Orchestra Hit",
|
||||
"Trumpet", "Trombone", "Tuba", "Muted Trumpet", "French Horn", "Brass Section", "Synth Brass 1", "Synth Brass 2",
|
||||
"Soprano Sax", "Alto Sax", "Tenor Sax", "Baritone Sax", "Oboe", "English Horn", "Bassoon", "Clarinet",
|
||||
"Piccolo", "Flute", "Recorder", "Pan Flute", "Blown Bottle", "Shakuhachi", "Whistle", "Ocarina",
|
||||
"Lead 1 Square", "Lead 2 Saw", "Lead 3 Calliope", "Lead 4 Chiff", "Lead 5 Charang", "Lead 6 Voice", "Lead 7 Fifths", "Lead 8 Bass+Lead",
|
||||
"Pad 1 New Age", "Pad 2 Warm", "Pad 3 Polysynth", "Pad 4 Choir", "Pad 5 Bowed", "Pad 6 Metallic", "Pad 7 Halo", "Pad 8 Sweep",
|
||||
"FX 1 Rain", "FX 2 Soundtrack", "FX 3 Crystal", "FX 4 Atmosphere", "FX 5 Brightness", "FX 6 Goblins", "FX 7 Echoes", "FX 8 Sci-fi",
|
||||
"Sitar", "Banjo", "Shamisen", "Koto", "Kalimba", "Bagpipe", "Fiddle", "Shanai",
|
||||
"Tinkle Bell", "Agogo", "Steel Drums", "Woodblock", "Taiko Drum", "Melodic Tom", "Synth Drum", "Reverse Cymbal",
|
||||
"Guitar Fret Noise", "Breath Noise", "Seashore", "Bird Tweet", "Telephone Ring", "Helicopter", "Applause", "Gunshot",
|
||||
];
|
||||
|
||||
export async function loadShuffledDefaultHarmony(agentIds: string[]) {
|
||||
const listing = await fetchJson<{ files: MidiCorpusFile[]; root: string }>("/midi-corpus/list");
|
||||
const files = shuffle(listing.files, Date.now()).filter((file) => file.size > 0);
|
||||
for (const file of files.slice(0, 40)) {
|
||||
try {
|
||||
const source = await loadHarmonySource(file);
|
||||
return { files, frame: createHarmonyRuntime(source, agentIds).frame, source };
|
||||
} catch {
|
||||
// MIDI archives contain plenty of weird little fossils. Keep digging.
|
||||
}
|
||||
}
|
||||
throw new Error("no parseable MIDI harmony source found in the default corpus");
|
||||
}
|
||||
|
||||
export async function loadNextHarmony(files: MidiCorpusFile[], agentIds: string[], currentPath?: string) {
|
||||
const candidates = shuffle(files.filter((file) => file.path !== currentPath), Date.now());
|
||||
for (const file of candidates.slice(0, 50)) {
|
||||
try {
|
||||
const source = await loadHarmonySource(file);
|
||||
return { frame: createHarmonyRuntime(source, agentIds).frame, source };
|
||||
} catch {
|
||||
// Keep shuffling; bad files do not get to be the main character.
|
||||
}
|
||||
}
|
||||
throw new Error("no alternate parseable MIDI file found");
|
||||
}
|
||||
|
||||
export async function pickHarmonyFolder(agentIds: string[]) {
|
||||
const picker = (window as any).showDirectoryPicker as undefined | (() => Promise<FileSystemDirectoryHandle>);
|
||||
if (!picker) {
|
||||
throw new Error("this browser does not expose a folder picker; use the Vite default corpus endpoint");
|
||||
}
|
||||
const root = await picker();
|
||||
const files: MidiCorpusFile[] = [];
|
||||
await collectHandleMidiFiles(root, files);
|
||||
const loaded = await loadFromFileHandles(files, agentIds);
|
||||
return { files, ...loaded };
|
||||
}
|
||||
|
||||
export function createHarmonyRuntime(source: HarmonySource, agentIds: string[]): HarmonyRuntime {
|
||||
const salient = [...source.chords].sort((a, b) => b.salience - a.salience);
|
||||
let state = salient[0] ?? source.chords[0];
|
||||
let step = 0;
|
||||
const next = () => {
|
||||
const options = source.markov.get(state.key) ?? salient.slice(0, 5).map((chord) => chord.key);
|
||||
const nextKey = options[(step * 7 + state.key.length) % Math.max(options.length, 1)];
|
||||
state = source.chords.find((chord) => chord.key === nextKey) ?? salient[(step + 1) % Math.max(salient.length, 1)] ?? state;
|
||||
step += 1;
|
||||
return frameFromState(source, state, agentIds, step);
|
||||
};
|
||||
return { frame: frameFromState(source, state, agentIds, step), next };
|
||||
}
|
||||
|
||||
async function loadFromFileHandles(files: MidiCorpusFile[], agentIds: string[]) {
|
||||
for (const file of shuffle(files, Date.now()).slice(0, 40)) {
|
||||
const handle = (file as any).handle as FileSystemFileHandle | undefined;
|
||||
if (!handle) continue;
|
||||
try {
|
||||
const source = buildHarmonySource(file, await (await handle.getFile()).arrayBuffer());
|
||||
return { frame: createHarmonyRuntime(source, agentIds).frame, source };
|
||||
} catch {
|
||||
// Same fossil policy, now with permissions.
|
||||
}
|
||||
}
|
||||
throw new Error("selected folder did not contain a parseable MIDI harmony source");
|
||||
}
|
||||
|
||||
async function loadHarmonySource(file: MidiCorpusFile) {
|
||||
const response = await fetch(`/midi-corpus/file?path=${encodeURIComponent(file.path)}`, { cache: "no-store" });
|
||||
if (!response.ok) throw new Error(`MIDI fetch failed: ${response.status}`);
|
||||
return buildHarmonySource(file, await response.arrayBuffer());
|
||||
}
|
||||
|
||||
function buildHarmonySource(file: MidiCorpusFile, buffer: ArrayBuffer): HarmonySource {
|
||||
const midi = parseMidi(new Uint8Array(buffer));
|
||||
const ticksPerBeat = midi.header.ticksPerBeat ?? midi.header.timeDivision ?? 480;
|
||||
const bucketSize = Math.max(1, ticksPerBeat * 2);
|
||||
const buckets = new Map<number, { low: number; notes: Map<number, number>; weight: number }>();
|
||||
const programs = new Map<number, number>();
|
||||
const transitions: string[] = [];
|
||||
|
||||
for (const track of midi.tracks) {
|
||||
let tick = 0;
|
||||
for (const event of track as any[]) {
|
||||
tick += event.deltaTime ?? 0;
|
||||
if (event.type === "programChange" && typeof event.programNumber === "number") {
|
||||
programs.set(event.programNumber, (programs.get(event.programNumber) ?? 0) + 1);
|
||||
}
|
||||
if (event.type !== "noteOn" || !event.velocity) continue;
|
||||
const bucketIndex = Math.round(tick / bucketSize);
|
||||
const bucket = buckets.get(bucketIndex) ?? { low: event.noteNumber, notes: new Map<number, number>(), weight: 0 };
|
||||
const pitchClass = event.noteNumber % 12;
|
||||
bucket.low = Math.min(bucket.low, event.noteNumber);
|
||||
bucket.notes.set(pitchClass, (bucket.notes.get(pitchClass) ?? 0) + event.velocity);
|
||||
bucket.weight += event.velocity;
|
||||
buckets.set(bucketIndex, bucket);
|
||||
}
|
||||
}
|
||||
|
||||
const statesByKey = new Map<string, ChordState>();
|
||||
const sortedBuckets = [...buckets.entries()].sort((a, b) => a[0] - b[0]);
|
||||
for (const [, bucket] of sortedBuckets) {
|
||||
const pcs = [...bucket.notes.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 5)
|
||||
.map(([pitchClass]) => pitchClass)
|
||||
.sort((a, b) => a - b);
|
||||
if (pcs.length < 2) continue;
|
||||
const root = bucket.low % 12;
|
||||
const key = `${root}:${pcs.join(".")}`;
|
||||
const existing = statesByKey.get(key);
|
||||
if (existing) {
|
||||
existing.salience += bucket.weight;
|
||||
} else {
|
||||
statesByKey.set(key, { key, label: chordLabel(root, pcs), pitchClasses: pcs, salience: bucket.weight });
|
||||
}
|
||||
transitions.push(key);
|
||||
}
|
||||
|
||||
const markov = new Map<string, string[]>();
|
||||
for (let index = 0; index < transitions.length - 1; index += 1) {
|
||||
const current = transitions[index];
|
||||
const next = transitions[index + 1];
|
||||
const options = markov.get(current) ?? [];
|
||||
options.push(next);
|
||||
markov.set(current, options);
|
||||
}
|
||||
|
||||
const chords = [...statesByKey.values()]
|
||||
.filter((chord) => chord.salience > 0)
|
||||
.sort((a, b) => b.salience - a.salience)
|
||||
.slice(0, 96);
|
||||
if (chords.length < 3) throw new Error("not enough chord material");
|
||||
|
||||
const instruments = [...programs.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([program, count]) => ({ count, name: gmProgramNames[program] ?? `Program ${program + 1}`, program }))
|
||||
.slice(0, 16);
|
||||
|
||||
return {
|
||||
chords,
|
||||
filesSeen: 1,
|
||||
instruments: instruments.length ? instruments : [{ count: 1, name: "Acoustic Grand Piano", program: 0 }],
|
||||
markov,
|
||||
sourceName: file.relativePath || file.name,
|
||||
sourcePath: file.path,
|
||||
};
|
||||
}
|
||||
|
||||
function frameFromState(source: HarmonySource, chord: ChordState, agentIds: string[], step: number): AquariumHarmonyFrame {
|
||||
const agentVoices: Record<string, AgentHarmonyVoice> = {};
|
||||
agentIds.forEach((id, index) => {
|
||||
const pitchClass = chord.pitchClasses[index % chord.pitchClasses.length];
|
||||
const octave = 3 + ((index + Math.floor(step / 2)) % 3);
|
||||
const midi = nearestMidiForPitchClass(12 * octave + 12, pitchClass, index);
|
||||
const instrument = source.instruments[index % source.instruments.length];
|
||||
agentVoices[id] = {
|
||||
chordDegree: index % chord.pitchClasses.length,
|
||||
frequency: midiToFrequency(midi),
|
||||
instrument: instrument.name,
|
||||
midi,
|
||||
program: instrument.program,
|
||||
};
|
||||
});
|
||||
return {
|
||||
agentVoices,
|
||||
chordLabel: chord.label,
|
||||
corpusLabel: defaultCorpusLabel,
|
||||
sourceName: source.sourceName,
|
||||
sourcePath: source.sourcePath,
|
||||
};
|
||||
}
|
||||
|
||||
function nearestMidiForPitchClass(anchor: number, pitchClass: number, spread: number) {
|
||||
let midi = anchor + ((pitchClass - anchor) % 12 + 12) % 12;
|
||||
if (midi - anchor > 6) midi -= 12;
|
||||
return clampNumber(midi + Math.floor(spread / 3) * 12, 36, 84);
|
||||
}
|
||||
|
||||
function chordLabel(root: number, pitchClasses: number[]) {
|
||||
const intervals = pitchClasses.map((pc) => (pc - root + 12) % 12);
|
||||
const quality =
|
||||
intervals.includes(4) && intervals.includes(7) ? "" :
|
||||
intervals.includes(3) && intervals.includes(7) ? "m" :
|
||||
intervals.includes(3) && intervals.includes(6) ? "dim" :
|
||||
intervals.includes(4) && intervals.includes(8) ? "aug" :
|
||||
intervals.includes(5) ? "sus4" :
|
||||
intervals.includes(2) ? "sus2" :
|
||||
"";
|
||||
const extensions = [
|
||||
intervals.includes(10) ? "7" : "",
|
||||
intervals.includes(11) ? "maj7" : "",
|
||||
intervals.includes(2) && !quality.includes("sus2") ? "9" : "",
|
||||
].filter(Boolean).join(" ");
|
||||
return `${noteNames[root]}${quality}${extensions ? ` ${extensions}` : ""}`;
|
||||
}
|
||||
|
||||
function midiToFrequency(midi: number) {
|
||||
return 440 * 2 ** ((midi - 69) / 12);
|
||||
}
|
||||
|
||||
function shuffle<T>(items: T[], seed: number) {
|
||||
const next = mulberry32(seed >>> 0);
|
||||
const copy = [...items];
|
||||
for (let index = copy.length - 1; index > 0; index -= 1) {
|
||||
const swap = Math.floor(next() * (index + 1));
|
||||
[copy[index], copy[swap]] = [copy[swap], copy[index]];
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
function mulberry32(seed: number) {
|
||||
let state = seed || 0x9e3779b9;
|
||||
return () => {
|
||||
state += 0x6d2b79f5;
|
||||
let value = state;
|
||||
value = Math.imul(value ^ (value >>> 15), value | 1);
|
||||
value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
|
||||
return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
function clampNumber(value: number, min: number, max: number) {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
async function fetchJson<T>(url: string): Promise<T> {
|
||||
const response = await fetch(url, { cache: "no-store" });
|
||||
if (!response.ok) throw new Error(`request failed ${response.status}: ${url}`);
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
async function collectHandleMidiFiles(directory: FileSystemDirectoryHandle, files: MidiCorpusFile[], prefix = "") {
|
||||
for await (const [name, handle] of (directory as any).entries() as AsyncIterable<[string, FileSystemHandle]>) {
|
||||
if (files.length >= 5000) return;
|
||||
const relativePath = prefix ? `${prefix}/${name}` : name;
|
||||
if (handle.kind === "directory") {
|
||||
await collectHandleMidiFiles(handle as FileSystemDirectoryHandle, files, relativePath);
|
||||
continue;
|
||||
}
|
||||
if (!/\.(mid|midi)$/i.test(name)) continue;
|
||||
const file = await (handle as FileSystemFileHandle).getFile();
|
||||
files.push({
|
||||
name,
|
||||
path: relativePath,
|
||||
relativePath,
|
||||
size: file.size,
|
||||
...( { handle } as any ),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { OperatorAction, OperatorActionResult, OperatorSnapshot, StatusRequest } from "./types";
|
||||
|
||||
export async function loadOperatorSnapshot(request: StatusRequest): Promise<OperatorSnapshot> {
|
||||
if (hasTauriRuntime()) {
|
||||
return invoke<OperatorSnapshot>("load_operator_snapshot", { request });
|
||||
}
|
||||
|
||||
const response = await fetch("/operator-snapshot.sample.json", { cache: "no-store" });
|
||||
if (!response.ok) {
|
||||
throw new Error(`sample operator snapshot failed: ${response.status}`);
|
||||
}
|
||||
return (await response.json()) as OperatorSnapshot;
|
||||
}
|
||||
|
||||
export async function runOperatorAction(action: OperatorAction, request: StatusRequest): Promise<OperatorActionResult> {
|
||||
if (hasTauriRuntime()) {
|
||||
return invoke<OperatorActionResult>("run_operator_action", { action, request });
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
return {
|
||||
action,
|
||||
artifactPath: "E:\\Projects\\EpiphanyAgent\\.epiphany-gui\\sample-action",
|
||||
summary: `${action} sample completed.`,
|
||||
threadId: action === "prepareCheckpoint" ? "019dd9d1-045b-7f13-b0e1-38ed89b31495" : request.threadId,
|
||||
};
|
||||
}
|
||||
|
||||
function hasTauriRuntime(): boolean {
|
||||
return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,101 +0,0 @@
|
|||
export interface StatusRequest {
|
||||
threadId?: string;
|
||||
cwd?: string;
|
||||
codexHome?: string;
|
||||
appServer?: string;
|
||||
planningDraftId?: string;
|
||||
}
|
||||
|
||||
export type OperatorAction =
|
||||
| "statusSnapshot"
|
||||
| "coordinatorPlan"
|
||||
| "inspectUnity"
|
||||
| "inspectRider"
|
||||
| "prepareCheckpoint"
|
||||
| "continueImplementation"
|
||||
| "launchImagination"
|
||||
| "readImaginationResult"
|
||||
| "acceptImagination"
|
||||
| "launchModeling"
|
||||
| "readModelingResult"
|
||||
| "acceptModeling"
|
||||
| "launchVerification"
|
||||
| "readVerificationResult"
|
||||
| "acceptVerification"
|
||||
| "adoptObjectiveDraft"
|
||||
| "launchReorient"
|
||||
| "readReorientResult"
|
||||
| "acceptReorient";
|
||||
|
||||
export interface OperatorActionResult {
|
||||
action: OperatorAction;
|
||||
artifactPath: string;
|
||||
summary: string;
|
||||
threadId?: string;
|
||||
}
|
||||
|
||||
export interface ArtifactBundle {
|
||||
name: string;
|
||||
path: string;
|
||||
files: string[];
|
||||
summaryPath?: string;
|
||||
finalStatusPath?: string;
|
||||
comparisonPath?: string;
|
||||
implementationAudit?: {
|
||||
resultPath: string;
|
||||
workspaceChanged: boolean;
|
||||
trackedDiffPresent: boolean;
|
||||
changedFiles: string[];
|
||||
};
|
||||
runtimeAudit?: {
|
||||
resultPath: string;
|
||||
status: string;
|
||||
projectPath?: string;
|
||||
projectVersion?: string;
|
||||
editorPath?: string;
|
||||
note?: string;
|
||||
editorBridge?: {
|
||||
exists: boolean;
|
||||
path?: string;
|
||||
relativePath?: string;
|
||||
executeMethod?: string;
|
||||
};
|
||||
installedEditors?: Array<{
|
||||
version?: string;
|
||||
editorPath?: string;
|
||||
}>;
|
||||
candidatePaths?: string[];
|
||||
searchRoots?: string[];
|
||||
};
|
||||
riderAudit?: {
|
||||
resultPath: string;
|
||||
status: string;
|
||||
workspace?: string;
|
||||
solutionPath?: string;
|
||||
solutionStatus?: string;
|
||||
riderPath?: string;
|
||||
installationCount?: number;
|
||||
note?: string;
|
||||
vcs?: {
|
||||
status?: string;
|
||||
branch?: string;
|
||||
dirty?: boolean;
|
||||
changedFiles?: string[];
|
||||
stagedFiles?: string[];
|
||||
changedRangesKnown?: boolean;
|
||||
};
|
||||
installations?: Array<{
|
||||
path?: string;
|
||||
versionHint?: string;
|
||||
}>;
|
||||
searchRoots?: string[];
|
||||
};
|
||||
modifiedMillis?: number;
|
||||
}
|
||||
|
||||
export interface OperatorSnapshot {
|
||||
generatedAt: string;
|
||||
repoRoot: string;
|
||||
status: any;
|
||||
artifacts: ArtifactBundle[];
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2020"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": []
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { readdir, readFile, stat } from "node:fs/promises";
|
||||
import { extname, join, relative } from "node:path";
|
||||
|
||||
const defaultMidiCorpus =
|
||||
"D:\\Documents\\130000_Pop_Rock_Classical_Videogame_EDM_MIDI_Archive[6_19_15]\\Classical_www.midiworld.com_MIDIRip";
|
||||
const midiFileLimit = 1000;
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), midiCorpusDevPlugin()],
|
||||
clearScreen: false,
|
||||
server: {
|
||||
host: "127.0.0.1",
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
},
|
||||
envPrefix: ["VITE_", "TAURI_"],
|
||||
});
|
||||
|
||||
function midiCorpusDevPlugin() {
|
||||
return {
|
||||
name: "epiphany-midi-corpus-dev",
|
||||
configureServer(server: any) {
|
||||
server.middlewares.use("/midi-corpus/list", async (request: any, response: any) => {
|
||||
try {
|
||||
const url = new URL(request.url ?? "", "http://127.0.0.1");
|
||||
const root = url.searchParams.get("root") || defaultMidiCorpus;
|
||||
const files = await listMidiFiles(root);
|
||||
writeJson(response, { root, files });
|
||||
} catch (error) {
|
||||
writeJson(response, { error: error instanceof Error ? error.message : String(error) }, 500);
|
||||
}
|
||||
});
|
||||
server.middlewares.use("/midi-corpus/file", async (request: any, response: any) => {
|
||||
try {
|
||||
const url = new URL(request.url ?? "", "http://127.0.0.1");
|
||||
const path = url.searchParams.get("path");
|
||||
if (!path) {
|
||||
writeJson(response, { error: "missing MIDI path" }, 400);
|
||||
return;
|
||||
}
|
||||
const data = await readFile(path);
|
||||
response.statusCode = 200;
|
||||
response.setHeader("content-type", "audio/midi");
|
||||
response.end(data);
|
||||
} catch (error) {
|
||||
writeJson(response, { error: error instanceof Error ? error.message : String(error) }, 500);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function listMidiFiles(root: string) {
|
||||
const files: Array<{ name: string; path: string; relativePath: string; size: number }> = [];
|
||||
async function visit(directory: string) {
|
||||
if (files.length >= midiFileLimit) return;
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (files.length >= midiFileLimit) break;
|
||||
const path = join(directory, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
await visit(path);
|
||||
continue;
|
||||
}
|
||||
const extension = extname(entry.name).toLowerCase();
|
||||
if (extension !== ".mid" && extension !== ".midi") continue;
|
||||
const metadata = await stat(path);
|
||||
files.push({
|
||||
name: entry.name,
|
||||
path,
|
||||
relativePath: relative(root, path),
|
||||
size: metadata.size,
|
||||
});
|
||||
}
|
||||
}
|
||||
await visit(root);
|
||||
return files;
|
||||
}
|
||||
|
||||
function writeJson(response: any, payload: unknown, status = 200) {
|
||||
response.statusCode = status;
|
||||
response.setHeader("content-type", "application/json");
|
||||
response.end(JSON.stringify(payload));
|
||||
}
|
||||
|
|
@ -86,7 +86,7 @@ The landed machine now has:
|
|||
- first auditable Phase 6 dogfood runner through `tools/epiphany_mvp_dogfood.py`, producing local status snapshots, sealed app-server transcript diagnostics, operator-safe final status artifacts, truthful vanilla-reference output, and comparison notes
|
||||
- first auditable Phase 6 fixed-lane coordinator runner through `tools/epiphany_mvp_coordinator.py`, producing coordinator summary, JSONL steps, rendered snapshots, transcript, stderr, and final next-action artifacts while keeping semantic findings review-gated by default
|
||||
- first auditable Phase 6 live-specialist runner through `tools/epiphany_mvp_live_specialist.py`, proving `roleLaunch -> agent_jobs worker -> report_agent_job_result -> roleResult` without manual backend completion
|
||||
- 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
|
||||
- first Phase 6 Aquarium operator shell extracted to sibling repo `E:\Projects\EpiphanyAquarium`, a Tauri v2 + React/WebGL client over the existing status bridge, dogfood artifacts, and GUI action artifacts, with its own distilled interface state/memory/doctrine plus durable checkpoint preparation, bounded status/coordinator artifact buttons, fixed modeling/verification/reorient launch and read-back buttons, and explicit review-gated reorient acceptance
|
||||
- first Unity editor/runtime bridge through `tools/epiphany_unity_bridge.py`, `tools/epiphany_unity_bridge_smoke.py`, and the GUI Inspect Unity action, resolving exact project-pinned editors and writing runtime artifacts while refusing wrong or missing versions
|
||||
- prompt-file ownership for major Epiphany prompt surfaces: rendered state intro/doctrine lives under `epiphany-core/src/prompts/`, while modeling/Body, research/Eyes, implementation/Hands, verification/Soul, reorientation/Life, coordinator/Self, and CRRC templates live in `vendor/codex/codex-rs/app-server/src/prompts/epiphany_specialists.toml`
|
||||
- prompt-level anti-Greenspun guardrails: Epiphany keeps Codex's useful operator discipline and requires a bounded research/scout check before agents invent bespoke versions of known algorithms, parsers, schedulers, renderers, protocols, storage layers, security mechanisms, or workflow engines
|
||||
|
|
@ -314,20 +314,27 @@ Do not spend Phase 6 polishing Phase 5 out of anxiety. The Phase 5 smoke harness
|
|||
is a regression guardrail, not a ritual drum circle for summoning more tiny
|
||||
hardening slices.
|
||||
|
||||
## GUI Operator Plan
|
||||
## Aquarium Operator Plan
|
||||
|
||||
Build the MVP GUI as a local Tauri v2 + React operator console.
|
||||
Build the MVP operator UI as a local Tauri v2 + React/WebGL Aquarium in
|
||||
`E:\Projects\EpiphanyAquarium`.
|
||||
|
||||
The GUI is not the source of truth. It is a desktop window over the same
|
||||
Aquarium is not the source of truth. It is a desktop window over the same
|
||||
app-server surfaces already proved by the CLI status, coordinator, dogfood, and
|
||||
live-specialist runners. Its job is to make the single-user loop usable without
|
||||
terminal handling, not to invent a parallel state model.
|
||||
terminal handling, not to invent a parallel state model. EpiphanyAgent owns the
|
||||
harness, typed state, coordinator policy, and bridge tools; Aquarium owns the
|
||||
interface organism, visual/audio interaction grammar, and its own local
|
||||
pseudo-Epiphany state.
|
||||
|
||||
Initial shape:
|
||||
Current shape:
|
||||
|
||||
1. Scaffold `apps/epiphany-gui` as a Tauri v2 app with a React frontend.
|
||||
1. Maintain `E:\Projects\EpiphanyAquarium` as the UI repo with `AGENTS.md`,
|
||||
`state/map.yaml`, `state/memory.json`, scratch/evidence, and interface
|
||||
doctrine.
|
||||
2. Start by consuming existing app-server JSON-RPC surfaces rather than adding new protocol unless the UI exposes a real missing primitive.
|
||||
3. Make the first screen an operator console, not a landing page:
|
||||
3. Make the default screen an aquarium of interactive objects, not a static
|
||||
admin panel:
|
||||
- active thread and workspace
|
||||
- coordinator recommendation and reason
|
||||
- pressure, CRRC, and reorientation status
|
||||
|
|
@ -347,7 +354,7 @@ Initial shape:
|
|||
- launch fixed reorient-worker when recommended
|
||||
- accept a completed reorientation finding after review
|
||||
- open artifact folder/file
|
||||
5. Keep semantic acceptance gated. The GUI may launch bounded work and display
|
||||
5. Keep semantic acceptance gated. Aquarium may launch bounded work and display
|
||||
findings, but it must not auto-promote evidence, auto-accept worker output,
|
||||
invent arbitrary specialists, or continue implementation after unresolved
|
||||
drift.
|
||||
|
|
@ -357,7 +364,8 @@ Implementation slices:
|
|||
1. **Read-only shell**: scaffold Tauri/React, connect to app-server through the
|
||||
existing MVP status bridge, render the same data as
|
||||
`tools/epiphany_mvp_status.py`, and provide artifact bundle links. This
|
||||
slice is landed under `apps/epiphany-gui`.
|
||||
slice landed first under `apps/epiphany-gui` and is now extracted to
|
||||
`E:\Projects\EpiphanyAquarium`.
|
||||
2. **Bounded operator actions**: status snapshot, coordinator-plan, Unity
|
||||
runtime inspection, durable checkpoint preparation, roleLaunch, roleResult,
|
||||
reorientLaunch, reorientResult, and explicit reorientAccept flows are
|
||||
|
|
@ -365,7 +373,7 @@ Implementation slices:
|
|||
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.
|
||||
artifact bundles and stream progress/status into Aquarium.
|
||||
4. **Usability pass**: make the current recommendation, blocked lane, pending
|
||||
review, and next safe action visually obvious enough that the user can test
|
||||
the product without reading sealed transcripts unless the user explicitly asks for forensic debugging.
|
||||
|
|
@ -375,9 +383,10 @@ Verification:
|
|||
- Keep the existing CLI smokes as backend guardrails.
|
||||
- Keep `tools/epiphany_unity_bridge_smoke.py` for Unity bridge regressions.
|
||||
- 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`.
|
||||
browser-fallback action clicks in `E:\Projects\EpiphanyAquarium`.
|
||||
- For native Aquarium changes, run `npm run build`, `cargo fmt --check`,
|
||||
`cargo check`, and `npm run tauri -- build --debug --no-bundle` from
|
||||
`E:\Projects\EpiphanyAquarium`.
|
||||
- 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.
|
||||
|
|
|
|||
|
|
@ -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 current context usage reaches 80% of the active auto-compact/context limit, the harness steers the active turn once with a CRRC checkpoint directive so the agent banks working context before compaction/reorientation. Pressure ignores cumulative token spend; cumulative-only telemetry reports unknown instead of yelling. A successful steer now latches a turn-scoped compact handoff that is consumed at clean turn completion, preventing the brake from decaying into another implementation turn. This is still bounded steering plus compaction handoff, not automatic semantic acceptance, a broad scheduler, or implementation continuation.
|
||||
- `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 sealed app-server transcript diagnostics, rendered snapshots, operator-safe 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, 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/accept the fixed Imagination planning lane, launch/read the fixed modeling and verification lanes, launch/read the fixed reorient-worker, and explicitly accept completed reorient findings after review.
|
||||
- The Aquarium operator UI now lives in sibling repo `E:\Projects\EpiphanyAquarium`, not under `apps/epiphany-gui`. It is a Tauri v2 + React/WebGL interface organism over the existing status bridge, dogfood artifacts, and GUI action artifacts, not a new source of truth. It has its own `AGENTS.md`, persistent `state/map.yaml`, `state/memory.json`, scratch/evidence files, and interface doctrine. EpiphanyAgent remains the authoritative harness/backend repo.
|
||||
- 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 prompt doctrine pass is landed. Shared Epiphany prompts now carry distilled memory/evidence discipline. Rendered state intro/doctrine text lives in `epiphany-core/src/prompts/`, and lane/control prompt text lives in `vendor/codex/codex-rs/app-server/src/prompts/epiphany_specialists.toml`: modeling is the Body, implementation is the Hands and GUI-launched main coding lane, verification is the Soul, reorientation is Life, coordinator remains the read-only Self, and CRRC owns the pre-compaction intervention template.
|
||||
|
|
@ -50,12 +50,12 @@ Do not trust this file for the exact live HEAD. Always check git.
|
|||
- The latest dogfood pass fixed concrete harness wounds: stale completed backend jobs now project as terminal, accepted verifier non-pass findings no longer clear implementation, verification coverage accepts modeler source evidence ids as current-model coverage, implementation audits compare pre/post diff hashes instead of treating existing dirt as fresh work, the latest implementation audit status wins, blocked no-diff turns route back through CRRC, stale accepted reorient findings relaunch bounded reorientation when the checkpoint regathers, and accepted reorientation findings now bank resume-ready checkpoints.
|
||||
- The latest audited implementation artifact is `.epiphany-dogfood/aetheria-supervised/gui-actions/continueImplementation-1777548050463576300-28568`: it correctly shows `workspaceChanged: false`, `dirtyWorkspace: true`, `trackedDiffChanged: false`, wrote a no-diff audit, advanced Epiphany state to revision 57, and routed coordinator/CRRC back to `launchReorientWorker` instead of pretending old dirt was progress.
|
||||
- Aetheria dogfood exposed and then landed the first editor/runtime bridge. An implementation worker launched legacy `D:\Unity\Editor\Unity.exe -version` (Unity 5.5.0f3) even though Aetheria pins Unity `6000.1.10f1`; the stray process was killed. `tools/epiphany_unity_bridge.py` now resolves the project-pinned Unity editor, refuses wrong/missing versions, owns `-batchmode`, `-quit`, and `-projectPath`, and writes inspection/command/log artifacts. `tools/epiphany_gui_action.py` points implementation workers at that bridge, and the GUI has an Inspect Unity action.
|
||||
- The current Aetheria runtime truth is blocked but legible: the project pins Unity `6000.1.10f1`, this machine currently has Hub editor `6000.4.2f1`, and the bridge wrote `.epiphany-gui/runtime/unity-inspect-1777549218802064800-23832` proving the exact editor is missing. Treat that artifact as the evidence gap until the pinned editor exists.
|
||||
- The current Aetheria runtime truth is blocked but legible: the project pins Unity `6000.1.10f1`, this machine currently has Hub editor `6000.4.2f1`, and the bridge wrote `.epiphany-gui/runtime/unity-inspect-1777549218802064800-23832` proving the exact editor is missing. Treat that artifact as the evidence gap until the pinned editor exists. The `.epiphany-gui` directory name is still a backend artifact contract, even though the client repo is now Aquarium.
|
||||
- The GUI parses `implementation-result.json` into artifact metadata, surfaces the latest implementation diff/no-diff outcome, and pauses immediate `Continue Implementation` repeats when the newest artifact is a no-diff implementation audit.
|
||||
- The planning loop is now runtime-backed and operator-visible. Typed captures, backlog items, roadmap streams, Objective Drafts, and GitHub issue source refs live in `EpiphanyThreadState`, validate through revision-gated `thread/epiphany/update`, render into prompts when present, project read-only through `thread/epiphany/planning`, render in the GUI Planning panel, can be synthesized by the fixed Imagination/planning lane, and can be explicitly adopted through the artifacted `adoptObjectiveDraft` GUI action. Chat is deliberation, not an objective pipe; ideas and GitHub Issues remain planning state until explicit human adoption.
|
||||
- The bridge/dashboard slice is now landed far enough to test locally. `tools/epiphany_rider_bridge.py` inspects Rider installation, solution, VCS, changed files, and captures file/selection/symbol context into `.epiphany-gui/rider` artifacts; the GUI has Inspect Rider and renders Rider audit artifacts in Environment; the GUI also embeds the adjacent EpiphanyGraph viewer over typed `graphs.architecture`, `graphs.dataflow`, and `graphs.links`.
|
||||
- The bridge/dashboard slice is now landed far enough to test locally. `tools/epiphany_rider_bridge.py` inspects Rider installation, solution, VCS, changed files, and captures file/selection/symbol context into `.epiphany-gui/rider` artifacts; Aquarium has Inspect Rider and renders Rider audit artifacts in Environment; Aquarium also embeds the adjacent EpiphanyGraph viewer over typed `graphs.architecture`, `graphs.dataflow`, and `graphs.links`.
|
||||
- A first Rider plugin scaffold lives under `integrations/rider`: tool window, Refresh status, and Send Context to Epiphany action. It shells through `tools/epiphany_rider_bridge.py` and does not own state. `gradle` is not installed on this machine, so the scaffold is not build-verified yet.
|
||||
- Next real product move: build-verify/package the Rider plugin scaffold when Gradle/wrapper support is available, install the Aetheria-pinned Unity `6000.1.10f1` editor, then run the next Aetheria dogfood pass through GUI/Rider/Unity bridge artifacts. GitHub Issues import is deferred until the backlog source is fresh enough to deserve Imagination's attention.
|
||||
- Next real product move: continue Aquarium UI work in `E:\Projects\EpiphanyAquarium`; keep EpiphanyAgent focused on backend contracts, typed state, coordinator policy, and bridge tools. Then build-verify/package the Rider plugin scaffold when Gradle/wrapper support is available, install the Aetheria-pinned Unity `6000.1.10f1` editor, and run the next Aetheria dogfood pass through Aquarium/Rider/Unity bridge artifacts. GitHub Issues import is deferred until the backlog source is fresh enough to deserve Imagination's attention.
|
||||
- The repo is an Epiphany fork of Codex, not a Codex preset.
|
||||
- `vendor/codex` is tracked directly, not a submodule.
|
||||
- `epiphany-core` owns the heavy Epiphany organs where practical.
|
||||
|
|
@ -148,7 +148,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, durable checkpoint preparation, fixed lane launch/read-back controls, explicit Imagination planning acceptance, explicit review-gated reorient acceptance, and bounded artifact-writing buttons
|
||||
- Tauri v2 + React/WebGL Aquarium operator shell under `E:\Projects\EpiphanyAquarium`, including visual smoke, durable checkpoint preparation, fixed lane launch/read-back controls, explicit Imagination planning acceptance, explicit review-gated reorient acceptance, and bounded artifact-writing buttons
|
||||
|
||||
The exact current control flow is documented in
|
||||
`notes/epiphany-current-algorithmic-map.md`.
|
||||
|
|
|
|||
|
|
@ -72,3 +72,4 @@
|
|||
{"ts": "2026-05-05T09:38:42+00:00", "type": "implementation", "status": "ok", "note": "Perfect Machine GUI audio pass cut the muted legacy WebAudio voice layer from the aquarium dashboard. Agent sound now flows through the buffered spectral vocal path only: projected agents update vocal bins, bounded status/touch events enqueue chirplet-driven vocal bursts, and the visual smoke probe checks vocalAgentCount plus reactiveFlushes instead of fake partial/hum counters."}
|
||||
{"ts": "2026-05-05T09:56:04+00:00", "type": "implementation", "status": "ok", "note": "Aquarium audio transient pass landed: interaction bursts now take priority over idle bins, carry per-sample GPU/CPU envelope shapes, fan out across a staggered 7-agent choir, and reduce idle vocal bins to a quiet residual. Visual smoke now requires lastBurstChoirVoices and transientBins so future changes cannot silently regress back to a constant drone."}
|
||||
{"ts": "2026-05-05T10:17:02+00:00", "type": "implementation", "status": "ok", "note": "Aquarium harmony pass landed: GUI audio now extracts chord/progression states and available General MIDI instruments from shuffled classical MIDI files, assigns chord-tone pitches/instruments to agents, and exposes a small playlist control for current file, shuffle, and folder selection. The Vite dev endpoint serves the default Classical MIDIRip archive; the browser folder picker supports alternate corpora. Build, visual smoke, and a deferred playlist-load probe passed."}
|
||||
{"ts": "2026-05-05T11:41:00+00:00", "type": "implementation", "status": "ok", "note": "Aquarium operator UI split into sibling repo E:\\Projects\\EpiphanyAquarium with its own AGENTS.md, canonical map, Ghostlight-flavored memory, scratch/evidence, and interface doctrine. EpiphanyAgent no longer carries apps/epiphany-gui source; it remains the harness/backend authority while Aquarium owns the Tauri/React/WebGL interface."}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ objective:
|
|||
explicit typed models of the codebase, active goal, evidence, and change
|
||||
machinery they are working on.
|
||||
current: >
|
||||
Make Epiphany's local operator workflow bridge-aware: GUI graph inspection,
|
||||
Rider source-context receipts, Unity pinned-runtime receipts, and explicit
|
||||
dogfood artifacts before the next Aetheria implementation pass.
|
||||
Keep EpiphanyAgent focused on harness, typed-state, coordinator, bridge, and
|
||||
backend surfaces while the Aquarium operator UI lives in its own sibling repo
|
||||
with its own persistent interface memory and doctrine.
|
||||
|
||||
constraints:
|
||||
- No frontier-scale training budget.
|
||||
|
|
@ -65,9 +65,9 @@ active_subgoals:
|
|||
- Keep the supervisor out of Epiphany worker thought streams: dogfood artifacts should be operator-safe by default, with direct transcripts and raw result payloads sealed.
|
||||
- Use `tools/epiphany_agent_telemetry.py` and generated `agent-function-telemetry.json` files to inspect behavior shape without opening sealed transcripts.
|
||||
- Treat repeated no-diff implementation turns as an MVP blocker, not success. A bounded implementation pass must leave a reviewable workspace diff or an explicit reviewable failure artifact before dogfood can be called healthy.
|
||||
- Build the Rider and Unity bridges before the next Aetheria implementation dogfood run: the Aetheria-side resident Unity editor package, named Unity bridge operation planner, GUI Environment panel, EpiphanyGraph-backed GUI graph dashboard, Rider context bridge CLI, and Rider plugin MVP scaffold are now landed; the Rider plugin still needs build verification.
|
||||
- Build the Rider and Unity bridges before the next Aetheria implementation dogfood run: the Aetheria-side resident Unity editor package, named Unity bridge operation planner, Aquarium Environment surface, EpiphanyGraph-backed graph dashboard, Rider context bridge CLI, and Rider plugin MVP scaffold are now landed; the Rider plugin still needs build verification.
|
||||
- Preserve the newly landed GUI Planning view, explicit Objective Draft adoption action, and Imagination planning synthesis lane; defer GitHub Issues import until a backlog source is fresh enough to be worth feeding Imagination.
|
||||
- Treat the target workflow as three integrated surfaces: Rider is the human code/diff view, Epiphany GUI is the agent dashboard and persisted-state graph view, and Unity is the pinned runtime/test/scene environment.
|
||||
- Treat the target workflow as three integrated surfaces: Rider is the human code/diff view, Epiphany Aquarium is the agent dashboard and persisted-state graph view, and Unity is the pinned runtime/test/scene environment.
|
||||
- Use `notes/epiphany-rider-unity-integration-plan.md` as the source-grounded plan for Rider-as-IDE and Unity-as-editor/runtime integration seams.
|
||||
|
||||
accepted_design:
|
||||
|
|
@ -86,8 +86,8 @@ accepted_design:
|
|||
- Context pressure must become an explicit runtime signal for automatic CRRC; inference from transcript vibes, user warnings, or shrinking safe scope is useful only as a temporary human/agent heuristic.
|
||||
- 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/planning 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 Imagination/modeling/verification/reorient launch and read-back, explicit Imagination planning acceptance, explicit Objective Draft adoption, and explicit review-gated reorient acceptance.
|
||||
- The Aquarium operator UI now lives in sibling repo `E:\Projects\EpiphanyAquarium`, with its own `AGENTS.md`, `state/map.yaml`, `state/memory.json`, and interface doctrine. It remains a local Tauri v2 + React/WebGL client over EpiphanyAgent's app-server/tooling surfaces; app-server and typed Epiphany state remain authoritative.
|
||||
- EpiphanyAgent no longer carries the embedded `apps/epiphany-gui` source tree. Backend artifact directories and action tools still use the historical `.epiphany-gui` contract until that backend naming seam is deliberately renamed.
|
||||
- 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.
|
||||
|
|
@ -136,7 +136,7 @@ accepted_design:
|
|||
- `tools/epiphany_agent_telemetry.py` derives operator-safe function/API telemetry from sealed app-server transcripts so the supervisor can see calls, statuses, paths, and counts without reading direct thoughts.
|
||||
- Editor/runtime verification is now part of the MVP product boundary for engine repos: `tools/epiphany_unity_bridge.py` is the first controlled bridge, resolving the project-pinned Unity editor, refusing wrong or missing versions, running explicit batch/probe commands only through the exact editor, and writing inspection/command/log artifacts for evidence.
|
||||
- Rider and Unity are environment organs, not sources of truth: Rider should expose IDE/source context, diagnostics, navigation, and changed ranges; Unity should expose pinned-editor runtime facts, compilation, tests, probes, assets, scenes, and logs; Epiphany remains the durable Self that coordinates, records, and reviews.
|
||||
- The product architecture is three-pronged: Rider is the code view for human inspection of repo state, source tree, diffs, diagnostics, and code refs; Epiphany GUI is the agent dashboard for objectives, specialist lane state, logs/artifacts, persisted typed state, and graph/control-flow views; Unity is the runtime environment for pinned tests, probes, scene configuration, assets, shaders, and play/edit-mode evidence.
|
||||
- The product architecture is three-pronged: Rider is the code view for human inspection of repo state, source tree, diffs, diagnostics, and code refs; Epiphany Aquarium is the agent dashboard for objectives, specialist lane state, logs/artifacts, persisted typed state, and graph/control-flow views; Unity is the runtime environment for pinned tests, probes, scene configuration, assets, shaders, and play/edit-mode evidence.
|
||||
- Epiphany needs a workspace-scoped planning substrate separate from the active objective: conversation captures, inbox, normalized backlog, roadmap streams, objective drafts, and explicit objective adoption are distinct states. The active objective remains the bounded run contract; chat and imported issues are planning inputs until reviewed and promoted. `notes/epiphany-planning-substrate.md` defines the product model, including GitHub Issues import semantics.
|
||||
- The first runtime planning slice is landed inside `EpiphanyThreadState`: typed captures, backlog items, roadmap streams, Objective Drafts, and GitHub source refs are revision-gated through `thread/epiphany/update`, rendered into prompts when present, and reflected read-only through `thread/epiphany/planning`.
|
||||
- The first GUI planning operator slice is landed: status collection includes `thread/epiphany/planning`, the Tauri/React dashboard renders planning summaries, captures, backlog, roadmap streams, and Objective Drafts, and the `adoptObjectiveDraft` GUI action requires an explicit selected draft before updating objective, subgoal, planning status, scratch, checkpoint, observation, evidence, and adoption artifacts.
|
||||
|
|
@ -144,8 +144,8 @@ accepted_design:
|
|||
- The Imagination lane is now a fixed roleLaunch/roleResult/roleAccept specialist over the existing job-control seam: it may launch as `planning-synthesis-worker`, return a reviewable planning-only `statePatch`, and update captures/backlog/roadmap/objective drafts after explicit acceptance without adopting an objective or touching implementation/modeling state.
|
||||
- Unity integration must be editor-resident, not text-archeology-only: scene files, prefabs, materials, shaders, ScriptableObjects, asset GUIDs, prefab overrides, and serialized component fields should be inspected through a Unity Editor package using Unity APIs, with narrow mutation preview/apply commands and artifact receipts.
|
||||
- The first resident Unity editor package now lives in the Aetheria branch `codex/epiphany-unity-editor-bridge`: `Assets/Editor/Epiphany/EpiphanyEditorBridge.cs` exposes editor-resident inspect, compilation, scene, prefab, serialized-object, and reference-search probes with JSON/Markdown artifacts. Epiphany's Unity bridge detects that package and plans named probe/test commands through `GameCult.Epiphany.Unity.EpiphanyEditorBridge.RunProbe`, while runtime execution remains blocked until the exact project-pinned Unity editor is present.
|
||||
- The GUI Environment panel now reflects latest Unity and Rider bridge audits: Unity shows project-pinned editor status, resident package presence, execute method, installed/candidate editor paths, search roots, and artifact bundle details; Rider shows executable, solution, branch, dirty state, changed files, installations, and bridge artifacts.
|
||||
- The adjacent EpiphanyGraph React viewer is now embedded in the GUI graph dashboard through the local package dependency; it consumes `graphs.architecture`, `graphs.dataflow`, and `graphs.links` directly and exposes node/edge/code-ref inspection with machine-readable graph data behind the visual layout.
|
||||
- The Aquarium Environment surface reflects latest Unity and Rider bridge audits: Unity shows project-pinned editor status, resident package presence, execute method, installed/candidate editor paths, search roots, and artifact bundle details; Rider shows executable, solution, branch, dirty state, changed files, installations, and bridge artifacts.
|
||||
- The adjacent EpiphanyGraph React viewer is embedded in the Aquarium graph dashboard through the local package dependency; it consumes `graphs.architecture`, `graphs.dataflow`, and `graphs.links` directly and exposes node/edge/code-ref inspection with machine-readable graph data behind the visual layout.
|
||||
- `tools/epiphany_rider_bridge.py` is the first operator-safe Rider source-context bridge: it inspects Rider installation/solution/VCS status, captures current file/selection/symbol context packets, plans explicit Rider code-ref opens, writes `.epiphany-gui/rider` artifacts, and feeds implementation prompt guardrails without reading sealed worker streams or owning durable state.
|
||||
- GitHub Issues should import into planning captures first, not directly into active objectives. The importer should preserve repository, issue number, ids, URL, title/body, labels, milestone, assignees, author, state, timestamps, comment metadata, PR marker, and optional Projects v2 metadata while keeping GitHub as a source adapter rather than Epiphany's internal backlog model.
|
||||
- If the agent's persistent instructions, memory, workflow, or state shape are making it less coherent, slower to rehydrate, less honest, or more likely to build Jenga, the agent should ask the user to change it.
|
||||
|
|
@ -170,13 +170,13 @@ 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, successful pre-compaction handoff 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, then latches a pending compact handoff for clean turn completion.
|
||||
- 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.
|
||||
- Phase 6 Aquarium extraction: the Tauri v2 + React/WebGL operator console moved from embedded `apps/epiphany-gui` into sibling repo `E:\Projects\EpiphanyAquarium`, with its own distilled interface state, memory, doctrine, build, visual smoke, and Tauri check.
|
||||
- Phase 6 Unity runtime bridge: `tools/epiphany_unity_bridge.py` plus GUI Inspect Unity action resolve pinned Unity editors, reject wrong/missing versions, and write runtime artifacts before engine assumptions are treated as testable.
|
||||
- Planning substrate design: `notes/epiphany-planning-substrate.md` formalizes conversation capture, inbox, backlog, roadmap streams, objective drafts, explicit adoption, the Imagination planning role, and GitHub Issues import.
|
||||
- Phase 6 planning substrate runtime slice: typed planning state in `EpiphanyThreadState`, prompt rendering, revision-gated update validation, read-only `thread/epiphany/planning`, scene action reflection, generated schemas, and live planning smoke coverage.
|
||||
- Phase 6 GUI planning operator slice: MVP status and Tauri/React GUI now render planning state, and `tools/epiphany_gui_action.py` exposes explicit selected Objective Draft adoption with artifacted state patch/update proof.
|
||||
- Phase 6 Imagination role lane: `thread/epiphany/roles` now exposes Imagination/planning, `roleLaunch` can launch the fixed `planning-synthesis-worker`, `roleResult` projects its finding, `roleAccept` applies planning-only patches with review-gated Objective Drafts, and the GUI/status tools can launch, read, and accept that lane.
|
||||
- Phase 6 GUI graph and Rider bridge slice: the Tauri/React GUI now embeds the adjacent EpiphanyGraph viewer over typed graph state, `tools/epiphany_rider_bridge.py` writes Rider/source-context artifacts, GUI Inspect Rider surfaces those artifacts, and `integrations/rider` scaffolds the first frontend-only Rider tool-window/context-action plugin.
|
||||
- Phase 6 Aquarium graph and Rider bridge slice: the Tauri/React/WebGL Aquarium embeds the adjacent EpiphanyGraph viewer over typed graph state, `tools/epiphany_rider_bridge.py` writes Rider/source-context artifacts, Aquarium Inspect Rider surfaces those artifacts, and `integrations/rider` scaffolds the first frontend-only Rider tool-window/context-action plugin.
|
||||
- Phase 6 role self-memory slice: Ghostlight-shaped role dossiers live under `state/agents`, specialists may request bounded `selfPatch` lane-memory mutations, coordinator review projects acceptance/refusal reasons through `selfPersistence`, and GUI/coordinator accept paths apply accepted requests with `tools/epiphany_agent_memory.py`.
|
||||
guardrails:
|
||||
- Use `tools/epiphany_phase5_smoke.py` before modifying update, distill, propose, promote, read-projection, or notification behavior.
|
||||
|
|
@ -201,7 +201,7 @@ landed_baseline:
|
|||
- Use `tools/epiphany_unity_bridge_smoke.py` before modifying the Unity editor/runtime bridge.
|
||||
- Use `tools/epiphany_rider_bridge_smoke.py` before modifying the Rider source-context bridge.
|
||||
- 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 `npm run build`, `npm run smoke:visual`, `cargo fmt --check`, `cargo check`, and `npm run tauri -- build --debug --no-bundle` under `E:\Projects\EpiphanyAquarium` before claiming Aquarium 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.
|
||||
|
|
@ -220,5 +220,5 @@ rejected_paths:
|
|||
reason: persistent state that accumulates every local truth becomes memory cruft and weakens rehydration
|
||||
|
||||
current_status:
|
||||
summary: Phase 1 durable state through Phase 6 GUI/coordinator/specialist/CRRC MVP surfaces are landed. The Aetheria supervised run found and fixed real harness wounds in coordinator policy, no-diff audits, stale job/result projection, and reorient acceptance. The Unity incident proved the next blocker is environment access, so `notes/epiphany-rider-unity-integration-plan.md` governs the Rider/Epiphany GUI/Unity workflow. Planning is now an operator-visible loop, the GUI renders typed graph state through the adjacent EpiphanyGraph viewer, and Rider now has an operator-safe source-context bridge plus a first plugin scaffold. Runtime Unity execution remains blocked until the exact project-pinned editor exists.
|
||||
next_action: Build-verify or package the Rider plugin scaffold when Gradle/wrapper support is available, install the Aetheria-pinned Unity 6000.1.10f1 editor, then run the next Aetheria dogfood pass through the GUI/Rider/Unity bridge surfaces. Defer GitHub Issues import until the backlog source is fresh enough to be useful.
|
||||
summary: Phase 1 durable state through Phase 6 coordinator/specialist/CRRC/backend surfaces are landed. The Aquarium operator UI has been split into sibling repo `E:\Projects\EpiphanyAquarium` with its own distilled interface doctrine and persistent memory. EpiphanyAgent remains the authoritative harness/backend repo; Aquarium is the whimsical local client over those surfaces. Runtime Unity execution remains blocked until the exact project-pinned editor exists.
|
||||
next_action: Continue Aquarium UI work in `E:\Projects\EpiphanyAquarium`; keep EpiphanyAgent changes to backend contracts, typed state, coordinator policy, and bridge tools. Build-verify or package the Rider plugin scaffold when Gradle/wrapper support is available, install the Aetheria-pinned Unity 6000.1.10f1 editor, then run the next Aetheria dogfood pass through Aquarium/Rider/Unity bridge surfaces.
|
||||
|
|
|
|||
Reference in a new issue