Move reorient smoke to Rust

This commit is contained in:
Meta Vens 2026-05-06 10:13:51 +01:00
commit bf3922ca91
7 changed files with 495 additions and 327 deletions

View file

@ -0,0 +1,489 @@
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use serde_json::Value;
use serde_json::json;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::thread;
use std::time::{Duration, Instant};
#[allow(dead_code)]
#[path = "epiphany-mvp-status.rs"]
mod status_cli;
const DEFAULT_APP_SERVER: &str = r"C:\Users\Meta\.cargo-target-codex\debug\codex-app-server.exe";
const WATCHED_RELATIVE_PATH: &str = "src/reorient_target.rs";
fn main() -> Result<()> {
let args = Args::parse()?;
let result = run_smoke(&args)?;
println!("{}", serde_json::to_string_pretty(&result)?);
Ok(())
}
#[derive(Debug)]
struct Args {
app_server: PathBuf,
codex_home: PathBuf,
workspace: PathBuf,
result: PathBuf,
transcript: PathBuf,
stderr: PathBuf,
}
impl Args {
fn parse() -> Result<Self> {
let root = env::current_dir().context("failed to resolve current dir")?;
let mut parsed = Self {
app_server: PathBuf::from(DEFAULT_APP_SERVER),
codex_home: root
.join(".epiphany-smoke")
.join("phase6-reorient-codex-home"),
workspace: root
.join(".epiphany-smoke")
.join("phase6-reorient-workspace"),
result: root
.join(".epiphany-smoke")
.join("phase6-reorient-smoke-result.json"),
transcript: root
.join(".epiphany-smoke")
.join("phase6-reorient-smoke-transcript.jsonl"),
stderr: root
.join(".epiphany-smoke")
.join("phase6-reorient-smoke-server.stderr.log"),
};
let mut args = env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--app-server" => parsed.app_server = take_path(&mut args, "--app-server")?,
"--codex-home" => parsed.codex_home = take_path(&mut args, "--codex-home")?,
"--workspace" => parsed.workspace = take_path(&mut args, "--workspace")?,
"--result" => parsed.result = take_path(&mut args, "--result")?,
"--transcript" => parsed.transcript = take_path(&mut args, "--transcript")?,
"--stderr" => parsed.stderr = take_path(&mut args, "--stderr")?,
other => return Err(anyhow!("unknown argument: {other}")),
}
}
Ok(parsed)
}
}
fn run_smoke(args: &Args) -> Result<Value> {
let root = env::current_dir().context("failed to resolve current dir")?;
let app_server = status_cli::absolute_path(&args.app_server)?;
if !app_server.exists() {
return Err(anyhow!(
"codex app-server binary not found: {}",
app_server.display()
));
}
let codex_home = status_cli::absolute_path(&args.codex_home)?;
let workspace = status_cli::absolute_path(&args.workspace)?;
let result_path = status_cli::absolute_path(&args.result)?;
let transcript_path = status_cli::absolute_path(&args.transcript)?;
let stderr_path = status_cli::absolute_path(&args.stderr)?;
reset_smoke_paths(
&root,
&[
codex_home.clone(),
workspace.clone(),
result_path.clone(),
transcript_path.clone(),
stderr_path.clone(),
],
)?;
fs::create_dir_all(&codex_home)
.with_context(|| format!("failed to create {}", codex_home.display()))?;
let watched_file = prepare_workspace(&workspace)?;
let mut client = status_cli::AppServerClient::start(
&app_server,
&codex_home,
&transcript_path,
&stderr_path,
)?;
client.send(
"initialize",
Some(json!({
"clientInfo": {
"name": "epiphany-phase6-reorient-smoke",
"title": "Epiphany Phase 6 Reorient Smoke",
"version": "0.1.0",
},
"capabilities": {"experimentalApi": true},
})),
true,
)?;
client.send("initialized", None, false)?;
let started = client.send(
"thread/start",
Some(json!({"cwd": workspace, "ephemeral": true})),
true,
)?;
let thread_id = started
.pointer("/thread/id")
.and_then(Value::as_str)
.ok_or_else(|| anyhow!("thread/start response missing thread.id"))?
.to_string();
let missing_notification_start = client.notification_len();
let missing_response = client.send(
"thread/epiphany/reorient",
Some(json!({"threadId": thread_id})),
true,
)?;
assert_missing_reorient(&missing_response)?;
client.require_no_notification(
"thread/epiphany/stateUpdated",
missing_notification_start,
Duration::from_secs(1),
)?;
let update_notification_start = client.notification_len();
let update = client.send(
"thread/epiphany/update",
Some(json!({
"threadId": thread_id,
"expectedRevision": 0,
"patch": reorient_patch(),
})),
true,
)?;
require(
update.get("revision").and_then(Value::as_u64) == Some(1),
"reorient smoke patch should advance revision to 1",
)?;
client.wait_for_notification(
"thread/epiphany/stateUpdated",
update_notification_start,
Duration::from_secs(10),
)?;
let ready_notification_start = client.notification_len();
let ready_response = client.send(
"thread/epiphany/reorient",
Some(json!({"threadId": thread_id})),
true,
)?;
assert_ready_reorient(&ready_response)?;
client.require_no_notification(
"thread/epiphany/stateUpdated",
ready_notification_start,
Duration::from_secs(1),
)?;
fs::write(
watched_file,
"pub fn reorient_target() -> &'static str {\n \"after\"\n}\n",
)?;
let regather_notification_start = client.notification_len();
let regather_response = wait_for_regather_reorient(&mut client, &thread_id)?;
assert_regather_reorient(&regather_response)?;
client.require_no_notification(
"thread/epiphany/stateUpdated",
regather_notification_start,
Duration::from_secs(1),
)?;
let final_read = client.send(
"thread/read",
Some(json!({"threadId": thread_id, "includeTurns": false})),
true,
)?;
require(
final_read
.pointer("/thread/epiphanyState/revision")
.and_then(Value::as_u64)
== Some(1),
"reorient reflection should not mutate durable state",
)?;
let result = json!({
"threadId": thread_id,
"codexHome": codex_home,
"workspace": workspace,
"missingAction": missing_response["decision"]["action"],
"readyAction": ready_response["decision"]["action"],
"regatherAction": regather_response["decision"]["action"],
"regatherReasons": regather_response["decision"]["reasons"],
"checkpointChangedPaths": regather_response["decision"]["checkpointChangedPaths"],
"activeFrontierNodeIds": regather_response["decision"]["activeFrontierNodeIds"],
"stateUpdatedNotificationCount": client.notification_count(
"thread/epiphany/stateUpdated",
regather_notification_start,
),
"finalReadRevision": final_read["thread"]["epiphanyState"]["revision"],
});
write_json(&result_path, &result)?;
Ok(result)
}
fn prepare_workspace(workspace: &Path) -> Result<PathBuf> {
fs::create_dir_all(workspace)?;
let watched_file = workspace.join(WATCHED_RELATIVE_PATH);
fs::create_dir_all(watched_file.parent().expect("watched path has parent"))?;
fs::write(
&watched_file,
"pub fn reorient_target() -> &'static str {\n \"before\"\n}\n",
)?;
Ok(watched_file)
}
fn reorient_patch() -> Value {
let code_ref = json!({
"path": WATCHED_RELATIVE_PATH,
"start_line": 1,
"end_line": 3,
"symbol": "reorient_target"
});
json!({
"objective": "Decide whether a durable checkpoint still deserves to be resumed after rehydrate.",
"activeSubgoalId": "phase6-reorient-smoke",
"subgoals": [{
"id": "phase6-reorient-smoke",
"title": "Live-smoke CRRC reorientation policy",
"status": "active",
"summary": "Resume when the checkpoint is still aligned; regather when the touched file proves it isn't."
}],
"graphs": {
"architecture": {
"nodes": [{
"id": "reorient-target",
"title": "Reorient target",
"purpose": "Map the file the watcher will touch so reorientation can notice drift.",
"code_refs": [code_ref.clone()]
}]
},
"dataflow": {"nodes": []},
"links": []
},
"graphFrontier": {
"active_node_ids": ["reorient-target"],
"dirty_paths": []
},
"graphCheckpoint": {
"checkpoint_id": "ck-reorient-1",
"graph_revision": 1,
"summary": "Reorientation smoke graph checkpoint",
"frontier_node_ids": ["reorient-target"]
},
"investigationCheckpoint": {
"checkpoint_id": "ix-reorient-1",
"kind": "source_gathering",
"disposition": "resume_ready",
"focus": "Verify the touched file before broad edits.",
"summary": "This checkpoint should remain resumable until the watched source moves.",
"next_action": "Resume the bounded slice if the watched source still matches the checkpoint.",
"captured_at_turn_id": "turn-phase6-reorient",
"code_refs": [code_ref]
},
"churn": {
"understanding_status": "ready",
"diff_pressure": "low",
"graph_freshness": "fresh",
"unexplained_writes": 0
}
})
}
fn assert_missing_reorient(response: &Value) -> Result<()> {
require(
response.get("source").and_then(Value::as_str) == Some("live"),
"missing reorient response should report live source",
)?;
require(
response.get("stateStatus").and_then(Value::as_str) == Some("missing"),
"missing reorient response should report missing state",
)?;
let decision = &response["decision"];
require(
decision.get("action").and_then(Value::as_str) == Some("regather"),
"missing reorient response should regather",
)?;
require(
decision.get("checkpointStatus").and_then(Value::as_str) == Some("missing"),
"missing reorient response should report missing checkpoint",
)?;
require(
string_array_eq(
decision.get("reasons").and_then(Value::as_array),
&["missingState", "missingCheckpoint"],
),
"missing reorient response should explain missing state and checkpoint",
)
}
fn assert_ready_reorient(response: &Value) -> Result<()> {
require(
response.get("source").and_then(Value::as_str) == Some("live"),
"ready reorient response should report live source",
)?;
require(
response.get("stateStatus").and_then(Value::as_str) == Some("ready"),
"ready reorient response should report ready state",
)?;
require(
response.get("stateRevision").and_then(Value::as_u64) == Some(1),
"ready reorient response should preserve revision identity",
)?;
let decision = &response["decision"];
require(
decision.get("action").and_then(Value::as_str) == Some("resume"),
"clean checkpoint should remain resumable",
)?;
require(
decision.get("checkpointStatus").and_then(Value::as_str) == Some("resumeReady"),
"ready reorient response should report resume-ready checkpoint status",
)?;
require(
string_array_eq(
decision.get("reasons").and_then(Value::as_array),
&["checkpointReady"],
),
"ready reorient response should explain that the checkpoint is still aligned",
)?;
require(
decision.get("checkpointId").and_then(Value::as_str) == Some("ix-reorient-1"),
"ready reorient response should expose the checkpoint id",
)?;
require(
decision.get("nextAction").and_then(Value::as_str)
== Some("Resume the bounded slice if the watched source still matches the checkpoint."),
"ready reorient response should preserve checkpoint next action",
)?;
require(
decision.get("watcherStatus").and_then(Value::as_str) == Some("clean"),
"ready reorient response should report a clean watcher before drift",
)
}
fn wait_for_regather_reorient(
client: &mut status_cli::AppServerClient,
thread_id: &str,
) -> Result<Value> {
let deadline = Instant::now() + Duration::from_secs(10);
let mut last_response = Value::Null;
while Instant::now() < deadline {
let response = client.send(
"thread/epiphany/reorient",
Some(json!({"threadId": thread_id})),
true,
)?;
if response.pointer("/decision/action").and_then(Value::as_str) == Some("regather") {
return Ok(response);
}
last_response = response;
thread::sleep(Duration::from_millis(200));
}
Err(anyhow!(
"reorientation policy did not switch to regather before timeout; last response: {last_response}"
))
}
fn assert_regather_reorient(response: &Value) -> Result<()> {
let decision = &response["decision"];
require(
decision.get("action").and_then(Value::as_str) == Some("regather"),
"touched checkpoint path should force regather",
)?;
require(
decision.get("checkpointStatus").and_then(Value::as_str) == Some("resumeReady"),
"regather response should preserve the underlying checkpoint disposition",
)?;
require(
string_array_eq(
decision.get("reasons").and_then(Value::as_array),
&["checkpointPathsChanged", "frontierChanged"],
),
"regather response should explain watcher path and frontier drift",
)?;
let changed_paths = decision
.get("checkpointChangedPaths")
.and_then(Value::as_array)
.ok_or_else(|| anyhow!("regather response missing changed paths"))?;
require(
changed_paths
.iter()
.filter_map(Value::as_str)
.map(|path| path.replace('\\', "/"))
.any(|path| path == WATCHED_RELATIVE_PATH),
"regather response should report the changed checkpoint path",
)?;
require(
string_array_eq(
decision
.get("activeFrontierNodeIds")
.and_then(Value::as_array),
&["reorient-target"],
),
"regather response should report touched frontier nodes",
)?;
require(
decision
.get("note")
.and_then(Value::as_str)
.is_some_and(|note| note.starts_with("Re-gather before editing:")),
"regather response should explain why the checkpoint is no longer safe to resume",
)
}
fn string_array_eq(values: Option<&Vec<Value>>, expected: &[&str]) -> bool {
values.is_some_and(|values| {
values.len() == expected.len()
&& values
.iter()
.zip(expected)
.all(|(value, expected)| value.as_str() == Some(*expected))
})
}
fn reset_smoke_paths(root: &Path, paths: &[PathBuf]) -> Result<()> {
let smoke_root = root.join(".epiphany-smoke").canonicalize().or_else(|_| {
let smoke_root = root.join(".epiphany-smoke");
fs::create_dir_all(&smoke_root)?;
smoke_root.canonicalize()
})?;
for path in paths {
if !path.exists() {
continue;
}
let resolved = path.canonicalize()?;
if resolved == smoke_root || !resolved.starts_with(&smoke_root) {
return Err(anyhow!(
"refusing to delete non-smoke path: {}",
path.display()
));
}
if path.is_dir() {
fs::remove_dir_all(path)?;
} else {
fs::remove_file(path)?;
}
}
Ok(())
}
fn write_json(path: &Path, value: &Value) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(path, format!("{}\n", serde_json::to_string_pretty(value)?))?;
Ok(())
}
fn require(condition: bool, message: &str) -> Result<()> {
if condition {
Ok(())
} else {
Err(anyhow!("{message}"))
}
}
fn take_path(args: &mut impl Iterator<Item = String>, name: &str) -> Result<PathBuf> {
args.next()
.map(PathBuf::from)
.ok_or_else(|| anyhow!("{name} requires a value"))
}

