diff --git a/notes/epiphany-current-algorithmic-map.md b/notes/epiphany-current-algorithmic-map.md index 70b3477f..3a84313f 100644 --- a/notes/epiphany-current-algorithmic-map.md +++ b/notes/epiphany-current-algorithmic-map.md @@ -104,7 +104,7 @@ Source audit on 2026-04-25 re-read each flow below against the cited code instea The current landed machine is still coherent. The core shape is not ornamental: one protocol state object flows through session state, prompt rendering, rollout persistence, thread hydration, explicit retrieval/indexing, read-only distillation/proposal, verifier-backed promotion, and the single durable update writer. The useful simplification pressure is already present in the boundaries: retrieval reads, indexing writes only the semantic catalog, distillation/proposal draft patches, promotion gates, and update persists. -Nothing in the current typed spine obviously deserves deletion right now. The parts that would turn this into Jenga are still correctly listed as non-flows: automatic tool-output ingestion, watcher invalidation, GUI reflection, specialist scheduling, and an automatic Compact-Rehydrate-Reorient-Continue coordinator. The next Perfect Machine move is therefore not adding another writer or a shiny surface. After match-kind-aware map-delta judgment, source-output-aware distillation, and first risky-delta promotion policy, the next pressure belongs in live-smoking the richer Phase 5 path and then adding only the smallest missing policy/proposal rule the smoke exposes. +Nothing in the current typed spine obviously deserves deletion right now. The parts that would turn this into Jenga are still correctly listed as non-flows: automatic tool-output ingestion, watcher invalidation, GUI reflection, specialist scheduling, and an automatic Compact-Rehydrate-Reorient-Continue coordinator. The next Perfect Machine move is therefore not adding another writer or a shiny surface. After match-kind-aware map-delta judgment, source-output-aware distillation, first risky-delta promotion policy, and a richer app-server smoke harness, the next pressure belongs in running that harness as a guardrail and adding only the smallest source-grounded policy/proposal rule real use exposes. ## Natural Language Spine @@ -879,7 +879,7 @@ stored Epiphany state -> explicit update patches can revise durable map/evidence/churn state ``` -The current remaining missing organ is not the red pen and not the first chewing motion. Those exist. Proposal now has the first bits of map memory, focus, and selection hygiene: it can auto-select a bounded evidence-backed observation cluster when ids are omitted, requires accepting recent evidence behind selected observations, prioritizes stronger selected observations for proposal wording, reuses existing architecture nodes by concrete code-ref/path/id checks before creating new path nodes, can rescue unanchored graph nodes through strict unique semantic overlap, follows graph links and incident edges into the frontier, and reports match-kind-aware churn pressure from the actual proposal shape. Distillation now also has the first source-output-aware teeth for tool/model summaries. Promotion now notices risky deltas instead of just checking the shape of the envelope. The missing organ is live app-server smoke coverage for the richer Phase 5 chain, watcher/freshness inputs, and eventually role-scoped specialist ownership without silently auto-promoting anything. +The current remaining missing organ is not the red pen and not the first chewing motion. Those exist. Proposal now has the first bits of map memory, focus, and selection hygiene: it can auto-select a bounded evidence-backed observation cluster when ids are omitted, requires accepting recent evidence behind selected observations, prioritizes stronger selected observations for proposal wording, reuses existing architecture nodes by concrete code-ref/path/id checks before creating new path nodes, can rescue unanchored graph nodes through strict unique semantic overlap, follows graph links and incident edges into the frontier, and reports match-kind-aware churn pressure from the actual proposal shape. Distillation now also has the first source-output-aware teeth for tool/model summaries. Promotion now notices risky deltas instead of just checking the shape of the envelope. Richer app-server smoke coverage now exists in `tools/epiphany_phase5_smoke.py`; the remaining missing organs are watcher/freshness inputs and eventually role-scoped specialist ownership without silently auto-promoting anything. ```text model/tool observations @@ -909,6 +909,7 @@ Current tests cover the landed flows at useful seams: - retrieval ranking, fallback, stale manifest detection, and mocked Qdrant/Ollama indexing in `epiphany-core`. - app-server protocol serde for `thread/epiphany/retrieve`, `thread/epiphany/index`, `thread/epiphany/distill`, `thread/epiphany/propose`, `thread/epiphany/promote`, and `thread/epiphany/update`. - app-server mapping of core retrieval/index summaries into protocol responses. +- reusable app-server smoke for the richer Phase 5 chain in `tools/epiphany_phase5_smoke.py`. Useful commands: @@ -917,4 +918,5 @@ cargo test --manifest-path .\epiphany-core\Cargo.toml $env:CARGO_TARGET_DIR='C:\Users\Meta\.cargo-target-codex'; cargo test -p codex-core --lib epiphany $env:CARGO_TARGET_DIR='C:\Users\Meta\.cargo-target-codex'; cargo test -p codex-app-server-protocol --lib thread_epiphany_ $env:CARGO_TARGET_DIR='C:\Users\Meta\.cargo-target-codex'; cargo test -p codex-app-server --lib map_epiphany_ +& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase5_smoke.py' ``` diff --git a/notes/epiphany-fork-implementation-plan.md b/notes/epiphany-fork-implementation-plan.md index 471e1db9..34e01ece 100644 --- a/notes/epiphany-fork-implementation-plan.md +++ b/notes/epiphany-fork-implementation-plan.md @@ -2,7 +2,7 @@ ## Status -Updated on 2026-04-25 after landing risky-delta promotion policy inside the Phase 5 promote path. +Updated on 2026-04-25 after live-smoking the richer Phase 5 app-server chain and adding a reusable smoke harness. This note tracks Epiphany as a fork of Codex with an opinionated modeling architecture. The point is not to offer Codex another collaboration preset. The point is to make the harness force the model to carry explicit structure about the codebase, the active subgoal, the evidence trail, and the machine it is modifying. @@ -24,9 +24,10 @@ This note tracks Epiphany as a fork of Codex with an opinionated modeling archit - a map-delta judgment slice that distinguishes exact-ref refinement, same-path broadening, deterministic-node reuse, semantic unanchored-node reuse, and new candidate surfaces before setting graph freshness and `diff_pressure` - a source-output distillation slice that keeps `thread/epiphany/distill` read-only while summarizing noisy tool/command/shell/model output into salient lines and defaulting evidence kind to `tool-output` or `model-output` - a risky-delta promotion policy slice that keeps `thread/epiphany/promote` as the only gate but requires medium/high/broadening/semantic/update churn deltas to carry explicit warning rationale plus strong verifier evidence kind +- a reusable richer Phase 5 app-server smoke harness at `tools/epiphany_phase5_smoke.py` that verifies source-output distillation, read-only proposal, risky-delta rejection, and accepted promotion through the loaded-thread seam - current phase: Phase 5 semantic distillation/promotion/proposal machinery -The next job is no longer to prove the retriever exists, sketch the persistent follow-up in prose, invent the first red-pen path, build the first observation proposal surface, make promotion notice broken map/churn replacements, ship the first read-only map/churn proposal surface, teach proposal to avoid duplicating already-mapped code surfaces, make proposal frontier focus follow existing graph links, make selected observations evidence-backed with non-haunted churn pressure, rescue unanchored graph nodes with strict semantic matching, let proposal choose a bounded ready observation set when ids are omitted, make proposal pressure understand match kinds, make the distiller summarize noisy source output, or make promotion notice risky deltas. Those parts are landed. The next job is to keep the landing zone honest: live-smoke the richer Phase 5 chain through app-server before adding more policy, and do not casually turn `thread/epiphany/retrieve`, `thread/epiphany/distill`, or `thread/epiphany/propose` into durable Epiphany-state writers. +The next job is no longer to prove the retriever exists, sketch the persistent follow-up in prose, invent the first red-pen path, build the first observation proposal surface, make promotion notice broken map/churn replacements, ship the first read-only map/churn proposal surface, teach proposal to avoid duplicating already-mapped code surfaces, make proposal frontier focus follow existing graph links, make selected observations evidence-backed with non-haunted churn pressure, rescue unanchored graph nodes with strict semantic matching, let proposal choose a bounded ready observation set when ids are omitted, make proposal pressure understand match kinds, make the distiller summarize noisy source output, make promotion notice risky deltas, or prove that richer chain over app-server by hand. Those parts are landed or live-smoked. The next job is to use the reusable smoke harness as a guardrail before adding the next smallest source-grounded proposal/promotion rule, and to keep `thread/epiphany/retrieve`, `thread/epiphany/distill`, and `thread/epiphany/propose` out of the durable-writer business. ## Summary @@ -276,7 +277,7 @@ Add tests for: ## Immediate Next Step -Treat the current retrieval baseline and explicit indexing follow-up as landed Phase 4. Treat the explicit update path, distillation proposal path, read-only map/churn proposal path, verifier-backed promotion gate, structural map/churn promotion validation, graph-node reuse, linked frontier focus, evidence-backed selection, map-delta churn pressure, selected-observation prioritization, strict unanchored-node semantic reuse, automatic bounded observation-set selection, match-kind-aware map-delta judgment, source-output-aware observation distillation, and risky-delta promotion policy as the active Phase 5 baseline. The next machine gap is no longer "can Epiphany retrieve code?", "can it propose one durable observation patch?", "can it draft one bounded map/churn candidate?", "can it avoid duplicating an existing graph node when an observation points at already-mapped code?", "can it focus linked graph context?", "can it reject selected observations that are not backed by accepting recent evidence?", "can it use graph language when no concrete refs exist yet?", "can it choose a proposal-ready observation set when ids are omitted?", "can it distinguish exact refinement from same-path broadening or semantic anchoring?", "can it summarize noisy tool/model output as typed evidence?", or "can promotion policy reason over richer deltas without silently writing them?" It can. The next gap is "does the richer Phase 5 chain behave through the app-server seam?" +Treat the current retrieval baseline and explicit indexing follow-up as landed Phase 4. Treat the explicit update path, distillation proposal path, read-only map/churn proposal path, verifier-backed promotion gate, structural map/churn promotion validation, graph-node reuse, linked frontier focus, evidence-backed selection, map-delta churn pressure, selected-observation prioritization, strict unanchored-node semantic reuse, automatic bounded observation-set selection, match-kind-aware map-delta judgment, source-output-aware observation distillation, risky-delta promotion policy, and reusable richer app-server smoke harness as the active Phase 5 baseline. The next machine gap is no longer "can Epiphany retrieve code?", "can it propose one durable observation patch?", "can it draft one bounded map/churn candidate?", "can it avoid duplicating an existing graph node when an observation points at already-mapped code?", "can it focus linked graph context?", "can it reject selected observations that are not backed by accepting recent evidence?", "can it use graph language when no concrete refs exist yet?", "can it choose a proposal-ready observation set when ids are omitted?", "can it distinguish exact refinement from same-path broadening or semantic anchoring?", "can it summarize noisy tool/model output as typed evidence?", "can promotion policy reason over richer deltas without silently writing them?", or "does the richer Phase 5 chain behave through the app-server seam?" It can. 1. treat the current verified-and-landed query-time hybrid retriever as the Phase 4 slice 1 baseline 2. treat the verified and live-smoked `thread/epiphany/index` slice as the bounded persistent-semantic follow-up @@ -284,8 +285,8 @@ Treat the current retrieval baseline and explicit indexing follow-up as landed P 4. do not add durable retrieval-summary writes from `thread/epiphany/retrieve` without a clean out-of-band rollout/update semantic 5. continue Phase 5 by hardening the layer above the landed distill/propose/promote/update surfaces: - better proposal heuristics beyond the current exact-code-ref, same-path, deterministic-id, graph-link, accepting-evidence, semantic-unanchored-node, selected-priority, auto-selection, and match-kind-aware map-delta-pressure checks - - live-smoke source-output distillation, proposal, risky-delta rejection, and accepted promotion through the app-server seam - - only after that, add the next smallest promotion/proposal rule the smoke exposes + - run `tools/epiphany_phase5_smoke.py` as a preflight before changing proposal or promotion policy + - add the next smallest promotion/proposal rule only after a source-grounded gap appears in real use or smoke output - verifier-backed acceptance/rejection evidence - no hidden retrieval writes - not GUI work diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 12b334c1..03cd1251 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -361,8 +361,9 @@ If a future session wakes up from compaction and starts bluffing, this is the pa - proposal churn now distinguishes exact-ref refinements from same-path broadening, deterministic-node reuse, semantic unanchored-node reuse, and new candidate surfaces before setting graph freshness and diff pressure - distillation now summarizes noisy tool/command/shell/model output into salient typed evidence rather than preserving raw output sludge as the summary - promotion now rejects medium/high/broadening/semantic/update churn deltas that lack a warning or only carry weak generic verifier evidence + - richer Phase 5 app-server smoke is now reproducible through `tools/epiphany_phase5_smoke.py`; it verifies `shell-tool` distillation to `tool-output`, read-only proposal, risky-churn rejection without warning, risky-churn rejection with weak verifier kind, and accepted promotion only with warning plus strong verifier evidence - the Epiphany algorithmic map has been semantically audited flow-by-flow against the cited code; stale line anchors were fixed, and the current typed spine still describes a coherent machine - - the next bounded follow-up is live-smoke coverage for the richer Phase 5 app-server chain, not proving the retriever exists again + - the next bounded follow-up should use the richer smoke harness as a guardrail before the next source-grounded proposal/promotion hardening slice - There is still no live `thread/epiphany/*` notification stream. - The next phase is **not** GUI work. - The current pushed baseline also extracted most Epiphany-owned implementation into `epiphany-core`: @@ -457,9 +458,9 @@ Without that, you get to learn about `symlink_dir failed: ... A required privile ## Recommended Next Implementation -Do not restart verification from superstition. Phase 4 retrieval/indexing/core-extraction is already verified, committed, and pushed. Phase 5 distill/propose/promote/update, the first structural promotion safety layer, graph-node reuse, linked frontier focus, evidence-backed proposal selection, selected-observation prioritization, strict unanchored-node semantic reuse, automatic bounded observation selection, match-kind-aware map-delta churn pressure, source-output-aware distillation, and risky-delta promotion policy are also landed. +Do not restart verification from superstition. Phase 4 retrieval/indexing/core-extraction is already verified, committed, and pushed. Phase 5 distill/propose/promote/update, the first structural promotion safety layer, graph-node reuse, linked frontier focus, evidence-backed proposal selection, selected-observation prioritization, strict unanchored-node semantic reuse, automatic bounded observation selection, match-kind-aware map-delta churn pressure, source-output-aware distillation, risky-delta promotion policy, and the reusable richer app-server smoke harness are also landed. -The durable state seam exists, the turn loop reads it, clients can load typed Epiphany state directly, the first retrieval slice is real, the explicit persistent-semantic indexing path is landed, the distill/propose/promote/update path is live-smoked, promotion now has a first structural map/churn safety layer and risky-delta policy, distillation summarizes noisy source output into typed evidence, and proposal now reuses existing architecture nodes before creating new path nodes, expands frontier focus through linked graph context, rejects selected observations without accepting recent evidence, prioritizes stronger selected observations for proposal wording, can reuse unanchored architecture nodes through strict semantic overlap, can auto-select a bounded evidence-backed observation cluster when ids are omitted, and reports match-kind-aware churn pressure from the proposal shape. The next clean move is live-smoke coverage for the richer Phase 5 chain, not pretending the already-shipped slices still need ceremony. +The durable state seam exists, the turn loop reads it, clients can load typed Epiphany state directly, the first retrieval slice is real, the explicit persistent-semantic indexing path is landed, the distill/propose/promote/update path is live-smoked, promotion now has a first structural map/churn safety layer and risky-delta policy, distillation summarizes noisy source output into typed evidence, and proposal now reuses existing architecture nodes before creating new path nodes, expands frontier focus through linked graph context, rejects selected observations without accepting recent evidence, prioritizes stronger selected observations for proposal wording, can reuse unanchored architecture nodes through strict semantic overlap, can auto-select a bounded evidence-backed observation cluster when ids are omitted, and reports match-kind-aware churn pressure from the proposal shape. The richer Phase 5 chain has now been smoke-tested through app-server and captured as a reusable tool instead of a one-off ritual. Current Phase 5 implementation move: @@ -467,7 +468,8 @@ Current Phase 5 implementation move: 2. keep the current env/default Qdrant/Ollama config surface for now 3. keep `thread/epiphany/retrieve`, `thread/epiphany/distill`, and `thread/epiphany/propose` read-only 4. continue Phase 5 by hardening the next layer above the distill/propose/promote/update surfaces: - - live-smoke source-output distillation, proposal, risky-delta rejection, and accepted promotion through app-server + - run `tools/epiphany_phase5_smoke.py` before changing proposal or promotion policy + - add the next smallest promotion/proposal rule only after a source-grounded gap appears in real use or smoke output - verifier-backed acceptance/rejection evidence - no GUI, watcher, or specialist-agent machinery yet @@ -485,6 +487,7 @@ The next slice should stay small and mean: - harden the explicit state-update path with a live app-server smoke when useful - harden distillation/proposal only through explicit promotion checks, not hidden writes +- use `tools/epiphany_phase5_smoke.py` as the current app-server seam guardrail - keep GUI, watcher mutation, and specialist-agent logic dark ## Verification Commands @@ -495,6 +498,7 @@ State sanity from the repo root: & 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_state.py' status Get-Content -Tail 5 '.\state\evidence.jsonl' git status --short +& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase5_smoke.py' ``` Targeted `codex-core` test pattern on this Windows machine: @@ -507,4 +511,4 @@ cmd /c "\"C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxi The repo is in a good state. -Phase 1, Phase 2, the minimal Phase 3 typed read surface, Phase 4 retrieval/indexing/core-extraction, and the current Phase 5 distill/propose/promote/update baseline are landed and verified. `vendor/codex` is first-class in the parent repo now, with the heavier Epiphany organs living in `epiphany-core`. Read-only distillation now summarizes noisy tool/model output into typed evidence, read-only proposal can auto-select a bounded evidence-backed observation cluster and report match-kind-aware map-delta pressure, promotion rejects risky deltas without warning plus strong verifier evidence, and the current Epiphany algorithmic map has been source-audited against the code flow it describes. The next clean move is live-smoke coverage for the richer Phase 5 chain, not GUI paint, watcher magic, or re-proving retrieval. Pre-compaction persistence is now an explicit design rule, not a lucky habit. +Phase 1, Phase 2, the minimal Phase 3 typed read surface, Phase 4 retrieval/indexing/core-extraction, and the current Phase 5 distill/propose/promote/update baseline are landed and verified. `vendor/codex` is first-class in the parent repo now, with the heavier Epiphany organs living in `epiphany-core`. Read-only distillation now summarizes noisy tool/model output into typed evidence, read-only proposal can auto-select a bounded evidence-backed observation cluster and report match-kind-aware map-delta pressure, promotion rejects risky deltas without warning plus strong verifier evidence, and the current Epiphany algorithmic map has been source-audited against the code flow it describes. The richer Phase 5 chain is now live-smoked through app-server by `tools/epiphany_phase5_smoke.py`. The next clean move is the next source-grounded proposal/promotion hardening slice, with that smoke harness as a guardrail; not GUI paint, watcher magic, or re-proving retrieval. Pre-compaction persistence is now an explicit design rule, not a lucky habit. diff --git a/state/evidence.jsonl b/state/evidence.jsonl index e3192131..23a3a102 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -123,3 +123,4 @@ {"ts": "2026-04-25T03:21:48+00:00", "type": "implementation", "status": "ok", "note": "Landed risky-delta promotion policy in epiphany-core. evaluate_promotion now treats medium/high diff_pressure and graph freshness that indicates broadening, semantic anchoring, or updates as risky churn deltas requiring patch.churn.warning plus strong verifier evidence kind, while accepted promotions still flow only through the existing durable update path."} {"ts": "2026-04-25T03:21:48+00:00", "type": "verification", "status": "ok", "note": "Verified risky-delta promotion policy with cargo fmt --manifest-path .\\epiphany-core\\Cargo.toml, targeted cargo test --manifest-path .\\epiphany-core\\Cargo.toml promotion::tests (9 tests), and full cargo test --manifest-path .\\epiphany-core\\Cargo.toml (41 tests)."} {"ts": "2026-04-25T03:24:38+00:00", "type": "verification", "status": "ok", "note": "Verified Compact-Rehydrate-Reorient-Continue readiness before compaction. state/map.yaml, epiphany_state.py status, fresh-workspace-handoff, implementation plan, and Epiphany algorithmic map agree that the current Phase 5 baseline includes source-output-aware distillation, match-kind-aware proposal churn pressure, and risky-delta promotion policy; git was clean/synced before this evidence record; next action is live-smoke the richer Phase 5 app-server chain while keeping retrieve/distill/propose read-only and avoiding GUI, watcher, or specialist-agent work."} +{"ts": "2026-04-25T03:34:04+00:00", "type": "verification", "status": "ok", "note": "Live-smoked the richer Phase 5 app-server chain with the reusable tools/epiphany_phase5_smoke.py harness after rebuilding the app-server binary. The smoke started an isolated ephemeral loaded thread, verified source-output distillation defaults shell-tool evidence to tool-output with salient output summary, confirmed thread/epiphany/propose remains read-only at revision 1, rejected risky churn without patch.churn.warning, rejected risky churn with weak verifierEvidence.kind=observation, and accepted the same risky churn only with warning plus strong verification evidence, persisting final revision 2. Result artifact: .epiphany-smoke/phase5-rich-smoke-result.json."} diff --git a/state/map.yaml b/state/map.yaml index 79dc5959..a4c6ae9c 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -49,7 +49,7 @@ active_subgoals: - Keep the Phase 5 distill/propose/promote/update work aligned with reality in code and repo memory. - Keep vendored Codex as a thin typed host seam while Epiphany-owned prompt and retrieval/indexing logic live in the repo-owned `epiphany-core` crate. - Preserve exact/path lookup and BM25 fallback while the Qdrant path stays explicit and bounded. - - Live-smoke the richer Phase 5 distill/propose/promote/update chain through app-server while keeping verifier-backed acceptance/rejection explicit and keeping retrieval/distill/propose read-only. + - Use the reusable richer Phase 5 app-server smoke harness as a guardrail before the next proposal/promotion hardening slice, keeping retrieval/distill/propose read-only and accepted writes routed through promotion/update. accepted_design: - external typed state files @@ -106,6 +106,7 @@ accepted_design: - harden proposal map-delta judgment incrementally: read-only `thread/epiphany/propose` should distinguish exact-ref refinement, same-path broadening, deterministic-node reuse, semantic unanchored-node reuse, and new candidate surfaces before setting graph freshness and `diff_pressure` - harden observation distillation incrementally: read-only `thread/epiphany/distill` should summarize noisy tool/command/shell/model output into salient bounded evidence and default evidence kind to `tool-output` or `model-output` when appropriate - harden promotion policy incrementally: `thread/epiphany/promote` should reject risky churn deltas unless they carry explicit warning rationale and strong verifier evidence kind, while still applying accepted candidates only through the durable update path + - live-smoke the richer Phase 5 app-server chain with a reusable repo-local harness: source-output distillation produced `tool-output`, proposal stayed read-only, risky churn rejected without warning, risky churn rejected with weak verifier evidence, and accepted risky churn persisted only with warning plus strong verifier evidence rejected_paths: - name: pinned_map_only @@ -122,5 +123,5 @@ rejected_paths: reason: audited candidates were either terminal-only, stale/aspirational, or incoherent wrappers; the comparison clones were removed from vendor to avoid building on untrusted drift current_status: - summary: Workspace initialized with protocol files, typed state, handoff notes, architecture maps, and a rolling implementation plan against vendored Codex. Phase 1 durable state, Phase 2 prompt integration, the minimal Phase 3 typed read surface, and Phase 4 retrieval/indexing/core-extraction are landed on `main`. Phase 5 semantic distillation/promotion/proposal is now active: the current baseline includes explicit Qdrant indexing with BM25 fallback, typed `thread/epiphany/update`, read-only `thread/epiphany/distill`, read-only `thread/epiphany/propose`, verifier-backed `thread/epiphany/promote`, structural map/churn promotion validation, source-output-aware distillation, graph-node reuse, linked frontier focus, evidence-backed proposal selection, selected-observation prioritization, strict unanchored-node semantic reuse, automatic bounded observation selection, match-kind-aware map-delta churn pressure, risky-delta promotion policy in `epiphany-core`, and thin vendored Codex host seams. The Windows app-server stack-pressure regression was narrowed to test-thread stack size; repo-local Cargo config sets `RUST_MIN_STACK=67108864`, and boxed-future request-dispatch experiments were removed after config-only verification passed. The repo also has source-available/commercial licensing plus a CLA gate for external contributions. - next_action: Live-smoke the richer Phase 5 app-server chain: source-output distillation, proposal, risky-delta rejection, and accepted promotion, while keeping retrieval/distillation/proposal read-only and avoiding watcher, GUI, or specialist-agent machinery. + summary: Workspace initialized with protocol files, typed state, handoff notes, architecture maps, and a rolling implementation plan against vendored Codex. Phase 1 durable state, Phase 2 prompt integration, the minimal Phase 3 typed read surface, and Phase 4 retrieval/indexing/core-extraction are landed on `main`. Phase 5 semantic distillation/promotion/proposal is now active: the current baseline includes explicit Qdrant indexing with BM25 fallback, typed `thread/epiphany/update`, read-only `thread/epiphany/distill`, read-only `thread/epiphany/propose`, verifier-backed `thread/epiphany/promote`, structural map/churn promotion validation, source-output-aware distillation, graph-node reuse, linked frontier focus, evidence-backed proposal selection, selected-observation prioritization, strict unanchored-node semantic reuse, automatic bounded observation selection, match-kind-aware map-delta churn pressure, risky-delta promotion policy in `epiphany-core`, a reusable richer Phase 5 app-server smoke harness, and thin vendored Codex host seams. The richer smoke now verifies source-output distillation, read-only proposal, risky-delta rejection, and accepted promotion through the app-server seam. The Windows app-server stack-pressure regression was narrowed to test-thread stack size; repo-local Cargo config sets `RUST_MIN_STACK=67108864`, and boxed-future request-dispatch experiments were removed after config-only verification passed. The repo also has source-available/commercial licensing plus a CLA gate for external contributions. + next_action: Use the reusable richer Phase 5 app-server smoke harness as a preflight, then land the next smallest source-grounded proposal/promotion hardening slice that real use exposes; keep retrieval/distillation/proposal read-only and avoid GUI, watcher, or specialist-agent machinery. diff --git a/tools/epiphany_phase5_smoke.py b/tools/epiphany_phase5_smoke.py new file mode 100644 index 00000000..719f1334 --- /dev/null +++ b/tools/epiphany_phase5_smoke.py @@ -0,0 +1,414 @@ +from __future__ import annotations + +import argparse +import copy +import json +import os +import queue +import shutil +import subprocess +import threading +import time +from pathlib import Path +from typing import Any + + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_APP_SERVER = Path(r"C:\Users\Meta\.cargo-target-codex\debug\codex-app-server.exe") +DEFAULT_CODEX_HOME = ROOT / ".epiphany-smoke" / "phase5-rich-codex-home" +DEFAULT_RESULT = ROOT / ".epiphany-smoke" / "phase5-rich-smoke-result.json" +DEFAULT_TRANSCRIPT = ROOT / ".epiphany-smoke" / "phase5-rich-smoke-transcript.jsonl" +DEFAULT_STDERR = ROOT / ".epiphany-smoke" / "phase5-rich-smoke-server.stderr.log" + + +class AppServerClient: + def __init__( + self, + app_server: Path, + codex_home: Path, + transcript_path: Path, + stderr_path: Path, + ) -> None: + self.app_server = app_server + self.codex_home = codex_home + self.transcript_path = transcript_path + self.stderr_path = stderr_path + self.messages: queue.Queue[dict[str, Any]] = queue.Queue() + self.next_id = 1 + self.proc: subprocess.Popen[str] | None = None + self.transcript = None + self.stderr_file = None + + def __enter__(self) -> "AppServerClient": + self.transcript_path.parent.mkdir(parents=True, exist_ok=True) + self.transcript = self.transcript_path.open("w", encoding="utf-8") + self.stderr_file = self.stderr_path.open("w", encoding="utf-8") + + env = os.environ.copy() + env["CODEX_HOME"] = str(self.codex_home) + env.setdefault("CARGO_TARGET_DIR", r"C:\Users\Meta\.cargo-target-codex") + + self.proc = subprocess.Popen( + [str(self.app_server)], + cwd=str(ROOT / "vendor" / "codex" / "codex-rs"), + env=env, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + encoding="utf-8", + errors="replace", + bufsize=1, + ) + threading.Thread(target=self._read_stdout, daemon=True).start() + threading.Thread(target=self._read_stderr, daemon=True).start() + return self + + def __exit__(self, *_: object) -> None: + if self.proc is not None: + try: + if self.proc.stdin is not None: + self.proc.stdin.close() + except OSError: + pass + if self.proc.poll() is None: + self.proc.terminate() + try: + self.proc.wait(timeout=5) + except subprocess.TimeoutExpired: + self.proc.kill() + if self.transcript is not None: + self.transcript.close() + if self.stderr_file is not None: + self.stderr_file.close() + + def send( + self, + method: str, + params: dict[str, Any] | None = None, + *, + expect_response: bool = True, + ) -> dict[str, Any] | None: + msg: dict[str, Any] = {"method": method} + request_id = None + if expect_response: + request_id = self.next_id + self.next_id += 1 + msg["id"] = request_id + if params is not None: + msg["params"] = params + + self._record("sent", msg) + assert self.proc is not None and self.proc.stdin is not None + self.proc.stdin.write(json.dumps(msg, separators=(",", ":")) + "\n") + self.proc.stdin.flush() + + if request_id is None: + return None + return self._wait_for(request_id) + + def _wait_for(self, request_id: int, timeout: float = 45.0) -> dict[str, Any]: + assert self.proc is not None + deadline = time.time() + timeout + while time.time() < deadline: + if self.proc.poll() is not None: + raise RuntimeError( + f"app-server exited with {self.proc.returncode} before response {request_id}" + ) + try: + msg = self.messages.get(timeout=0.5) + except queue.Empty: + continue + if msg.get("id") != request_id: + continue + if "error" in msg: + raise RuntimeError(f"request {request_id} failed: {msg['error']}") + result = msg.get("result") + if not isinstance(result, dict): + raise RuntimeError(f"request {request_id} returned non-object result: {result!r}") + return result + raise TimeoutError(f"timed out waiting for response {request_id}") + + def _read_stdout(self) -> None: + assert self.proc is not None and self.proc.stdout is not None + for line in self.proc.stdout: + line = line.strip() + if not line: + continue + try: + msg = json.loads(line) + except json.JSONDecodeError as exc: + msg = {"_decode_error": str(exc), "raw": line} + self._record("received", msg) + self.messages.put(msg) + + def _read_stderr(self) -> None: + assert self.proc is not None and self.proc.stderr is not None + assert self.stderr_file is not None + for line in self.proc.stderr: + self.stderr_file.write(line) + self.stderr_file.flush() + + def _record(self, kind: str, payload: dict[str, Any]) -> None: + assert self.transcript is not None + self.transcript.write(json.dumps({kind: payload}, ensure_ascii=False) + "\n") + self.transcript.flush() + + +def require(condition: bool, message: str) -> None: + if not condition: + raise AssertionError(message) + + +def reset_smoke_paths(codex_home: Path, result_path: Path, transcript_path: Path, stderr_path: Path) -> None: + smoke_root = ROOT / ".epiphany-smoke" + resolved_home = codex_home.resolve() + resolved_smoke = smoke_root.resolve() + if resolved_home == resolved_smoke or resolved_smoke not in resolved_home.parents: + raise ValueError(f"refusing to delete non-smoke CODEX_HOME: {codex_home}") + if codex_home.exists(): + shutil.rmtree(codex_home) + codex_home.mkdir(parents=True, exist_ok=True) + for path in (result_path, transcript_path, stderr_path): + if path.exists(): + path.unlink() + + +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() + 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-phase5-rich-smoke", + "title": "Epiphany Phase 5 Rich Smoke", + "version": "0.1.0", + }, + "capabilities": {"experimentalApi": True}, + }, + ) + client.send("initialized", expect_response=False) + started = client.send( + "thread/start", + {"cwd": str(ROOT / "epiphany-core"), "ephemeral": True}, + ) + assert started is not None + thread_id = started["thread"]["id"] + + noisy_tool_text = "\n".join( + [ + r"Compiling epiphany-core v0.1.0 (E:\Projects\EpiphanyAgent\epiphany-core)", + "warning: unrelated smoke warning that should not dominate the summary", + "running 3 tests", + "test distillation::tests::source_output_summary ... ok", + "test proposal::tests::match_kind_pressure ... ok", + "test result: ok. 3 passed; 0 failed; 0 ignored; finished in 0.02s", + "Finished test profile [unoptimized + debuginfo] target(s) in 0.31s", + ] + ) + distill = client.send( + "thread/epiphany/distill", + { + "threadId": thread_id, + "sourceKind": "shell-tool", + "status": "ok", + "subject": "phase5 richer app-server smoke", + "text": noisy_tool_text, + "codeRefs": [ + { + "path": "src/distillation.rs", + "start_line": 28, + "end_line": 160, + "symbol": "distill_observation", + } + ], + }, + ) + assert distill is not None + distill_patch = distill["patch"] + evidence = distill_patch["evidence"][0] + observation = distill_patch["observations"][0] + summary = evidence["summary"].lower() + require(distill["expectedRevision"] == 0, "distill expected revision should start at 0") + require(evidence["kind"] == "tool-output", "distill evidence kind should be tool-output") + require( + "test result: ok" in summary or "finished test profile" in summary, + "distill summary should preserve salient tool output", + ) + + update = client.send( + "thread/epiphany/update", + {"threadId": thread_id, "expectedRevision": 0, "patch": distill_patch}, + ) + assert update is not None + require(update["epiphanyState"]["revision"] == 1, "update should advance revision to 1") + + propose = client.send("thread/epiphany/propose", {"threadId": thread_id}) + assert propose is not None + proposal_patch = propose["patch"] + require(propose["expectedRevision"] == 1, "proposal should see revision 1") + require(bool(proposal_patch.get("graphs")), "proposal should include graph replacements") + require(bool(proposal_patch.get("churn")), "proposal should include churn replacement") + require(bool(proposal_patch["observations"]), "proposal should include a candidate observation") + + read_after_propose = client.send( + "thread/read", {"threadId": thread_id, "includeTurns": False} + ) + assert read_after_propose is not None + state_after_propose = read_after_propose["thread"].get("epiphanyState", {}) + require(state_after_propose.get("revision") == 1, "read-only propose should not mutate") + require("churn" not in state_after_propose, "propose should not persist churn before promotion") + + risky_missing_warning = copy.deepcopy(proposal_patch) + risky_missing_warning["churn"]["diff_pressure"] = "medium" + risky_missing_warning["churn"]["graph_freshness"] = "proposal_broadened" + risky_missing_warning["churn"].pop("warning", None) + reject_missing_warning = client.send( + "thread/epiphany/promote", + { + "threadId": thread_id, + "expectedRevision": 1, + "patch": risky_missing_warning, + "verifierEvidence": { + "id": "ev-phase5-risky-warning-verifier", + "kind": "verification", + "status": "ok", + "summary": "Intentional missing-warning rejection check", + }, + }, + ) + assert reject_missing_warning is not None + require(not reject_missing_warning["accepted"], "risky churn without warning should reject") + require( + any("patch.churn.warning" in reason for reason in reject_missing_warning["reasons"]), + "missing-warning rejection should name patch.churn.warning", + ) + + read_after_warning_reject = client.send( + "thread/read", {"threadId": thread_id, "includeTurns": False} + ) + assert read_after_warning_reject is not None + require( + read_after_warning_reject["thread"].get("epiphanyState", {}).get("revision") == 1, + "missing-warning rejection should not mutate state", + ) + + risky_weak_verifier = copy.deepcopy(proposal_patch) + risky_weak_verifier["churn"]["diff_pressure"] = "medium" + risky_weak_verifier["churn"]["graph_freshness"] = "proposal_broadened" + risky_weak_verifier["churn"]["warning"] = ( + "Same-path or medium-pressure smoke delta requires explicit strong verifier evidence." + ) + reject_weak_verifier = client.send( + "thread/epiphany/promote", + { + "threadId": thread_id, + "expectedRevision": 1, + "patch": risky_weak_verifier, + "verifierEvidence": { + "id": "ev-phase5-risky-weak-verifier", + "kind": "observation", + "status": "ok", + "summary": "Intentional weak-kind rejection check", + }, + }, + ) + assert reject_weak_verifier is not None + require(not reject_weak_verifier["accepted"], "weak verifier kind should reject") + require( + any("verifierEvidence.kind" in reason for reason in reject_weak_verifier["reasons"]), + "weak-kind rejection should name verifierEvidence.kind", + ) + + read_after_kind_reject = client.send( + "thread/read", {"threadId": thread_id, "includeTurns": False} + ) + assert read_after_kind_reject is not None + require( + read_after_kind_reject["thread"].get("epiphanyState", {}).get("revision") == 1, + "weak-kind rejection should not mutate state", + ) + + accepted = client.send( + "thread/epiphany/promote", + { + "threadId": thread_id, + "expectedRevision": 1, + "patch": risky_weak_verifier, + "verifierEvidence": { + "id": "ev-phase5-risky-strong-verifier", + "kind": "verification", + "status": "ok", + "summary": "Verifier accepted the risky churn smoke after warning rationale was present", + }, + }, + ) + assert accepted is not None + require(accepted["accepted"], "strong verifier should accept risky churn with warning") + final_state = accepted["epiphanyState"] + require(final_state["revision"] == 2, "accepted promotion should advance revision to 2") + require( + final_state["churn"]["diff_pressure"] == "medium", + "accepted state should preserve risky churn pressure", + ) + + final_read = client.send("thread/read", {"threadId": thread_id, "includeTurns": False}) + assert final_read is not None + require( + final_read["thread"]["epiphanyState"]["revision"] == 2, + "final read should return persisted revision 2", + ) + + result = { + "threadId": thread_id, + "codexHome": str(codex_home), + "distillExpectedRevision": distill["expectedRevision"], + "distillObservationId": observation["id"], + "distillEvidenceId": evidence["id"], + "distillEvidenceKind": evidence["kind"], + "distillSummary": evidence["summary"], + "proposalExpectedRevision": propose["expectedRevision"], + "proposalObservationId": proposal_patch["observations"][0]["id"], + "proposalChurn": proposal_patch["churn"], + "readOnlyRevisionAfterPropose": state_after_propose.get("revision"), + "missingWarningAccepted": reject_missing_warning["accepted"], + "missingWarningReasons": reject_missing_warning["reasons"], + "weakVerifierAccepted": reject_weak_verifier["accepted"], + "weakVerifierReasons": reject_weak_verifier["reasons"], + "accepted": accepted["accepted"], + "finalRevision": final_state["revision"], + "finalChurn": final_state["churn"], + "graphNodeCount": len(final_state.get("graphs", {}).get("architecture", {}).get("nodes", [])), + } + 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 richer Phase 5 Epiphany app-server chain." + ) + 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("--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())