1714 lines
66 KiB
Python
1714 lines
66 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
from copy import deepcopy
|
|
from datetime import datetime
|
|
from datetime import timezone
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import time
|
|
import tomllib
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from epiphany_mvp_status import DEFAULT_APP_SERVER
|
|
from epiphany_mvp_status import collect_status
|
|
from epiphany_mvp_status import render_status
|
|
from epiphany_mvp_status import sanitize_for_operator
|
|
from epiphany_mvp_status import write_transcript_telemetry
|
|
from epiphany_phase5_smoke import AppServerClient
|
|
from epiphany_phase5_smoke import ROOT
|
|
from epiphany_phase6_reorient_launch_smoke import BINDING_ID as REORIENT_BINDING_ID
|
|
|
|
|
|
DEFAULT_CODEX_HOME = ROOT / ".epiphany-gui" / "codex-home"
|
|
DEFAULT_ARTIFACT_ROOT = ROOT / ".epiphany-gui" / "actions"
|
|
DEFAULT_AGENT_MEMORY_DIR = ROOT / "state" / "agents.msgpack"
|
|
DEFAULT_HEARTBEAT_STORE = ROOT / "state" / "agent-heartbeats.msgpack"
|
|
DEFAULT_HEARTBEAT_ARTIFACT_DIR = ROOT / ".epiphany-heartbeats"
|
|
DEFAULT_FACE_ARTIFACT_DIR = ROOT / ".epiphany-face"
|
|
HEARTBEAT_DISPLAY_NAMES = {
|
|
"coordinator": "Self",
|
|
"face": "Face",
|
|
"imagination": "Imagination",
|
|
"research": "Eyes",
|
|
"modeling": "Body",
|
|
"implementation": "Hands",
|
|
"verification": "Soul",
|
|
"reorientation": "Life",
|
|
}
|
|
HEARTBEAT_ROLE_ORDER = list(HEARTBEAT_DISPLAY_NAMES)
|
|
EPIPHANY_PROMPTS_PATH = (
|
|
ROOT
|
|
/ "vendor"
|
|
/ "codex"
|
|
/ "codex-rs"
|
|
/ "app-server"
|
|
/ "src"
|
|
/ "prompts"
|
|
/ "epiphany_specialists.toml"
|
|
)
|
|
TERMINAL_ROLE_STATUSES = {"completed", "failed", "cancelled"}
|
|
TERMINAL_REORIENT_STATUSES = {"completed", "failed", "cancelled"}
|
|
|
|
|
|
def native_epiphany_bin(bin_name: str, *args: str) -> dict[str, Any]:
|
|
exe = Path(os.environ.get("CARGO_TARGET_DIR", r"C:\Users\Meta\.cargo-target-codex")) / "debug" / f"{bin_name}.exe"
|
|
if exe.exists():
|
|
command = [str(exe), *args]
|
|
else:
|
|
command = [
|
|
"cargo",
|
|
"run",
|
|
"--quiet",
|
|
"--manifest-path",
|
|
str(ROOT / "epiphany-core" / "Cargo.toml"),
|
|
"--bin",
|
|
bin_name,
|
|
"--",
|
|
*args,
|
|
]
|
|
completed = subprocess.run(command, cwd=ROOT, text=True, capture_output=True, check=False)
|
|
if completed.returncode != 0:
|
|
raise RuntimeError(completed.stderr.strip() or completed.stdout.strip() or f"{bin_name} failed")
|
|
return json.loads(completed.stdout)
|
|
|
|
|
|
def heartbeat_status(*, store_file: Path, artifact_dir: Path, limit: int = 8) -> dict[str, Any]:
|
|
return native_epiphany_bin(
|
|
"epiphany-heartbeat-store",
|
|
"status",
|
|
"--store",
|
|
str(store_file),
|
|
"--artifact-dir",
|
|
str(artifact_dir),
|
|
"--limit",
|
|
str(limit),
|
|
)
|
|
|
|
|
|
def write_bubble(content: str, *, artifact_dir: Path, source: str, status: str = "ready", mood: str = "attentive") -> Path:
|
|
response = native_epiphany_bin(
|
|
"epiphany-face-discord",
|
|
"bubble",
|
|
"--artifact-dir",
|
|
str(artifact_dir),
|
|
"--content",
|
|
content,
|
|
"--source",
|
|
source,
|
|
"--status",
|
|
status,
|
|
"--mood",
|
|
mood,
|
|
)
|
|
return Path(response["bubblePath"])
|
|
|
|
|
|
def latest_face_artifacts(artifact_dir: Path) -> list[dict[str, Any]]:
|
|
response = native_epiphany_bin(
|
|
"epiphany-face-discord",
|
|
"latest",
|
|
"--artifact-dir",
|
|
str(artifact_dir),
|
|
)
|
|
return response.get("latestArtifacts", [])
|
|
|
|
|
|
def maybe_apply_role_self_patch(
|
|
response: dict[str, Any], *, agent_memory_dir: Path
|
|
) -> dict[str, Any] | None:
|
|
finding = response.get("finding")
|
|
if not isinstance(finding, dict):
|
|
return None
|
|
self_patch = finding.get("selfPatch")
|
|
review = finding.get("selfPersistence")
|
|
if not isinstance(self_patch, dict) or not isinstance(review, dict):
|
|
return None
|
|
if review.get("status") != "accepted":
|
|
return {
|
|
"status": "rejected",
|
|
"targetAgentId": review.get("targetAgentId"),
|
|
"targetPath": review.get("targetPath"),
|
|
"reasons": review.get("reasons") or ["coordinator refused the selfPatch"],
|
|
"applied": False,
|
|
}
|
|
role_id = response.get("roleId")
|
|
if not isinstance(role_id, str) or not role_id:
|
|
return {
|
|
"status": "rejected",
|
|
"reasons": ["roleAccept response did not include a roleId for selfPatch application"],
|
|
"applied": False,
|
|
}
|
|
result = native_epiphany_bin(
|
|
"epiphany-agent-memory-store",
|
|
"apply-patch",
|
|
"--store",
|
|
str(agent_memory_dir),
|
|
"--role-id",
|
|
role_id,
|
|
"--patch",
|
|
json.dumps(self_patch),
|
|
)
|
|
result["appliedFromRoleAccept"] = True
|
|
return result
|
|
|
|
|
|
def load_epiphany_prompt_config() -> dict[str, Any]:
|
|
with EPIPHANY_PROMPTS_PATH.open("rb") as handle:
|
|
config = tomllib.load(handle)
|
|
if not isinstance(config, dict):
|
|
raise ValueError(f"invalid Epiphany prompt config: {EPIPHANY_PROMPTS_PATH}")
|
|
return config
|
|
|
|
|
|
def implementation_continue_template() -> str:
|
|
config = load_epiphany_prompt_config()
|
|
shared = config.get("shared")
|
|
persistent_memory = None
|
|
if isinstance(shared, dict):
|
|
persistent_memory = shared.get("persistent_memory")
|
|
template = nested_get(config, "implementation", "continue_template")
|
|
if not isinstance(template, str) or not template.strip():
|
|
raise ValueError(
|
|
"missing [implementation].continue_template in "
|
|
f"{EPIPHANY_PROMPTS_PATH}"
|
|
)
|
|
body = template.strip()
|
|
if isinstance(persistent_memory, str) and persistent_memory.strip():
|
|
return f"{persistent_memory.strip()}\n\n{body}"
|
|
return body
|
|
|
|
|
|
def prompt_text(value: Any, default: str = "none") -> str:
|
|
if value is None:
|
|
return default
|
|
text = str(value)
|
|
if not text.strip():
|
|
return default
|
|
return text
|
|
|
|
|
|
def run_git(cwd: Path, *args: str) -> dict[str, Any]:
|
|
try:
|
|
completed = subprocess.run(
|
|
["git", *args],
|
|
cwd=cwd,
|
|
capture_output=True,
|
|
check=False,
|
|
encoding="utf-8",
|
|
errors="replace",
|
|
)
|
|
except OSError as error:
|
|
return {"ok": False, "error": str(error), "args": list(args)}
|
|
return {
|
|
"ok": completed.returncode == 0,
|
|
"returncode": completed.returncode,
|
|
"args": list(args),
|
|
"stdout": completed.stdout.strip(),
|
|
"stderr": completed.stderr.strip(),
|
|
}
|
|
|
|
|
|
def git_diff_hash(cwd: Path) -> dict[str, Any]:
|
|
diff = run_git(cwd, "diff", "--binary")
|
|
if not diff.get("ok"):
|
|
return diff
|
|
text = str(diff.get("stdout") or "")
|
|
return {
|
|
"ok": True,
|
|
"sha256": hashlib.sha256(text.encode("utf-8", errors="replace")).hexdigest(),
|
|
"bytes": len(text.encode("utf-8", errors="replace")),
|
|
}
|
|
|
|
|
|
def git_snapshot(cwd: Path) -> dict[str, Any]:
|
|
return {
|
|
"head": run_git(cwd, "rev-parse", "--short", "HEAD"),
|
|
"branch": run_git(cwd, "branch", "--show-current"),
|
|
"status": run_git(cwd, "status", "--short", "--branch"),
|
|
"changedFiles": run_git(cwd, "diff", "--name-only"),
|
|
"untrackedFiles": run_git(cwd, "ls-files", "--others", "--exclude-standard"),
|
|
"diffStat": run_git(cwd, "diff", "--stat"),
|
|
"diffHash": git_diff_hash(cwd),
|
|
}
|
|
|
|
|
|
def split_git_lines(value: Any) -> list[str]:
|
|
text = str(value or "").strip()
|
|
return [line for line in text.splitlines() if line.strip()]
|
|
|
|
|
|
def normalize_repo_path(path: str) -> str:
|
|
return path.strip().replace("\\", "/")
|
|
|
|
|
|
def parse_review_file_note(note: str) -> list[str]:
|
|
if ":" not in note:
|
|
return []
|
|
_, raw_paths = note.split(":", 1)
|
|
paths: list[str] = []
|
|
for raw_path in raw_paths.split(","):
|
|
path = normalize_repo_path(raw_path)
|
|
if not path or path.lower() in {"none", "unknown files"} or "*" in path:
|
|
continue
|
|
paths.append(path)
|
|
return paths
|
|
|
|
|
|
def parent_path(path: str) -> str:
|
|
normalized = normalize_repo_path(path)
|
|
if "/" not in normalized:
|
|
return ""
|
|
return normalized.rsplit("/", 1)[0]
|
|
|
|
|
|
def is_under(path: str, root: str) -> bool:
|
|
normalized = normalize_repo_path(path)
|
|
if not root:
|
|
return "/" not in normalized
|
|
return normalized == root or normalized.startswith(f"{root}/")
|
|
|
|
|
|
def review_untracked_files(
|
|
*,
|
|
changed_files: list[str],
|
|
new_untracked_files: list[str],
|
|
after_untracked: set[str],
|
|
active_review_files: list[str] | None = None,
|
|
) -> list[str]:
|
|
active_review_file_set = {
|
|
normalize_repo_path(path)
|
|
for path in active_review_files or []
|
|
if normalize_repo_path(path)
|
|
}
|
|
review_roots = {
|
|
parent_path(path)
|
|
for path in [*changed_files, *new_untracked_files, *active_review_file_set]
|
|
if path
|
|
}
|
|
if not review_roots:
|
|
return sorted(new_untracked_files)
|
|
return sorted(
|
|
path
|
|
for path in after_untracked
|
|
if path in new_untracked_files
|
|
or path in active_review_file_set
|
|
or any(is_under(path, root) for root in review_roots)
|
|
)
|
|
|
|
|
|
def git_change_summary(
|
|
before: dict[str, Any],
|
|
after: dict[str, Any],
|
|
*,
|
|
active_review_files: list[str] | None = None,
|
|
) -> dict[str, Any]:
|
|
before_status = set(str(nested_get(before, "status", "stdout") or "").splitlines())
|
|
after_status = set(str(nested_get(after, "status", "stdout") or "").splitlines())
|
|
added_status = sorted(after_status - before_status)
|
|
removed_status = sorted(before_status - after_status)
|
|
before_changed_files = split_git_lines(nested_get(before, "changedFiles", "stdout"))
|
|
changed_files = split_git_lines(nested_get(after, "changedFiles", "stdout"))
|
|
before_untracked = set(split_git_lines(nested_get(before, "untrackedFiles", "stdout")))
|
|
after_untracked = set(split_git_lines(nested_get(after, "untrackedFiles", "stdout")))
|
|
new_untracked_files = sorted(after_untracked - before_untracked)
|
|
active_review_files = sorted(
|
|
{
|
|
normalize_repo_path(path)
|
|
for path in active_review_files or []
|
|
if normalize_repo_path(path)
|
|
}
|
|
)
|
|
untracked_files = review_untracked_files(
|
|
changed_files=changed_files,
|
|
new_untracked_files=new_untracked_files,
|
|
after_untracked=after_untracked,
|
|
active_review_files=active_review_files,
|
|
)
|
|
workspace_files = sorted({*changed_files, *untracked_files})
|
|
ignored_untracked_count = max(0, len(after_untracked) - len(set(untracked_files)))
|
|
before_diff_stat = str(nested_get(before, "diffStat", "stdout") or "").strip()
|
|
diff_stat = str(nested_get(after, "diffStat", "stdout") or "").strip()
|
|
before_diff_hash = nested_get(before, "diffHash", "sha256")
|
|
after_diff_hash = nested_get(after, "diffHash", "sha256")
|
|
if isinstance(before_diff_hash, str) and isinstance(after_diff_hash, str):
|
|
tracked_diff_changed = before_diff_hash != after_diff_hash
|
|
else:
|
|
tracked_diff_changed = before_changed_files != changed_files or before_diff_stat != diff_stat
|
|
workspace_changed = bool(
|
|
added_status or removed_status or new_untracked_files or tracked_diff_changed
|
|
)
|
|
return {
|
|
"workspaceChanged": workspace_changed,
|
|
"dirtyWorkspace": bool(changed_files or after_untracked or diff_stat),
|
|
"trackedDiffPresent": bool(changed_files or diff_stat),
|
|
"trackedDiffChanged": tracked_diff_changed,
|
|
"beforeChangedFiles": before_changed_files,
|
|
"changedFiles": changed_files,
|
|
"untrackedFiles": untracked_files,
|
|
"newUntrackedFiles": new_untracked_files,
|
|
"activeReviewFiles": active_review_files,
|
|
"workspaceFiles": workspace_files,
|
|
"ignoredUntrackedFileCount": ignored_untracked_count,
|
|
"statusAdded": added_status,
|
|
"statusRemoved": removed_status,
|
|
"diffStat": diff_stat,
|
|
}
|
|
|
|
|
|
def render_implementation_audit(result: dict[str, Any]) -> str:
|
|
changed_files = result.get("changedFiles")
|
|
if not isinstance(changed_files, list):
|
|
changed_files = []
|
|
status_added = result.get("statusAdded")
|
|
if not isinstance(status_added, list):
|
|
status_added = []
|
|
status_removed = result.get("statusRemoved")
|
|
if not isinstance(status_removed, list):
|
|
status_removed = []
|
|
untracked_files = result.get("untrackedFiles")
|
|
if not isinstance(untracked_files, list):
|
|
untracked_files = []
|
|
new_untracked_files = result.get("newUntrackedFiles")
|
|
if not isinstance(new_untracked_files, list):
|
|
new_untracked_files = []
|
|
active_review_files = result.get("activeReviewFiles")
|
|
if not isinstance(active_review_files, list):
|
|
active_review_files = []
|
|
ignored_untracked_count = result.get("ignoredUntrackedFileCount")
|
|
if not isinstance(ignored_untracked_count, int):
|
|
ignored_untracked_count = 0
|
|
workspace_files = result.get("workspaceFiles")
|
|
if not isinstance(workspace_files, list):
|
|
workspace_files = []
|
|
|
|
outcome = (
|
|
"reviewable new workspace diff"
|
|
if result.get("workspaceChanged")
|
|
else "no new workspace diff"
|
|
)
|
|
next_action = (
|
|
"Review the changed files before accepting this implementation slice."
|
|
if result.get("workspaceChanged")
|
|
else "Stop and review this as an implementation-lane failure before rerunning; the worker completed without changing the target workspace further."
|
|
)
|
|
return "\n".join(
|
|
[
|
|
"# Implementation Audit",
|
|
"",
|
|
f"Outcome: {outcome}",
|
|
f"Dirty workspace present: {bool(result.get('dirtyWorkspace'))}",
|
|
f"Tracked diff present: {bool(result.get('trackedDiffPresent'))}",
|
|
f"Tracked diff changed this turn: {bool(result.get('trackedDiffChanged'))}",
|
|
"",
|
|
"Changed files:",
|
|
*(f"- {path}" for path in changed_files),
|
|
*([] if changed_files else ["- none"]),
|
|
"",
|
|
"Untracked files:",
|
|
*(f"- {path}" for path in untracked_files),
|
|
*([] if untracked_files else ["- none"]),
|
|
"",
|
|
"New untracked files this turn:",
|
|
*(f"- {path}" for path in new_untracked_files),
|
|
*([] if new_untracked_files else ["- none"]),
|
|
"",
|
|
"Carried review files from Epiphany scratch:",
|
|
*(f"- {path}" for path in active_review_files),
|
|
*([] if active_review_files else ["- none"]),
|
|
"",
|
|
f"Ignored unrelated untracked files: {ignored_untracked_count}",
|
|
"",
|
|
"Workspace files needing review:",
|
|
*(f"- {path}" for path in workspace_files),
|
|
*([] if workspace_files else ["- none"]),
|
|
"",
|
|
"Git status delta:",
|
|
*(f"- added: {line}" for line in status_added),
|
|
*(f"- removed: {line}" for line in status_removed),
|
|
*([] if status_added or status_removed else ["- none"]),
|
|
"",
|
|
f"Next action: {next_action}",
|
|
"",
|
|
]
|
|
)
|
|
|
|
|
|
def nested_get(value: dict[str, Any], *keys: str) -> Any:
|
|
current: Any = value
|
|
for key in keys:
|
|
if not isinstance(current, dict):
|
|
return None
|
|
current = current.get(key)
|
|
return current
|
|
|
|
|
|
def active_review_files_from_status(status: dict[str, Any]) -> list[str]:
|
|
state = nested_get(status, "read", "thread", "epiphanyState")
|
|
if not isinstance(state, dict):
|
|
return []
|
|
scratch = state.get("scratch")
|
|
if not isinstance(scratch, dict):
|
|
return []
|
|
notes = scratch.get("notes")
|
|
if not isinstance(notes, list):
|
|
return []
|
|
files: list[str] = []
|
|
for note in notes:
|
|
if not isinstance(note, str):
|
|
continue
|
|
if note.startswith(
|
|
(
|
|
"Tracked files:",
|
|
"Review untracked files:",
|
|
"Untracked files:",
|
|
"New untracked files this turn:",
|
|
)
|
|
):
|
|
files.extend(parse_review_file_note(note))
|
|
return sorted(set(files))
|
|
|
|
|
|
def unity_guidance(cwd: Path) -> str:
|
|
try:
|
|
result = native_epiphany_bin(
|
|
"epiphany-unity-bridge",
|
|
"guidance",
|
|
"--project-path",
|
|
str(cwd),
|
|
)
|
|
except RuntimeError as error:
|
|
return f"- Unity bridge: native guidance failed: {error}"
|
|
return str(result.get("guidance") or "- Unity bridge: native guidance returned no text.")
|
|
|
|
|
|
def rider_guidance(cwd: Path) -> str:
|
|
try:
|
|
result = native_epiphany_bin(
|
|
"epiphany-rider-bridge",
|
|
"guidance",
|
|
"--project-root",
|
|
str(cwd),
|
|
)
|
|
except RuntimeError as error:
|
|
return f"- Rider bridge: native guidance failed: {error}"
|
|
return str(result.get("guidance") or "- Rider bridge: native guidance returned no text.")
|
|
|
|
|
|
def first_present(value: dict[str, Any], *paths: tuple[str, ...]) -> Any:
|
|
for path in paths:
|
|
item = nested_get(value, *path)
|
|
if item not in (None, "", []):
|
|
return item
|
|
return None
|
|
|
|
|
|
def summarized_recent_evidence(state: dict[str, Any], limit: int = 8) -> list[str]:
|
|
recent = state.get("recent_evidence")
|
|
if not isinstance(recent, list):
|
|
return []
|
|
lines: list[str] = []
|
|
for item in recent[:limit]:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
summary = item.get("summary")
|
|
if not isinstance(summary, str) or not summary.strip():
|
|
continue
|
|
evidence_id = item.get("id", "unknown")
|
|
kind = item.get("kind", "unknown")
|
|
status = item.get("status", "unknown")
|
|
lines.append(f"- {evidence_id} [{kind}/{status}]: {summary}")
|
|
return lines
|
|
|
|
|
|
def build_implementation_prompt(status: dict[str, Any], cwd: Path) -> str:
|
|
operator_status = sanitize_for_operator(status)
|
|
state = first_present(
|
|
operator_status,
|
|
("read", "thread", "epiphanyState"),
|
|
("scene", "scene", "epiphanyState"),
|
|
)
|
|
if not isinstance(state, dict):
|
|
state = {}
|
|
coordinator = operator_status.get("coordinator")
|
|
if not isinstance(coordinator, dict):
|
|
coordinator = {}
|
|
crrc = nested_get(operator_status, "crrc", "decision")
|
|
if not isinstance(crrc, dict):
|
|
crrc = {}
|
|
|
|
checkpoint = first_present(
|
|
state,
|
|
("graph_checkpoint",),
|
|
("graphCheckpoint",),
|
|
)
|
|
if not isinstance(checkpoint, dict):
|
|
checkpoint = {}
|
|
frontier = first_present(state, ("graph_frontier",), ("graphFrontier",))
|
|
if not isinstance(frontier, dict):
|
|
frontier = {}
|
|
scratch = state.get("scratch")
|
|
if not isinstance(scratch, dict):
|
|
scratch = {}
|
|
|
|
evidence_lines = summarized_recent_evidence(state)
|
|
if not evidence_lines:
|
|
evidence_lines = ["- none recorded"]
|
|
|
|
active_node_ids = frontier.get("active_node_ids") or frontier.get("activeNodeIds") or []
|
|
if isinstance(active_node_ids, list):
|
|
active_node_text = ", ".join(str(item) for item in active_node_ids)
|
|
else:
|
|
active_node_text = str(active_node_ids or "none")
|
|
|
|
template = implementation_continue_template()
|
|
return template.format(
|
|
coordinator_action=prompt_text(coordinator.get("action")),
|
|
coordinator_target_role=prompt_text(coordinator.get("targetRole")),
|
|
coordinator_reason=prompt_text(coordinator.get("reason")),
|
|
checkpoint_id=prompt_text(
|
|
checkpoint.get("checkpoint_id") or checkpoint.get("checkpointId")
|
|
),
|
|
checkpoint_summary=prompt_text(checkpoint.get("summary")),
|
|
frontier_active_nodes=prompt_text(active_node_text),
|
|
continuity_next_action=prompt_text(
|
|
crrc.get("nextAction")
|
|
or nested_get(operator_status, "reorient", "decision", "nextAction")
|
|
),
|
|
scratch_summary=prompt_text(scratch.get("summary")),
|
|
accepted_evidence="\n".join(evidence_lines),
|
|
unity_guidance=unity_guidance(cwd),
|
|
rider_guidance=rider_guidance(cwd),
|
|
)
|
|
|
|
|
|
def implementation_no_diff_patch(
|
|
status: dict[str, Any],
|
|
*,
|
|
artifact_dir: Path,
|
|
implementation_result: dict[str, Any],
|
|
) -> dict[str, Any] | None:
|
|
operator_status = sanitize_for_operator(status)
|
|
state = first_present(
|
|
operator_status,
|
|
("read", "thread", "epiphanyState"),
|
|
("scene", "scene", "epiphanyState"),
|
|
)
|
|
if not isinstance(state, dict):
|
|
return None
|
|
checkpoint = first_present(
|
|
state,
|
|
("investigation_checkpoint",),
|
|
("investigationCheckpoint",),
|
|
)
|
|
if not isinstance(checkpoint, dict):
|
|
return None
|
|
|
|
evidence_id = f"ev-implementation-no-diff-{artifact_dir.name}"
|
|
observation_id = f"obs-implementation-no-diff-{artifact_dir.name}"
|
|
updated_checkpoint = deepcopy(checkpoint)
|
|
updated_checkpoint["disposition"] = "regather_required"
|
|
updated_checkpoint["summary"] = (
|
|
"Implementation turn completed with no new workspace diff; the current "
|
|
"checkpoint is insufficient to drive the coding lane safely."
|
|
)
|
|
updated_checkpoint["next_action"] = (
|
|
"Review the no-diff implementation audit, then repair the implementation "
|
|
"lane through modeling or reorientation before continuing code work."
|
|
)
|
|
evidence_ids = updated_checkpoint.get("evidence_ids")
|
|
if not isinstance(evidence_ids, list):
|
|
evidence_ids = []
|
|
if evidence_id not in evidence_ids:
|
|
evidence_ids = [*evidence_ids, evidence_id]
|
|
updated_checkpoint["evidence_ids"] = evidence_ids
|
|
|
|
return {
|
|
"investigationCheckpoint": updated_checkpoint,
|
|
"observations": [
|
|
{
|
|
"id": observation_id,
|
|
"summary": (
|
|
"The implementation lane completed a bounded turn but left "
|
|
"no new tracked or untracked workspace delta, so continuation "
|
|
"needs review or checkpoint repair."
|
|
),
|
|
"source_kind": "implementation",
|
|
"status": "blocked",
|
|
"evidence_ids": [evidence_id],
|
|
}
|
|
],
|
|
"evidence": [
|
|
{
|
|
"id": evidence_id,
|
|
"kind": "implementation-audit",
|
|
"status": "blocked",
|
|
"summary": (
|
|
"continueImplementation produced no new workspace diff "
|
|
f"(trackedDiffPresent={bool(implementation_result.get('trackedDiffPresent'))}); "
|
|
f"artifact={artifact_dir.name}."
|
|
),
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
def implementation_diff_patch(
|
|
*,
|
|
artifact_dir: Path,
|
|
implementation_result: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
evidence_id = f"ev-implementation-diff-{artifact_dir.name}"
|
|
observation_id = f"obs-implementation-diff-{artifact_dir.name}"
|
|
workspace_files = implementation_result.get("workspaceFiles")
|
|
if not isinstance(workspace_files, list):
|
|
workspace_files = []
|
|
changed_files = implementation_result.get("changedFiles")
|
|
if not isinstance(changed_files, list):
|
|
changed_files = []
|
|
untracked_files = implementation_result.get("untrackedFiles")
|
|
if not isinstance(untracked_files, list):
|
|
untracked_files = []
|
|
new_untracked_files = implementation_result.get("newUntrackedFiles")
|
|
if not isinstance(new_untracked_files, list):
|
|
new_untracked_files = []
|
|
ignored_untracked_count = implementation_result.get("ignoredUntrackedFileCount")
|
|
if not isinstance(ignored_untracked_count, int):
|
|
ignored_untracked_count = 0
|
|
|
|
file_text = ", ".join(str(path) for path in workspace_files[:12]) or "unknown files"
|
|
code_refs = [
|
|
{
|
|
"path": str(path),
|
|
"note": "Changed by bounded continueImplementation evidence-gathering turn.",
|
|
}
|
|
for path in workspace_files[:20]
|
|
]
|
|
summary = (
|
|
"continueImplementation produced a reviewable workspace diff "
|
|
f"(trackedDiffPresent={bool(implementation_result.get('trackedDiffPresent'))}); "
|
|
f"artifact={artifact_dir.name}; files={file_text}."
|
|
)
|
|
return {
|
|
"observations": [
|
|
{
|
|
"id": observation_id,
|
|
"summary": (
|
|
"The implementation lane produced a bounded workspace diff "
|
|
"from the current accepted modeling checkpoint; verification "
|
|
"should review the changed files before implementation continues."
|
|
),
|
|
"source_kind": "implementation",
|
|
"status": "accepted",
|
|
"evidence_ids": [evidence_id],
|
|
"code_refs": code_refs,
|
|
}
|
|
],
|
|
"evidence": [
|
|
{
|
|
"id": evidence_id,
|
|
"kind": "implementation-audit",
|
|
"status": "ok",
|
|
"summary": summary,
|
|
"code_refs": code_refs,
|
|
}
|
|
],
|
|
"scratch": {
|
|
"summary": (
|
|
"Implementation produced a reviewable diff; next safe move is "
|
|
"verification of the changed workspace files, including untracked files."
|
|
),
|
|
"next_probe": "Run verification/review against the implementation diff before continuing.",
|
|
"notes": [
|
|
f"Tracked files: {', '.join(str(path) for path in changed_files) or 'none'}",
|
|
f"Untracked files: {', '.join(str(path) for path in untracked_files) or 'none'}",
|
|
f"New untracked files this turn: {', '.join(str(path) for path in new_untracked_files) or 'none'}",
|
|
f"Ignored unrelated untracked files: {ignored_untracked_count}",
|
|
],
|
|
},
|
|
}
|
|
|
|
|
|
def wait_for_role_result(
|
|
client: AppServerClient,
|
|
*,
|
|
thread_id: str,
|
|
role_id: str,
|
|
timeout_seconds: int,
|
|
poll_seconds: float,
|
|
) -> dict[str, Any]:
|
|
deadline = time.time() + timeout_seconds
|
|
latest: dict[str, Any] | None = None
|
|
while time.time() < deadline:
|
|
latest = client.send(
|
|
"thread/epiphany/roleResult",
|
|
{"threadId": thread_id, "roleId": role_id},
|
|
)
|
|
assert latest is not None
|
|
if latest.get("status") in TERMINAL_ROLE_STATUSES:
|
|
return latest
|
|
time.sleep(poll_seconds)
|
|
assert latest is not None
|
|
return latest
|
|
|
|
|
|
def wait_for_reorient_result(
|
|
client: AppServerClient,
|
|
*,
|
|
thread_id: str,
|
|
timeout_seconds: int,
|
|
poll_seconds: float,
|
|
) -> dict[str, Any]:
|
|
deadline = time.time() + timeout_seconds
|
|
latest: dict[str, Any] | None = None
|
|
while time.time() < deadline:
|
|
latest = client.send(
|
|
"thread/epiphany/reorientResult",
|
|
{"threadId": thread_id, "bindingId": REORIENT_BINDING_ID},
|
|
)
|
|
assert latest is not None
|
|
if latest.get("status") in TERMINAL_REORIENT_STATUSES:
|
|
return latest
|
|
time.sleep(poll_seconds)
|
|
assert latest is not None
|
|
return latest
|
|
|
|
|
|
def checkpoint_patch(cwd: Path) -> dict[str, Any]:
|
|
generated_at = datetime.now(timezone.utc).isoformat()
|
|
return {
|
|
"objective": "Reach a testable Epiphany MVP through the local GUI/operator workflow.",
|
|
"activeSubgoalId": "epiphany-gui-mvp",
|
|
"subgoals": [
|
|
{
|
|
"id": "epiphany-gui-mvp",
|
|
"title": "Make the Epiphany GUI usable for local operator testing",
|
|
"status": "active",
|
|
"summary": "The operator can prepare durable state, inspect coordinator guidance, and launch/read fixed specialist lanes without terminal work.",
|
|
}
|
|
],
|
|
"graphs": {
|
|
"architecture": {
|
|
"nodes": [
|
|
{
|
|
"id": "gui-operator-console",
|
|
"title": "Tauri/React operator console",
|
|
"purpose": "Human-facing surface for status, artifacts, and bounded lane actions.",
|
|
"code_refs": [
|
|
{
|
|
"path": "apps/epiphany-gui/src/App.tsx",
|
|
"start_line": 1,
|
|
"end_line": 1,
|
|
"symbol": "App",
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"id": "gui-action-bridge",
|
|
"title": "GUI action bridge",
|
|
"purpose": "Python bridge that invokes fixed app-server Epiphany lane APIs and writes auditable artifacts.",
|
|
"code_refs": [
|
|
{
|
|
"path": "tools/epiphany_gui_action.py",
|
|
"start_line": 1,
|
|
"end_line": 1,
|
|
"symbol": "run_action",
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"id": "app-server-epiphany-control-plane",
|
|
"title": "App-server Epiphany control plane",
|
|
"purpose": "Typed state, coordinator, CRRC, role, reorient, and job surfaces used by the GUI.",
|
|
"code_refs": [
|
|
{
|
|
"path": "vendor/codex/codex-rs/app-server/src/codex_message_processor.rs",
|
|
"start_line": 1,
|
|
"end_line": 1,
|
|
"symbol": "thread/epiphany",
|
|
}
|
|
],
|
|
},
|
|
]
|
|
},
|
|
"dataflow": {
|
|
"nodes": [
|
|
{
|
|
"id": "operator-action-flow",
|
|
"title": "Operator action flow",
|
|
"purpose": "GUI button -> Tauri command -> Python bridge -> app-server API -> artifact bundle -> refreshed GUI status.",
|
|
}
|
|
]
|
|
},
|
|
"links": [
|
|
{
|
|
"dataflow_node_id": "operator-action-flow",
|
|
"architecture_node_id": "gui-operator-console",
|
|
"relationship": "operator-surface",
|
|
},
|
|
{
|
|
"dataflow_node_id": "operator-action-flow",
|
|
"architecture_node_id": "gui-action-bridge",
|
|
"relationship": "bridge",
|
|
},
|
|
{
|
|
"dataflow_node_id": "operator-action-flow",
|
|
"architecture_node_id": "app-server-epiphany-control-plane",
|
|
"relationship": "control-plane",
|
|
},
|
|
],
|
|
},
|
|
"graphFrontier": {
|
|
"active_node_ids": [
|
|
"gui-operator-console",
|
|
"gui-action-bridge",
|
|
"app-server-epiphany-control-plane",
|
|
],
|
|
"dirty_paths": [],
|
|
},
|
|
"graphCheckpoint": {
|
|
"checkpoint_id": f"gui-operator-{int(time.time())}",
|
|
"graph_revision": 1,
|
|
"summary": "GUI-prepared checkpoint for local Epiphany MVP operator testing.",
|
|
"frontier_node_ids": [
|
|
"gui-operator-console",
|
|
"gui-action-bridge",
|
|
"app-server-epiphany-control-plane",
|
|
],
|
|
},
|
|
"investigationCheckpoint": {
|
|
"checkpoint_id": f"ix-gui-operator-{int(time.time())}",
|
|
"kind": "source_gathering",
|
|
"disposition": "resume_ready",
|
|
"focus": "Validate the GUI operator loop against the fixed Epiphany lane APIs.",
|
|
"summary": "The GUI is the operator surface; app-server typed state remains the source of truth.",
|
|
"next_action": "Use the GUI to run coordinator plan, launch/read specialist lanes, and inspect written artifacts.",
|
|
"captured_at_turn_id": f"gui-prepare-{int(time.time())}",
|
|
"code_refs": [
|
|
{
|
|
"path": "apps/epiphany-gui/src/App.tsx",
|
|
"start_line": 1,
|
|
"end_line": 1,
|
|
"symbol": "App",
|
|
},
|
|
{
|
|
"path": "tools/epiphany_gui_action.py",
|
|
"start_line": 1,
|
|
"end_line": 1,
|
|
"symbol": "run_action",
|
|
},
|
|
],
|
|
},
|
|
"scratch": {
|
|
"summary": "Prepared from the GUI so the operator has a durable thread before launching fixed lanes.",
|
|
"current_focus": "Test the local GUI MVP workflow end to end.",
|
|
"next_action": "Run coordinator guidance, then launch/read modeling, verification, or reorient lanes as recommended.",
|
|
"updated_at": generated_at,
|
|
"cwd": str(cwd),
|
|
},
|
|
"churn": {
|
|
"understanding_status": "ready",
|
|
"diff_pressure": "low",
|
|
"graph_freshness": "fresh",
|
|
"unexplained_writes": 0,
|
|
},
|
|
}
|
|
|
|
|
|
def write_json(path: Path, value: Any) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(value, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
|
|
|
|
def write_text(path: Path, value: str) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(value, encoding="utf-8")
|
|
|
|
|
|
def state_revision(status: dict[str, Any]) -> int | None:
|
|
state = status.get("read", {}).get("thread", {}).get("epiphanyState")
|
|
if isinstance(state, dict):
|
|
revision = state.get("revision")
|
|
if isinstance(revision, int):
|
|
return revision
|
|
scene_revision = status.get("scene", {}).get("scene", {}).get("revision")
|
|
return scene_revision if isinstance(scene_revision, int) else None
|
|
|
|
|
|
def status_epiphany_state(status: dict[str, Any]) -> dict[str, Any]:
|
|
state = first_present(
|
|
status,
|
|
("read", "thread", "epiphanyState"),
|
|
("scene", "scene", "epiphanyState"),
|
|
)
|
|
return state if isinstance(state, dict) else {}
|
|
|
|
|
|
def status_planning_state(status: dict[str, Any]) -> dict[str, Any]:
|
|
planning_response = status.get("planning")
|
|
if isinstance(planning_response, dict):
|
|
planning = planning_response.get("planning")
|
|
if isinstance(planning, dict):
|
|
return planning
|
|
state = status_epiphany_state(status)
|
|
planning = state.get("planning")
|
|
return planning if isinstance(planning, dict) else {}
|
|
|
|
|
|
def planning_drafts(planning: dict[str, Any]) -> list[dict[str, Any]]:
|
|
drafts = planning.get("objective_drafts") or planning.get("objectiveDrafts")
|
|
if not isinstance(drafts, list):
|
|
return []
|
|
return [draft for draft in drafts if isinstance(draft, dict)]
|
|
|
|
|
|
def planning_backlog_items(planning: dict[str, Any]) -> list[dict[str, Any]]:
|
|
items = planning.get("backlog_items") or planning.get("backlogItems")
|
|
if not isinstance(items, list):
|
|
return []
|
|
return [item for item in items if isinstance(item, dict)]
|
|
|
|
|
|
def string_list(value: Any) -> list[str]:
|
|
if not isinstance(value, list):
|
|
return []
|
|
return [str(item) for item in value if str(item).strip()]
|
|
|
|
|
|
def objective_subgoal_id(draft_id: str) -> str:
|
|
cleaned = "".join(
|
|
char if char.isalnum() or char in {"-", "_", "."} else "-"
|
|
for char in draft_id.strip()
|
|
).strip("-")
|
|
return f"objective-{cleaned or int(time.time())}"
|
|
|
|
|
|
def update_or_append_subgoal(
|
|
state: dict[str, Any],
|
|
*,
|
|
subgoal_id: str,
|
|
title: str,
|
|
summary: str,
|
|
) -> list[dict[str, Any]]:
|
|
raw_subgoals = state.get("subgoals")
|
|
subgoals = deepcopy(raw_subgoals) if isinstance(raw_subgoals, list) else []
|
|
subgoal = {
|
|
"id": subgoal_id,
|
|
"title": title,
|
|
"status": "active",
|
|
"summary": summary,
|
|
}
|
|
for index, item in enumerate(subgoals):
|
|
if isinstance(item, dict) and item.get("id") == subgoal_id:
|
|
subgoals[index] = {**item, **subgoal}
|
|
return subgoals
|
|
subgoals.append(subgoal)
|
|
return subgoals
|
|
|
|
|
|
def adoption_note_list(
|
|
scratch: dict[str, Any],
|
|
*,
|
|
draft_id: str,
|
|
acceptance_criteria: list[str],
|
|
review_gates: list[str],
|
|
) -> list[str]:
|
|
notes = [
|
|
f"Adopted Objective Draft {draft_id} from planning state.",
|
|
"Acceptance criteria: "
|
|
+ (", ".join(acceptance_criteria[:6]) if acceptance_criteria else "none recorded"),
|
|
"Review gates: " + (", ".join(review_gates[:6]) if review_gates else "none recorded"),
|
|
]
|
|
existing = scratch.get("notes")
|
|
if isinstance(existing, list):
|
|
notes.extend(str(note) for note in existing if str(note).strip())
|
|
return notes[:20]
|
|
|
|
|
|
def adopted_investigation_checkpoint(
|
|
state: dict[str, Any],
|
|
*,
|
|
artifact_dir: Path,
|
|
evidence_id: str,
|
|
draft_id: str,
|
|
title: str,
|
|
summary: str,
|
|
) -> dict[str, Any]:
|
|
existing = first_present(
|
|
state,
|
|
("investigation_checkpoint",),
|
|
("investigationCheckpoint",),
|
|
)
|
|
checkpoint = existing if isinstance(existing, dict) else {}
|
|
evidence_ids = string_list(
|
|
checkpoint.get("evidence_ids") or checkpoint.get("evidenceIds")
|
|
)
|
|
if evidence_id not in evidence_ids:
|
|
evidence_ids = [evidence_id, *evidence_ids]
|
|
return {
|
|
"checkpoint_id": str(
|
|
checkpoint.get("checkpoint_id")
|
|
or checkpoint.get("checkpointId")
|
|
or f"objective-adoption-{int(time.time())}"
|
|
),
|
|
"kind": "objective_adoption",
|
|
"disposition": "resume_ready",
|
|
"focus": title,
|
|
"summary": f"Human adopted Objective Draft {draft_id}: {summary}",
|
|
"next_action": (
|
|
"Run coordinator guidance from the adopted objective; launch modeling "
|
|
"before implementation if the current graph/checkpoint is insufficient."
|
|
),
|
|
"captured_at_turn_id": f"objective-adoption-{artifact_dir.name}",
|
|
"evidence_ids": evidence_ids,
|
|
}
|
|
|
|
|
|
def adopt_objective_draft_patch(
|
|
status: dict[str, Any],
|
|
*,
|
|
draft_id: str,
|
|
cwd: Path,
|
|
artifact_dir: Path,
|
|
) -> dict[str, Any]:
|
|
if not draft_id.strip():
|
|
raise ValueError("adoptObjectiveDraft requires --planning-draft-id")
|
|
state = status_epiphany_state(status)
|
|
planning = deepcopy(status_planning_state(status))
|
|
if not planning:
|
|
raise ValueError("adoptObjectiveDraft requires existing planning state")
|
|
|
|
draft_items = planning_drafts(planning)
|
|
selected_draft: dict[str, Any] | None = None
|
|
for draft in draft_items:
|
|
if draft.get("id") == draft_id:
|
|
selected_draft = draft
|
|
break
|
|
if selected_draft is None:
|
|
raise ValueError(f"objective draft not found: {draft_id}")
|
|
|
|
draft_status = str(selected_draft.get("status") or "").strip().lower()
|
|
if draft_status in {"adopted", "rejected", "superseded"}:
|
|
raise ValueError(f"objective draft {draft_id} is {draft_status} and cannot be adopted")
|
|
|
|
title = str(selected_draft.get("title") or "").strip()
|
|
summary = str(selected_draft.get("summary") or "").strip()
|
|
if not title or not summary:
|
|
raise ValueError(f"objective draft {draft_id} is missing title or summary")
|
|
|
|
generated_at = datetime.now(timezone.utc).isoformat()
|
|
source_item_ids = string_list(
|
|
selected_draft.get("source_item_ids") or selected_draft.get("sourceItemIds")
|
|
)
|
|
acceptance_criteria = string_list(
|
|
selected_draft.get("acceptance_criteria")
|
|
or selected_draft.get("acceptanceCriteria")
|
|
)
|
|
review_gates = string_list(
|
|
selected_draft.get("review_gates") or selected_draft.get("reviewGates")
|
|
)
|
|
|
|
for draft in draft_items:
|
|
if draft.get("id") == draft_id:
|
|
draft["status"] = "adopted"
|
|
backlog_items = planning_backlog_items(planning)
|
|
for item in backlog_items:
|
|
if item.get("id") in source_item_ids and str(item.get("status", "")).lower() in {
|
|
"ready",
|
|
"triaged",
|
|
"draft",
|
|
}:
|
|
item["status"] = "active"
|
|
item["updated_at"] = generated_at
|
|
|
|
subgoal_id = objective_subgoal_id(draft_id)
|
|
evidence_id = f"ev-objective-adoption-{artifact_dir.name}"
|
|
observation_id = f"obs-objective-adoption-{artifact_dir.name}"
|
|
scratch = deepcopy(state.get("scratch")) if isinstance(state.get("scratch"), dict) else {}
|
|
scratch.update(
|
|
{
|
|
"summary": f"Adopted planning draft {draft_id} as the active objective: {summary}",
|
|
"hypothesis": summary,
|
|
"next_probe": (
|
|
"Use coordinator/modeling guidance to strengthen the graph and "
|
|
"checkpoint before implementation starts chasing this objective."
|
|
),
|
|
"notes": adoption_note_list(
|
|
scratch,
|
|
draft_id=draft_id,
|
|
acceptance_criteria=acceptance_criteria,
|
|
review_gates=review_gates,
|
|
),
|
|
}
|
|
)
|
|
|
|
patch = {
|
|
"objective": title,
|
|
"activeSubgoalId": subgoal_id,
|
|
"subgoals": update_or_append_subgoal(
|
|
state,
|
|
subgoal_id=subgoal_id,
|
|
title=title,
|
|
summary=summary,
|
|
),
|
|
"planning": planning,
|
|
"scratch": scratch,
|
|
"investigationCheckpoint": adopted_investigation_checkpoint(
|
|
state,
|
|
artifact_dir=artifact_dir,
|
|
evidence_id=evidence_id,
|
|
draft_id=draft_id,
|
|
title=title,
|
|
summary=summary,
|
|
),
|
|
"observations": [
|
|
{
|
|
"id": observation_id,
|
|
"summary": (
|
|
f"Objective Draft {draft_id} was explicitly adopted as the "
|
|
"active implementation objective by the operator."
|
|
),
|
|
"source_kind": "planning",
|
|
"status": "accepted",
|
|
"evidence_ids": [evidence_id],
|
|
}
|
|
],
|
|
"evidence": [
|
|
{
|
|
"id": evidence_id,
|
|
"kind": "planning-adoption",
|
|
"status": "accepted",
|
|
"summary": (
|
|
f"Human-gated planning adoption set active objective to {title!r}; "
|
|
f"source items={', '.join(source_item_ids) or 'none'}; cwd={cwd}."
|
|
),
|
|
}
|
|
],
|
|
}
|
|
return patch
|
|
|
|
|
|
def require_thread_id(thread_id: str | None, action: str) -> str:
|
|
if not thread_id:
|
|
raise ValueError(f"{action} requires a persistent thread id")
|
|
return thread_id
|
|
|
|
|
|
def is_unresumable_empty_thread_error(error: RuntimeError) -> bool:
|
|
message = str(error)
|
|
return "no rollout found for thread id" in message or "thread not loaded" in message
|
|
|
|
|
|
def heartbeat_bubble_text(event: dict[str, Any]) -> str:
|
|
role_id = str(event.get("selectedRole") or "unknown")
|
|
display = HEARTBEAT_DISPLAY_NAMES.get(role_id, role_id)
|
|
action_type = event.get("actionType") or "heartbeat"
|
|
coordinator_action = event.get("coordinatorAction") or "idle cadence"
|
|
if action_type == "ruminate_memory":
|
|
return f"{display} won an idle heartbeat and is distilling its own role memory."
|
|
return f"{display} caught heartbeat work for {coordinator_action}."
|
|
|
|
|
|
def run_local_surface_action(args: argparse.Namespace, *, artifact_dir: Path) -> dict[str, Any]:
|
|
if args.action == "heartbeatStatus":
|
|
response = heartbeat_status(
|
|
store_file=args.heartbeat_store_file,
|
|
artifact_dir=args.heartbeat_artifact_dir,
|
|
)
|
|
summary = "Read the Ghostlight heartbeat initiative status."
|
|
elif args.action == "runHeartbeat":
|
|
schedule_id = f"epiphany-gui-heartbeat-{time.time_ns()}-{os.getpid()}"
|
|
heartbeat_args = [
|
|
"tick",
|
|
"--store",
|
|
str(args.heartbeat_store_file),
|
|
"--artifact-dir",
|
|
str(args.heartbeat_artifact_dir),
|
|
"--target-heartbeat-rate",
|
|
str(args.target_heartbeat_rate),
|
|
"--urgency",
|
|
str(args.heartbeat_urgency),
|
|
"--schedule-id",
|
|
schedule_id,
|
|
"--source-scene-ref",
|
|
"epiphany/gui/heartbeat",
|
|
"--agent-store",
|
|
str(args.agent_memory_dir),
|
|
]
|
|
if args.heartbeat_coordinator_action:
|
|
heartbeat_args.extend(["--coordinator-action", args.heartbeat_coordinator_action])
|
|
if args.heartbeat_target_role:
|
|
heartbeat_args.extend(["--target-role", args.heartbeat_target_role])
|
|
if args.apply_rumination:
|
|
heartbeat_args.append("--apply-rumination")
|
|
response = native_epiphany_bin("epiphany-heartbeat-store", *heartbeat_args)
|
|
event = response.get("event") or {}
|
|
bubble_path = write_bubble(
|
|
heartbeat_bubble_text(event),
|
|
artifact_dir=args.face_artifact_dir,
|
|
source=f"heartbeat/{schedule_id}",
|
|
status="ready",
|
|
mood="awake",
|
|
)
|
|
response["faceBubblePath"] = str(bubble_path)
|
|
response["faceBubble"] = json.loads(bubble_path.read_text(encoding="utf-8"))
|
|
summary = "Ran one heartbeat slot and emitted a Face bubble for Aquarium."
|
|
elif args.action == "faceBubble":
|
|
content = args.face_content
|
|
if not content:
|
|
current = heartbeat_status(
|
|
store_file=args.heartbeat_store_file,
|
|
artifact_dir=args.heartbeat_artifact_dir,
|
|
)
|
|
latest = current.get("latestEvent") or {}
|
|
content = heartbeat_bubble_text(latest) if latest else "Face is listening. No heartbeat event has spoken yet."
|
|
bubble_path = write_bubble(
|
|
content,
|
|
artifact_dir=args.face_artifact_dir,
|
|
source=args.face_source,
|
|
status="ready",
|
|
mood=args.face_mood,
|
|
)
|
|
response = {
|
|
"ok": True,
|
|
"bubblePath": str(bubble_path),
|
|
"bubble": json.loads(bubble_path.read_text(encoding="utf-8")),
|
|
"latestFaceArtifacts": latest_face_artifacts(args.face_artifact_dir),
|
|
}
|
|
summary = "Created a Discord-independent Face bubble for Aquarium."
|
|
else:
|
|
raise ValueError(f"unsupported local surface action: {args.action}")
|
|
|
|
write_json(artifact_dir / "action-response.json", response)
|
|
result = {
|
|
"action": args.action,
|
|
"artifactPath": str(artifact_dir),
|
|
"summary": summary,
|
|
"threadId": args.thread_id or "",
|
|
"response": response,
|
|
"sealedArtifactManifest": [],
|
|
"telemetryPath": None,
|
|
}
|
|
write_json(artifact_dir / "gui-action-summary.json", result)
|
|
return result
|
|
|
|
|
|
def run_action(args: argparse.Namespace) -> dict[str, Any]:
|
|
thread_id = args.thread_id or ""
|
|
local_surface_actions = {"heartbeatStatus", "runHeartbeat", "faceBubble"}
|
|
if args.action not in {"prepareCheckpoint", *local_surface_actions}:
|
|
thread_id = require_thread_id(args.thread_id, args.action)
|
|
codex_home = args.codex_home.resolve()
|
|
artifact_dir = args.artifact_root.resolve() / f"{args.action}-{time.time_ns()}-{os.getpid()}"
|
|
transcript_path = artifact_dir / "transcript.jsonl"
|
|
stderr_path = artifact_dir / "server.stderr.log"
|
|
telemetry_path = artifact_dir / "agent-function-telemetry.json"
|
|
cwd = args.cwd.resolve()
|
|
codex_home.mkdir(parents=True, exist_ok=True)
|
|
artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
if args.action in local_surface_actions:
|
|
return run_local_surface_action(args, artifact_dir=artifact_dir)
|
|
|
|
with AppServerClient(args.app_server.resolve(), codex_home, transcript_path, stderr_path) as client:
|
|
client.send(
|
|
"initialize",
|
|
{
|
|
"clientInfo": {
|
|
"name": "epiphany-gui-action",
|
|
"title": "Epiphany GUI Action",
|
|
"version": "0.1.0",
|
|
},
|
|
"capabilities": {"experimentalApi": True},
|
|
},
|
|
)
|
|
client.send("initialized", expect_response=False)
|
|
if args.action == "prepareCheckpoint":
|
|
if args.thread_id:
|
|
try:
|
|
client.send("thread/resume", {"threadId": args.thread_id})
|
|
except RuntimeError as error:
|
|
if not is_unresumable_empty_thread_error(error):
|
|
raise
|
|
thread_id = ""
|
|
if not thread_id:
|
|
started = client.send("thread/start", {"cwd": str(cwd), "ephemeral": False})
|
|
if started is None:
|
|
raise RuntimeError("thread/start returned no response")
|
|
thread_id = started["thread"]["id"]
|
|
before = collect_status(client, thread_id=thread_id, cwd=cwd, ephemeral=True)
|
|
revision = state_revision(before)
|
|
response = client.send(
|
|
"thread/epiphany/update",
|
|
{
|
|
"threadId": thread_id,
|
|
"expectedRevision": revision if revision is not None else 0,
|
|
"patch": checkpoint_patch(cwd),
|
|
},
|
|
)
|
|
summary = "Prepared durable Epiphany checkpoint."
|
|
else:
|
|
client.send("thread/resume", {"threadId": thread_id})
|
|
before = collect_status(client, thread_id=thread_id, cwd=cwd, ephemeral=False)
|
|
revision = state_revision(before)
|
|
|
|
if args.action == "adoptObjectiveDraft":
|
|
if revision is None:
|
|
raise ValueError(
|
|
"adoptObjectiveDraft requires ready Epiphany state with a revision"
|
|
)
|
|
patch = adopt_objective_draft_patch(
|
|
before,
|
|
draft_id=args.planning_draft_id or "",
|
|
cwd=cwd,
|
|
artifact_dir=artifact_dir,
|
|
)
|
|
write_json(artifact_dir / "objective-adoption-state-patch.json", patch)
|
|
response = client.send(
|
|
"thread/epiphany/update",
|
|
{
|
|
"threadId": thread_id,
|
|
"expectedRevision": revision,
|
|
"patch": patch,
|
|
},
|
|
)
|
|
write_json(artifact_dir / "objective-adoption-state-update.json", response)
|
|
summary = (
|
|
"Adopted reviewed Objective Draft "
|
|
f"{args.planning_draft_id} as the active objective."
|
|
)
|
|
elif args.action in {"launchImagination", "launchModeling", "launchVerification"}:
|
|
role_id = {
|
|
"launchImagination": "imagination",
|
|
"launchModeling": "modeling",
|
|
"launchVerification": "verification",
|
|
}[args.action]
|
|
payload: dict[str, Any] = {
|
|
"threadId": thread_id,
|
|
"roleId": role_id,
|
|
"maxRuntimeSeconds": args.max_runtime_seconds,
|
|
}
|
|
if revision is not None:
|
|
payload["expectedRevision"] = revision
|
|
launch = client.send("thread/epiphany/roleLaunch", payload)
|
|
if args.wait:
|
|
result = wait_for_role_result(
|
|
client,
|
|
thread_id=thread_id,
|
|
role_id=role_id,
|
|
timeout_seconds=args.timeout_seconds,
|
|
poll_seconds=args.poll_seconds,
|
|
)
|
|
response = {"launch": launch, "result": result}
|
|
summary = f"Launched {role_id} role worker and waited for a reviewable result."
|
|
else:
|
|
response = launch
|
|
summary = f"Launched {role_id} role worker without waiting."
|
|
elif args.action in {
|
|
"readImaginationResult",
|
|
"readModelingResult",
|
|
"readVerificationResult",
|
|
}:
|
|
role_id = {
|
|
"readImaginationResult": "imagination",
|
|
"readModelingResult": "modeling",
|
|
"readVerificationResult": "verification",
|
|
}[args.action]
|
|
response = client.send(
|
|
"thread/epiphany/roleResult", {"threadId": thread_id, "roleId": role_id}
|
|
)
|
|
summary = f"Read {role_id} role result."
|
|
elif args.action == "acceptImagination":
|
|
if revision is None:
|
|
raise ValueError("acceptImagination requires ready Epiphany state with a revision")
|
|
response = client.send(
|
|
"thread/epiphany/roleAccept",
|
|
{
|
|
"threadId": thread_id,
|
|
"roleId": "imagination",
|
|
"expectedRevision": revision,
|
|
},
|
|
)
|
|
self_memory = maybe_apply_role_self_patch(
|
|
response, agent_memory_dir=args.agent_memory_dir
|
|
)
|
|
if self_memory is not None:
|
|
response["selfMemoryApply"] = self_memory
|
|
summary = "Accepted reviewed imagination planning patch."
|
|
elif args.action == "acceptModeling":
|
|
if revision is None:
|
|
raise ValueError("acceptModeling requires ready Epiphany state with a revision")
|
|
response = client.send(
|
|
"thread/epiphany/roleAccept",
|
|
{
|
|
"threadId": thread_id,
|
|
"roleId": "modeling",
|
|
"expectedRevision": revision,
|
|
},
|
|
)
|
|
self_memory = maybe_apply_role_self_patch(
|
|
response, agent_memory_dir=args.agent_memory_dir
|
|
)
|
|
if self_memory is not None:
|
|
response["selfMemoryApply"] = self_memory
|
|
summary = "Accepted reviewed modeling graph/checkpoint patch."
|
|
elif args.action == "acceptVerification":
|
|
if revision is None:
|
|
raise ValueError("acceptVerification requires ready Epiphany state with a revision")
|
|
response = client.send(
|
|
"thread/epiphany/roleAccept",
|
|
{
|
|
"threadId": thread_id,
|
|
"roleId": "verification",
|
|
"expectedRevision": revision,
|
|
},
|
|
)
|
|
self_memory = maybe_apply_role_self_patch(
|
|
response, agent_memory_dir=args.agent_memory_dir
|
|
)
|
|
if self_memory is not None:
|
|
response["selfMemoryApply"] = self_memory
|
|
summary = "Accepted reviewed verification finding."
|
|
elif args.action == "launchReorient":
|
|
payload = {"threadId": thread_id, "maxRuntimeSeconds": args.max_runtime_seconds}
|
|
if revision is not None:
|
|
payload["expectedRevision"] = revision
|
|
launch = client.send("thread/epiphany/reorientLaunch", payload)
|
|
if args.wait:
|
|
result = wait_for_reorient_result(
|
|
client,
|
|
thread_id=thread_id,
|
|
timeout_seconds=args.timeout_seconds,
|
|
poll_seconds=args.poll_seconds,
|
|
)
|
|
response = {"launch": launch, "result": result}
|
|
summary = "Launched fixed reorient-worker and waited for a reviewable result."
|
|
else:
|
|
response = launch
|
|
summary = "Launched fixed reorient-worker without waiting."
|
|
elif args.action == "readReorientResult":
|
|
response = client.send(
|
|
"thread/epiphany/reorientResult",
|
|
{"threadId": thread_id, "bindingId": REORIENT_BINDING_ID},
|
|
)
|
|
summary = "Read reorient-worker result."
|
|
elif args.action == "acceptReorient":
|
|
if revision is None:
|
|
raise ValueError("acceptReorient requires ready Epiphany state with a revision")
|
|
response = client.send(
|
|
"thread/epiphany/reorientAccept",
|
|
{
|
|
"threadId": thread_id,
|
|
"expectedRevision": revision,
|
|
"bindingId": REORIENT_BINDING_ID,
|
|
"updateScratch": True,
|
|
"updateInvestigationCheckpoint": True,
|
|
},
|
|
)
|
|
summary = "Accepted reviewed reorientation finding."
|
|
elif args.action == "continueImplementation":
|
|
coordinator = before.get("coordinator")
|
|
if not isinstance(coordinator, dict):
|
|
raise ValueError("continueImplementation requires coordinator status")
|
|
if coordinator.get("action") != "continueImplementation" and not args.force:
|
|
raise ValueError(
|
|
"coordinator is not recommending continueImplementation "
|
|
f"(got {coordinator.get('action')!r})"
|
|
)
|
|
pre_git = git_snapshot(cwd)
|
|
prompt = build_implementation_prompt(before, cwd)
|
|
write_text(artifact_dir / "implementation-prompt.md", prompt)
|
|
turn = client.send(
|
|
"turn/start",
|
|
{
|
|
"threadId": thread_id,
|
|
"cwd": str(cwd),
|
|
"sandboxPolicy": {
|
|
"type": "workspaceWrite",
|
|
"writableRoots": [str(cwd)],
|
|
"readOnlyAccess": {"type": "fullAccess"},
|
|
"networkAccess": False,
|
|
"excludeTmpdirEnvVar": False,
|
|
"excludeSlashTmp": False,
|
|
},
|
|
"input": [
|
|
{
|
|
"type": "text",
|
|
"text": prompt,
|
|
"textElements": [],
|
|
}
|
|
],
|
|
},
|
|
)
|
|
completed = client.wait_for_notification(
|
|
"turn/completed", timeout=args.timeout_seconds
|
|
)
|
|
post_git = git_snapshot(cwd)
|
|
implementation_result = git_change_summary(
|
|
pre_git,
|
|
post_git,
|
|
active_review_files=active_review_files_from_status(before),
|
|
)
|
|
write_json(artifact_dir / "git-before.json", pre_git)
|
|
write_json(artifact_dir / "git-after.json", post_git)
|
|
write_json(artifact_dir / "implementation-result.json", implementation_result)
|
|
write_text(
|
|
artifact_dir / "implementation-audit.md",
|
|
render_implementation_audit(implementation_result),
|
|
)
|
|
update_status = collect_status(client, thread_id=thread_id, cwd=cwd, ephemeral=False)
|
|
update_revision = state_revision(update_status)
|
|
response = {
|
|
"turn": turn,
|
|
"completed": sanitize_for_operator(completed),
|
|
"preGit": pre_git,
|
|
"postGit": post_git,
|
|
"implementationResult": implementation_result,
|
|
}
|
|
if implementation_result["workspaceChanged"]:
|
|
diff_patch = implementation_diff_patch(
|
|
artifact_dir=artifact_dir,
|
|
implementation_result=implementation_result,
|
|
)
|
|
if update_revision is not None:
|
|
write_json(artifact_dir / "implementation-state-patch.json", diff_patch)
|
|
diff_update = client.send(
|
|
"thread/epiphany/update",
|
|
{
|
|
"threadId": thread_id,
|
|
"expectedRevision": update_revision,
|
|
"patch": diff_patch,
|
|
},
|
|
)
|
|
write_json(artifact_dir / "implementation-state-update.json", diff_update)
|
|
response["implementationUpdate"] = sanitize_for_operator(diff_update)
|
|
summary = (
|
|
"Ran bounded implementation turn and produced a reviewable workspace diff."
|
|
)
|
|
else:
|
|
no_diff_patch = implementation_no_diff_patch(
|
|
before,
|
|
artifact_dir=artifact_dir,
|
|
implementation_result=implementation_result,
|
|
)
|
|
if no_diff_patch is not None and revision is not None:
|
|
write_json(artifact_dir / "no-diff-state-patch.json", no_diff_patch)
|
|
no_diff_update = client.send(
|
|
"thread/epiphany/update",
|
|
{
|
|
"threadId": thread_id,
|
|
"expectedRevision": update_revision
|
|
if update_revision is not None
|
|
else revision,
|
|
"patch": no_diff_patch,
|
|
},
|
|
)
|
|
write_json(artifact_dir / "no-diff-state-update.json", no_diff_update)
|
|
response["noDiffUpdate"] = sanitize_for_operator(no_diff_update)
|
|
summary = (
|
|
"Ran bounded implementation turn, but it produced no new workspace diff."
|
|
)
|
|
if response.get("noDiffUpdate"):
|
|
summary += " Marked the checkpoint for repair before retry."
|
|
else:
|
|
raise ValueError(f"unsupported GUI action: {args.action}")
|
|
|
|
assert response is not None
|
|
after = collect_status(client, thread_id=thread_id, cwd=cwd, ephemeral=True)
|
|
|
|
operator_before = sanitize_for_operator(before)
|
|
operator_response = sanitize_for_operator(response)
|
|
operator_after = sanitize_for_operator(after)
|
|
write_json(artifact_dir / "before-status.json", operator_before)
|
|
write_json(artifact_dir / "action-response.json", operator_response)
|
|
write_json(artifact_dir / "after-status.json", operator_after)
|
|
write_text(artifact_dir / "after-status.txt", render_status(operator_after))
|
|
result = {
|
|
"action": args.action,
|
|
"artifactPath": str(artifact_dir),
|
|
"summary": summary,
|
|
"threadId": thread_id,
|
|
"response": operator_response,
|
|
"sealedArtifactManifest": [
|
|
{
|
|
"path": "transcript.jsonl",
|
|
"reason": "sealed JSON-RPC audit trail; do not read during normal supervision",
|
|
},
|
|
{
|
|
"path": "server.stderr.log",
|
|
"reason": "sealed app-server diagnostics; inspect only for explicit debugging",
|
|
},
|
|
],
|
|
"telemetryPath": str(telemetry_path),
|
|
}
|
|
write_json(artifact_dir / "gui-action-summary.json", result)
|
|
write_transcript_telemetry(transcript_path, telemetry_path)
|
|
return result
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Run one bounded Epiphany GUI action.")
|
|
parser.add_argument("--app-server", type=Path, default=DEFAULT_APP_SERVER)
|
|
parser.add_argument("--codex-home", type=Path, default=DEFAULT_CODEX_HOME)
|
|
parser.add_argument("--cwd", type=Path, default=ROOT)
|
|
parser.add_argument("--thread-id")
|
|
parser.add_argument("--artifact-root", type=Path, default=DEFAULT_ARTIFACT_ROOT)
|
|
parser.add_argument("--agent-memory-dir", type=Path, default=DEFAULT_AGENT_MEMORY_DIR)
|
|
parser.add_argument("--max-runtime-seconds", type=int, default=180)
|
|
parser.add_argument("--timeout-seconds", type=int, default=300)
|
|
parser.add_argument("--poll-seconds", type=float, default=5.0)
|
|
parser.add_argument("--wait", action=argparse.BooleanOptionalAction, default=True)
|
|
parser.add_argument(
|
|
"--action",
|
|
required=True,
|
|
choices=[
|
|
"launchImagination",
|
|
"readImaginationResult",
|
|
"acceptImagination",
|
|
"launchModeling",
|
|
"readModelingResult",
|
|
"acceptModeling",
|
|
"launchVerification",
|
|
"readVerificationResult",
|
|
"acceptVerification",
|
|
"launchReorient",
|
|
"readReorientResult",
|
|
"acceptReorient",
|
|
"adoptObjectiveDraft",
|
|
"continueImplementation",
|
|
"prepareCheckpoint",
|
|
"heartbeatStatus",
|
|
"runHeartbeat",
|
|
"faceBubble",
|
|
],
|
|
)
|
|
parser.add_argument("--heartbeat-store-file", type=Path, default=DEFAULT_HEARTBEAT_STORE)
|
|
parser.add_argument("--heartbeat-artifact-dir", type=Path, default=DEFAULT_HEARTBEAT_ARTIFACT_DIR)
|
|
parser.add_argument("--face-artifact-dir", type=Path, default=DEFAULT_FACE_ARTIFACT_DIR)
|
|
parser.add_argument("--target-heartbeat-rate", type=float, default=1.0)
|
|
parser.add_argument("--heartbeat-coordinator-action", default="surfaceAgentThoughts")
|
|
parser.add_argument("--heartbeat-target-role", choices=HEARTBEAT_ROLE_ORDER)
|
|
parser.add_argument("--heartbeat-urgency", type=float, default=0.8)
|
|
parser.add_argument("--apply-rumination", action="store_true")
|
|
parser.add_argument("--face-content", help="Content for faceBubble. Defaults to latest heartbeat event.")
|
|
parser.add_argument("--face-source", default="epiphany/gui/faceBubble")
|
|
parser.add_argument("--face-mood", default="attentive")
|
|
parser.add_argument(
|
|
"--planning-draft-id",
|
|
help="Objective Draft id to adopt for adoptObjectiveDraft.",
|
|
)
|
|
parser.add_argument(
|
|
"--force",
|
|
action="store_true",
|
|
help="Allow continueImplementation even when the coordinator is not recommending it.",
|
|
)
|
|
args = parser.parse_args()
|
|
print(json.dumps(run_action(args), indent=2, ensure_ascii=False))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|