View file

@ -1508,7 +1508,7 @@ Current tests cover the landed flows at useful seams:
- live app-server smoke for the first Phase 6 graph traversal boundary through native `epiphany-phase6-graph-query-smoke`.
- live app-server smoke for the first Phase 6 planning boundary through native `epiphany-phase6-planning-smoke`.
- live app-server smoke for the first Phase 6 pressure boundary through native `epiphany-phase6-pressure-smoke`.
- live app-server smoke for the first Phase 6 reorientation policy boundary in `tools/epiphany_phase6_reorient_smoke.py`.
- live app-server smoke for the first Phase 6 reorientation policy boundary through native `epiphany-phase6-reorient-smoke`.
- live app-server smoke for the first bounded reorient-launch runtime consumer plus CRRC coordinator recommendations in `tools/epiphany_phase6_reorient_launch_smoke.py`.
- live app-server smoke for the first MVP operator status view through native `epiphany-mvp-status-smoke`.
- focused app-server mapper coverage for `thread/epiphany/roles`, plus MVP status smoke coverage that consumes the roles surface.
@ -1532,7 +1532,7 @@ cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-phase6-conte
cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-phase6-graph-query-smoke
cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-phase6-planning-smoke
cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-phase6-pressure-smoke
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_reorient_smoke.py'
cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-phase6-reorient-smoke
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_reorient_launch_smoke.py'
cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-mvp-status-smoke
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_job_control_smoke.py'

View file

@ -91,7 +91,7 @@ The landed machine now has:
- first Unity editor/runtime bridge through native `epiphany-unity-bridge`, native `epiphany-unity-bridge-smoke`, 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
- live Phase 6 reorientation app-server smoke coverage in `tools/epiphany_phase6_reorient_smoke.py`
- live Phase 6 reorientation app-server smoke coverage through native `epiphany-phase6-reorient-smoke`
- live Phase 6 reorient-launch app-server smoke coverage in `tools/epiphany_phase6_reorient_launch_smoke.py`
- live Phase 6 MVP status smoke coverage through native `epiphany-mvp-status-smoke`
- live Phase 6 job-control app-server smoke coverage in `tools/epiphany_phase6_job_control_smoke.py`

View file

@ -152,7 +152,7 @@ The current spine, blessed but not yet finished:
- live planning app-server smoke through native `epiphany-phase6-planning-smoke`
- live GUI planning adoption smoke through `tools/epiphany_gui_planning_smoke.py`
- live pressure app-server smoke through native `epiphany-phase6-pressure-smoke`
- live reorientation app-server smoke through `tools/epiphany_phase6_reorient_smoke.py`
- live reorientation app-server smoke through native `epiphany-phase6-reorient-smoke`
- live reorient-guided worker launch smoke through `tools/epiphany_phase6_reorient_launch_smoke.py`
- live MVP operator status smoke through native `epiphany-mvp-status-smoke`
- live MVP coordinator smoke through native `epiphany-mvp-coordinator-smoke`
@ -252,7 +252,7 @@ cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-phase6-press
For reorientation policy behavior changes, run:
```powershell
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_reorient_smoke.py'
cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-phase6-reorient-smoke
```
For the bounded reorient-guided worker launch surface, run:

Binary file not shown.

View file

@ -208,7 +208,7 @@ landed_baseline:
- Use `cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-phase6-planning-smoke` before modifying planning state or the read-only planning projection.
- Use `tools/epiphany_gui_planning_smoke.py` before modifying GUI Objective Draft adoption behavior.
- Use `cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-phase6-pressure-smoke` before modifying context-pressure reflection behavior.
- Use `tools/epiphany_phase6_reorient_smoke.py` before modifying bounded CRRC reorientation policy behavior.
- Use `cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-phase6-reorient-smoke` before modifying bounded CRRC reorientation policy behavior.
- Use `tools/epiphany_phase6_reorient_launch_smoke.py` before modifying the bounded reorient-guided worker launch surface.
- Use `tools/epiphany_phase6_reorient_launch_smoke.py` before modifying the bounded reorient-worker result read-back surface.
- Use `tools/epiphany_phase6_reorient_launch_smoke.py` before modifying the read-only CRRC coordinator recommendation surface.

View file

@ -1,321 +0,0 @@
from __future__ import annotations
import argparse
import json
import shutil
import time
from pathlib import Path
from typing import Any
from epiphany_phase5_smoke import AppServerClient
from epiphany_phase5_smoke import DEFAULT_APP_SERVER
from epiphany_phase5_smoke import ROOT
from epiphany_phase5_smoke import require
from epiphany_phase5_smoke import reset_smoke_paths
DEFAULT_CODEX_HOME = ROOT / ".epiphany-smoke" / "phase6-reorient-codex-home"
DEFAULT_WORKSPACE = ROOT / ".epiphany-smoke" / "phase6-reorient-workspace"
DEFAULT_RESULT = ROOT / ".epiphany-smoke" / "phase6-reorient-smoke-result.json"
DEFAULT_TRANSCRIPT = ROOT / ".epiphany-smoke" / "phase6-reorient-smoke-transcript.jsonl"
DEFAULT_STDERR = ROOT / ".epiphany-smoke" / "phase6-reorient-smoke-server.stderr.log"
WATCHED_RELATIVE_PATH = Path("src") / "reorient_target.rs"
def prepare_workspace(workspace: Path) -> Path:
if workspace.exists():
shutil.rmtree(workspace)
watched_file = workspace / WATCHED_RELATIVE_PATH
watched_file.parent.mkdir(parents=True, exist_ok=True)
watched_file.write_text(
"pub fn reorient_target() -> &'static str {\n \"before\"\n}\n",
encoding="utf-8",
)
return watched_file
def reorient_patch() -> dict[str, Any]:
return {
"objective": "Decide whether a durable checkpoint still deserves to be resumed after rehydrate.",
"activeSubgoalId": "phase6-reorient-smoke",
"subgoals": [
{
"id": "phase6-reorient-smoke",
"title": "Live-smoke CRRC reorientation policy",
"status": "active",
"summary": "Resume when the checkpoint is still aligned; regather when the touched file proves it isn't.",
}
],
"graphs": {
"architecture": {
"nodes": [
{
"id": "reorient-target",
"title": "Reorient target",
"purpose": "Map the file the watcher will touch so reorientation can notice drift.",
"code_refs": [
{
"path": WATCHED_RELATIVE_PATH.as_posix(),
"start_line": 1,
"end_line": 3,
"symbol": "reorient_target",
}
],
}
]
},
"dataflow": {"nodes": []},
"links": [],
},
"graphFrontier": {
"active_node_ids": ["reorient-target"],
"dirty_paths": [],
},
"graphCheckpoint": {
"checkpoint_id": "ck-reorient-1",
"graph_revision": 1,
"summary": "Reorientation smoke graph checkpoint",
"frontier_node_ids": ["reorient-target"],
},
"investigationCheckpoint": {
"checkpoint_id": "ix-reorient-1",
"kind": "source_gathering",
"disposition": "resume_ready",
"focus": "Verify the touched file before broad edits.",
"summary": "This checkpoint should remain resumable until the watched source moves.",
"next_action": "Resume the bounded slice if the watched source still matches the checkpoint.",
"captured_at_turn_id": "turn-phase6-reorient",
"code_refs": [
{
"path": WATCHED_RELATIVE_PATH.as_posix(),
"start_line": 1,
"end_line": 3,
"symbol": "reorient_target",
}
],
},
"churn": {
"understanding_status": "ready",
"diff_pressure": "low",
"graph_freshness": "fresh",
"unexplained_writes": 0,
},
}
def assert_missing_reorient(response: dict[str, Any]) -> None:
require(response["source"] == "live", "missing reorient response should report live source")
require(response["stateStatus"] == "missing", "missing reorient response should report missing state")
decision = response["decision"]
require(decision["action"] == "regather", "missing reorient response should regather")
require(
decision["checkpointStatus"] == "missing",
"missing reorient response should report missing checkpoint",
)
require(
decision["reasons"] == ["missingState", "missingCheckpoint"],
"missing reorient response should explain missing state and checkpoint",
)
def assert_ready_reorient(response: dict[str, Any]) -> None:
require(response["source"] == "live", "ready reorient response should report live source")
require(response["stateStatus"] == "ready", "ready reorient response should report ready state")
require(response["stateRevision"] == 1, "ready reorient response should preserve revision identity")
decision = response["decision"]
require(decision["action"] == "resume", "clean checkpoint should remain resumable")
require(
decision["checkpointStatus"] == "resumeReady",
"ready reorient response should report resume-ready checkpoint status",
)
require(
decision["reasons"] == ["checkpointReady"],
"ready reorient response should explain that the checkpoint is still aligned",
)
require(
decision["checkpointId"] == "ix-reorient-1",
"ready reorient response should expose the checkpoint id",
)
require(
decision["nextAction"]
== "Resume the bounded slice if the watched source still matches the checkpoint.",
"ready reorient response should preserve checkpoint next action",
)
require(
decision["watcherStatus"] == "clean",
"ready reorient response should report a clean watcher before drift",
)
def wait_for_regather_reorient(
client: AppServerClient,
thread_id: str,
*,
timeout: float = 10.0,
) -> dict[str, Any]:
deadline = time.time() + timeout
last_response: dict[str, Any] | None = None
while time.time() < deadline:
response = client.send("thread/epiphany/reorient", {"threadId": thread_id})
assert response is not None
last_response = response
if response["decision"]["action"] == "regather":
return response
time.sleep(0.2)
raise AssertionError(
f"reorientation policy did not switch to regather before timeout; last response: {last_response!r}"
)
def assert_regather_reorient(response: dict[str, Any]) -> None:
decision = response["decision"]
normalized_changed_paths = [
path.replace("\\", "/") for path in decision["checkpointChangedPaths"]
]
require(decision["action"] == "regather", "touched checkpoint path should force regather")
require(
decision["checkpointStatus"] == "resumeReady",
"regather response should preserve the underlying checkpoint disposition",
)
require(
decision["reasons"] == ["checkpointPathsChanged", "frontierChanged"],
"regather response should explain watcher path and frontier drift",
)
require(
WATCHED_RELATIVE_PATH.as_posix() in normalized_changed_paths,
"regather response should report the changed checkpoint path",
)
require(
decision["activeFrontierNodeIds"] == ["reorient-target"],
"regather response should report touched frontier nodes",
)
require(
decision["note"].startswith("Re-gather before editing:"),
"regather response should explain why the checkpoint is no longer safe to resume",
)
def run_smoke(args: argparse.Namespace) -> dict[str, Any]:
app_server = args.app_server.resolve()
if not app_server.exists():
raise FileNotFoundError(f"codex app-server binary not found: {app_server}")
codex_home = args.codex_home.resolve()
workspace = args.workspace.resolve()
watched_file = prepare_workspace(workspace)
result_path = args.result.resolve()
transcript_path = args.transcript.resolve()
stderr_path = args.stderr.resolve()
reset_smoke_paths(codex_home, result_path, transcript_path, stderr_path)
with AppServerClient(app_server, codex_home, transcript_path, stderr_path) as client:
client.send(
"initialize",
{
"clientInfo": {
"name": "epiphany-phase6-reorient-smoke",
"title": "Epiphany Phase 6 Reorient Smoke",
"version": "0.1.0",
},
"capabilities": {"experimentalApi": True},
},
)
client.send("initialized", expect_response=False)
started = client.send("thread/start", {"cwd": str(workspace), "ephemeral": True})
assert started is not None
thread_id = started["thread"]["id"]
missing_notification_start = len(client.notifications)
missing_response = client.send("thread/epiphany/reorient", {"threadId": thread_id})
assert missing_response is not None
assert_missing_reorient(missing_response)
client.require_no_notification(
"thread/epiphany/stateUpdated",
start_index=missing_notification_start,
)
update_notification_start = len(client.notifications)
update = client.send(
"thread/epiphany/update",
{"threadId": thread_id, "expectedRevision": 0, "patch": reorient_patch()},
)
assert update is not None
require(update["revision"] == 1, "reorient smoke patch should advance revision to 1")
client.wait_for_notification(
"thread/epiphany/stateUpdated",
start_index=update_notification_start,
)
ready_notification_start = len(client.notifications)
ready_response = client.send("thread/epiphany/reorient", {"threadId": thread_id})
assert ready_response is not None
assert_ready_reorient(ready_response)
client.require_no_notification(
"thread/epiphany/stateUpdated",
start_index=ready_notification_start,
)
watched_file.write_text(
"pub fn reorient_target() -> &'static str {\n \"after\"\n}\n",
encoding="utf-8",
)
regather_notification_start = len(client.notifications)
regather_response = wait_for_regather_reorient(client, thread_id)
assert_regather_reorient(regather_response)
client.require_no_notification(
"thread/epiphany/stateUpdated",
start_index=regather_notification_start,
)
final_read = client.send("thread/read", {"threadId": thread_id, "includeTurns": False})
assert final_read is not None
require(
final_read["thread"]["epiphanyState"]["revision"] == 1,
"reorient reflection should not mutate durable state",
)
result = {
"threadId": thread_id,
"codexHome": str(codex_home),
"workspace": str(workspace),
"missingAction": missing_response["decision"]["action"],
"readyAction": ready_response["decision"]["action"],
"regatherAction": regather_response["decision"]["action"],
"regatherReasons": regather_response["decision"]["reasons"],
"checkpointChangedPaths": regather_response["decision"]["checkpointChangedPaths"],
"activeFrontierNodeIds": regather_response["decision"]["activeFrontierNodeIds"],
"stateUpdatedNotificationCount": client.count_notifications(
"thread/epiphany/stateUpdated",
start_index=regather_notification_start,
),
"finalReadRevision": final_read["thread"]["epiphanyState"]["revision"],
}
result_path.parent.mkdir(parents=True, exist_ok=True)
result_path.write_text(
json.dumps(result, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
return result
def main() -> int:
parser = argparse.ArgumentParser(
description="Live-smoke the Phase 6 Epiphany reorientation policy surface."
)
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("--workspace", type=Path, default=DEFAULT_WORKSPACE)
parser.add_argument("--result", type=Path, default=DEFAULT_RESULT)
parser.add_argument("--transcript", type=Path, default=DEFAULT_TRANSCRIPT)
parser.add_argument("--stderr", type=Path, default=DEFAULT_STDERR)
args = parser.parse_args()
result = run_smoke(args)
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())