diff --git a/notes/epiphany-current-algorithmic-map.md b/notes/epiphany-current-algorithmic-map.md index 2ca60f69..00ebb9fd 100644 --- a/notes/epiphany-current-algorithmic-map.md +++ b/notes/epiphany-current-algorithmic-map.md @@ -10,7 +10,7 @@ The important question for this file is not "what is Epiphany supposed to become Current Epiphany is a forked Codex harness with a typed modeling spine. -The spine exists in twenty-three live paths: +The spine exists in twenty-four live paths: - protocol state shape: `EpiphanyThreadState`, `RolloutItem::EpiphanyState`, retrieval summaries, and prompt tags live in [protocol.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/protocol/src/protocol.rs:103). - in-memory session state: `SessionState` stores an optional `EpiphanyThreadState`, and `Session` exposes thin async accessors over it in [state/session.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/state/session.rs:36) and [session/mod.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/session/mod.rs:1240). @@ -31,7 +31,8 @@ The spine exists in twenty-three live paths: - context-pressure reflection: core token telemetry carries `model_auto_compact_token_limit` beside total/last usage and `model_context_window`; app-server declares experimental read-only `thread/epiphany/pressure`, reads live token usage or the latest stored rollout `TokenCount`, and derives `unknown`/`low`/`elevated`/`high`/`critical` pressure plus a non-acting compaction-prep recommendation without mutating Epiphany state or scheduling compaction in [protocol.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/protocol/src/protocol.rs:2237), [turn_context.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/session/turn_context.rs:103), [session/mod.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/session/mod.rs:2716), [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:360), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4361), [token_usage_replay.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor/token_usage_replay.rs:75), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4306), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11028). - reorientation policy: app-server declares experimental read-only `thread/epiphany/reorient`, reuses the same live/stored thread view plus mapped pressure/freshness/watcher signals, and derives a bounded `resume`/`regather` verdict with checkpoint-path/frontier reasons without mutating, compacting, scheduling, notifying, or continuing work in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:365), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4514), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4394), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11424). - CRRC coordinator recommendation: app-server declares experimental read-only `thread/epiphany/crrc`, reuses the same pressure/freshness/watcher/reorientation mapping plus the fixed `reorient-worker` binding and result read-back helper, and returns one bounded recommendation such as continue, prepare checkpoint, launch worker, wait, review, accept, or regather manually without mutating, launching, accepting, compacting, notifying, scheduling, or silently continuing work in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:370), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4661), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4546). -- MVP operator view: `tools/epiphany_mvp_status.py` launches app-server over stdio, starts or reads one thread, gathers scene, pressure, reorient, jobs, reorient result, and CRRC responses, and renders them as text or JSON so the loop can be dogfooded without reading Rust. +- role ownership projection: app-server declares experimental read-only `thread/epiphany/roles`, reuses the same state/jobs/pressure/reorient/result/CRRC signals as the coordinator loop, and projects implementation, modeling/checkpoint, verification/review, and reorientation lanes with owner roles, status, linked jobs, authority scopes, and recommended scene actions without launching, scheduling, accepting, compacting, notifying, or mutating state in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:350), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4161), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4310). +- MVP operator view: `tools/epiphany_mvp_status.py` launches app-server over stdio, starts or reads one thread, gathers scene, pressure, reorient, jobs, roles, reorient result, and CRRC responses, and renders them as text or JSON so the loop can be dogfooded without reading Rust. - reorientation launch: app-server now declares experimental `thread/epiphany/reorientLaunch`, requires a loaded thread plus durable investigation checkpoint, consumes the live reorientation verdict, and launches one fixed `reorient-worker` binding through the existing job-control seam with explicit resume-versus-regather scope, checkpoint-derived payload, and structured-output expectations instead of pretending CRRC suddenly grew a scheduler in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:370), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4575), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4519). - reorientation result read-back: app-server now declares experimental read-only `thread/epiphany/reorientResult`, defaults to the fixed `reorient-worker` binding, resolves that binding through the current `agent_jobs` backend when available, reads the single backend item result, and projects a reviewable finding with mode, summary, next safe move, checkpoint-validity flag, inspected files, frontier ids, evidence ids, raw result, and any job/item errors without mutating state, promoting evidence, or scheduling follow-up work in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:374), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4610), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4684). - reorientation finding acceptance: app-server now declares experimental `thread/epiphany/reorientAccept`, requires a loaded thread and a completed `reorient-worker` backend result, appends accepted observation/evidence records, optionally banks the finding into scratch and the durable investigation checkpoint, persists through the normal typed update path, and emits `thread/epiphany/stateUpdated` with source `reorientAccept` without accepting pending work, auto-promoting arbitrary worker output, launching follow-up work, or silently continuing implementation in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:378), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4635), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4885). @@ -1327,6 +1328,7 @@ stored Epiphany state -> reflected as read-only context pressure from token telemetry -> reflected as a read-only resume/regather verdict from checkpoint + freshness + watcher + pressure -> reflected as a read-only CRRC recommendation over verdict + worker/result state +-> reflected as read-only MVP role ownership over implementation, modeling/checkpoint, verification/review, and reorientation lanes -> can be consumed explicitly by one bounded reorient-worker launch surface -> retrieval/indexing can inform future work -> explicit distillation can draft observation/evidence patches @@ -1338,6 +1340,8 @@ stored Epiphany state The current remaining missing organ is not the red pen, not the first chewing motion, not the first state-change bell, not the first client-shaped scene, not the first job/progress reflection, not the first live bound-job notification, not the first freshness lens, not the first watcher-backed invalidation read, not the first targeted graph/evidence context read, not the first context-pressure gauge, not the first durable investigation checkpoint packet, not the first bounded reorientation verdict, not the first thin launcher seam, not the first explicit launch/interrupt authority surface over that seam, not the first runtime consumer that can act on the reorientation verdict without becoming a hidden tyrant, not the first read-back surface for that worker's findings, not the first explicit acceptance write for completed findings, not the first read-only CRRC coordinator recommendation, and no longer the first dogfood-facing operator view over the landed loop. 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, treats graph expansion as risky even if the patch tries to mumble "low pressure" while dragging a new wall into the blueprint, token-matches strong verifier kinds so `contest` cannot sneak past as `test`, and validates investigation checkpoints against known evidence before the packet can be inked. Successful update/promote writes now emit `thread/epiphany/stateUpdated` with `source: "update"` or `source: "promote"`, event-level revision, and typed changed fields so clients no longer need to poll blindly, infer the cause from timing, or diff the whole state to know which top-level sections moved. Runtime `agent_job_progress` background events now also emit `thread/epiphany/jobsUpdated` with changed bound-job payloads so clients no longer need to poll `thread/epiphany/jobs` just to watch one real owner move, the bound jobs now reflect launcher id, authority scope, and backend kind/job id instead of treating raw runtime pointers as the architecture, `jobLaunch` / `jobInterrupt` can create or tear down those bindings without pretending reflection is authority, `reorientLaunch` can launch one fixed `reorient-worker` job from the checkpoint verdict without turning the verdict itself into a writer, `reorientResult` can read the worker's structured output back as a reviewable finding without promoting it, `reorientAccept` can explicitly bank a completed finding into typed observation/evidence plus optional scratch/checkpoint state, and `crrc` can recommend the next explicit action over that loop without doing the action itself. The scene, jobs, freshness, context, pressure, reorient, crrc, reorient-result, and MVP status surfaces give clients compact or bounded read-only views without making the GUI/client authoritative, scene/context surface the checkpoint packet back out without becoming writers, freshness surfaces watcher telemetry without becoming a hidden worker, reorient turns those signals into an explicit wakeup verdict that one explicit launch surface can consume on purpose, crrc tells the operator whether to continue, checkpoint, launch, wait, review, accept, or regather manually, and the status tool prints the answer without requiring a Rust spelunk. The remaining missing organs are a second backend if `agent_jobs` stops being enough and eventually role-scoped specialist ownership without silently auto-promoting anything. +Update: first role-scoped specialist ownership is now landed as read-only `thread/epiphany/roles`. The next missing organ is explicit role-scoped launch/read-back templates for modeling/checkpoint and verification/review, still manual and review-gated. + ```text model/tool observations -> structured observation distillation @@ -1352,6 +1356,8 @@ The typed write path exists, the first deterministic observation/evidence distil In natural language: current Epiphany can preserve a map, show a map, project a compact scene from the map, project derived job/progress slots from the map and retrieval summary, reflect a thin launcher seam over bound work, explicitly launch or interrupt that bound work through a bounded authority surface, emit live bound-job progress notifications from runtime `agent_job_progress` events, project a read-only freshness lens over retrieval and graph staleness, layer watcher-backed invalidation telemetry onto that lens for loaded threads, project a targeted graph/evidence context shard from the map, project context pressure from token telemetry, bank an in-flight investigation packet in durable typed state, reflect that packet back into prompt/scene/context, turn those signals into a read-only resume-versus-regather verdict, recommend the next explicit CRRC action over the verdict and worker/result state, print that recommendation in a dogfood operator view, explicitly launch one bounded `reorient-worker` job from that verdict, read its result back for review, explicitly accept a completed finding into typed state, retrieve evidence for the map, draft one explicit observation/evidence patch, derive a bounded graph/frontier/churn candidate from verified evidence-backed observations with code refs, choose a small coherent observation set when the caller does not provide one, favor the strongest selected observation when drafting that candidate, reject or accept a verified candidate, apply explicit map/evidence/churn edits, notify clients after successful writes, and sanity-check those edits before they hit the ledger. It still does not invalidate stale graph regions automatically, execute Epiphany-owned long-running jobs beyond the borrowed runtime `agent_jobs` backend, or coordinate broad specialist ownership. Teeth are installed; chewing is supervised; the first bell rings; the first wall board exists; the first shift board exists; the first freshness clipboard exists; the first door sensor now exists too; the first binder lens exists; the first pressure gauge exists; the first ember packet exists; the first verdict slip now exists too; the first bounded launch lever and stop switch now exist too; the first explicit wakeup-hand now exists too; the first coordinator clipboard now exists too; the first operator readout exists too. The next slices are about giving real roles real ownership, not jawbones. +The role board now exists too. Current Epiphany can project implementation, modeling/checkpoint, verification/review, and reorientation ownership through `thread/epiphany/roles`; what remains is giving modeling and verification explicit launch/read-back templates without growing an ambient dispatcher. + ## Verification Hooks Current tests cover the landed flows at useful seams: @@ -1378,6 +1384,7 @@ Current tests cover the landed flows at useful seams: - 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 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 in `tools/epiphany_mvp_status_smoke.py`. +- focused app-server mapper coverage for `thread/epiphany/roles`, plus MVP status smoke coverage that consumes the roles surface. - live app-server smoke for the first Phase 6 job-control boundary in `tools/epiphany_phase6_job_control_smoke.py`. Useful commands: diff --git a/notes/epiphany-fork-implementation-plan.md b/notes/epiphany-fork-implementation-plan.md index 7932379d..ccb1b85e 100644 --- a/notes/epiphany-fork-implementation-plan.md +++ b/notes/epiphany-fork-implementation-plan.md @@ -75,6 +75,7 @@ The landed machine now has: - read-only Phase 6 reorient-worker result read-back through `thread/epiphany/reorientResult` - explicit Phase 6 reorient-worker finding acceptance through `thread/epiphany/reorientAccept` - read-only Phase 6 CRRC coordinator recommendation through `thread/epiphany/crrc` +- read-only Phase 6 role ownership through `thread/epiphany/roles`, projecting implementation, modeling/checkpoint, verification/review, and reorientation lanes from typed state plus jobs/CRRC/result signals - first Phase 6 dogfood operator view through `tools/epiphany_mvp_status.py` - live Phase 6 reorientation app-server smoke coverage in `tools/epiphany_phase6_reorient_smoke.py` - live Phase 6 reorient-launch app-server smoke coverage in `tools/epiphany_phase6_reorient_launch_smoke.py` @@ -104,6 +105,7 @@ These boundaries are more important than the individual method names: - `thread/epiphany/pressure` is a context-pressure reflection, not an automatic compactor, scheduler, or CRRC coordinator. - `thread/epiphany/reorient` is a bounded policy verdict, not an automatic runtime coordinator, scheduler, compactor, or hidden state writer. - `thread/epiphany/crrc` is a read-only coordinator recommendation over existing signals, not a scheduler, launch button, acceptance gate, compactor, or hidden state writer. +- `thread/epiphany/roles` is a read-only role ownership projection, not a specialist scheduler, marketplace, launcher, acceptance gate, or second job backend. - The GUI may render and steer typed state, but it must not manufacture canonical understanding. - The app-server remains a host seam; Epiphany-owned machinery should live in `epiphany-core` where practical. - Qdrant is the preferred persistent semantic backend; BM25 remains the bootstrap/fallback/control path. @@ -173,14 +175,15 @@ marketplace, broad ambient scheduling, automatic promotion of every tool observation, a GUI-first workflow, or a second job backend unless the current `agent_jobs` seam blocks the product loop. -The read-back, acceptance, coordinator, and first dogfood-view blockers are now -landed as `thread/epiphany/reorientResult`, `thread/epiphany/reorientAccept`, -`thread/epiphany/crrc`, and `tools/epiphany_mvp_status.py`. The next MVP -blocker is richer role ownership: a human can now ask the harness what it -believes and what it recommends without reading Rust, and the operator view now -shows implementation, modeling/checkpoint maintenance, verification/review, and -reorientation as distinct lanes. Those lanes are still derived views rather -than a reusable specialist registry. +The read-back, acceptance, coordinator, first dogfood-view, and first +harness-native role ownership blockers are now landed as +`thread/epiphany/reorientResult`, `thread/epiphany/reorientAccept`, +`thread/epiphany/crrc`, `tools/epiphany_mvp_status.py`, and +`thread/epiphany/roles`. A human can now ask the harness what it believes, what +it recommends, and which role lane owns the next visible work without reading +Rust. The next MVP blocker is making modeling/checkpoint and verification/review +launch/read-back templates explicit over the landed job-control seam, while +keeping scheduling manual and review-gated. ## Phase 6 Direction @@ -188,7 +191,7 @@ Phase 6 should grow observable harness state outward from the typed spine. Useful candidates: -1. Add the smallest role-scoped specialist ownership layer that makes implementation, modeling/checkpoint maintenance, and verification/review visible as distinct work lanes without building a marketplace. +1. Add the smallest explicit role-scoped specialist launch/read-back templates for modeling/checkpoint maintenance and verification/review over the landed job-control seam, without building a marketplace. 2. Keep accepted worker findings review-gated; do not convert acceptance into automatic promotion of arbitrary worker output. 3. Add targeted operator-view fields only when dogfooding exposes a real gap. diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index b7c04ce7..9989bcc8 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -28,8 +28,8 @@ Do not trust this file for the exact live HEAD. Always check git. - Do not copy exact branch or HEAD from this note. Run `git status --short --branch` and `git log --oneline -5`. - Phase 1 through Phase 5 are complete enough. -- Phase 6 has read-only `thread/epiphany/scene`, `thread/epiphany/jobs`, `thread/epiphany/freshness`, `thread/epiphany/context`, `thread/epiphany/pressure`, `thread/epiphany/reorient`, `thread/epiphany/crrc`, and `thread/epiphany/reorientResult`; durable `jobBindings` now act as a thin Epiphany-owned launcher seam with launcher id, authority scope, and backend kind/job id, `thread/epiphany/jobLaunch` / `thread/epiphany/jobInterrupt` now provide explicit bounded authority over that seam, jobs can overlay it onto live runtime `agent_jobs` progress for loaded threads, and `thread/epiphany/jobsUpdated` now translates live `agent_job_progress:{json}` background events into changed launcher-bound notifications without polling or scheduling. Freshness carries watcher-backed invalidation inputs, reorient turns checkpoint plus freshness/pressure/watcher signals into a read-only resume-versus-regather verdict, `thread/epiphany/reorientLaunch` is the first explicit runtime consumer over that verdict, `thread/epiphany/reorientResult` reads the launched worker output back as a reviewable finding without mutation or promotion, `thread/epiphany/reorientAccept` explicitly banks completed findings into accepted observation/evidence plus optional scratch/checkpoint state, and `thread/epiphany/crrc` recommends the next explicit CRRC action without launching, accepting, compacting, scheduling, or mutating. -- `tools/epiphany_mvp_status.py` is the first dogfood operator view. It starts or reads a thread through app-server and prints scene, pressure, reorient, jobs, reorient result, CRRC recommendation, and MVP role lanes as text or JSON. +- Phase 6 has read-only `thread/epiphany/scene`, `thread/epiphany/jobs`, `thread/epiphany/roles`, `thread/epiphany/freshness`, `thread/epiphany/context`, `thread/epiphany/pressure`, `thread/epiphany/reorient`, `thread/epiphany/crrc`, and `thread/epiphany/reorientResult`; durable `jobBindings` now act as a thin Epiphany-owned launcher seam with launcher id, authority scope, and backend kind/job id, `thread/epiphany/jobLaunch` / `thread/epiphany/jobInterrupt` now provide explicit bounded authority over that seam, jobs can overlay it onto live runtime `agent_jobs` progress for loaded threads, and `thread/epiphany/jobsUpdated` now translates live `agent_job_progress:{json}` background events into changed launcher-bound notifications without polling or scheduling. Freshness carries watcher-backed invalidation inputs, roles project implementation/modeling/verification/reorientation ownership from existing signals without becoming a scheduler, reorient turns checkpoint plus freshness/pressure/watcher signals into a read-only resume-versus-regather verdict, `thread/epiphany/reorientLaunch` is the first explicit runtime consumer over that verdict, `thread/epiphany/reorientResult` reads the launched worker output back as a reviewable finding without mutation or promotion, `thread/epiphany/reorientAccept` explicitly banks completed findings into accepted observation/evidence plus optional scratch/checkpoint state, and `thread/epiphany/crrc` recommends the next explicit CRRC action without launching, accepting, compacting, scheduling, or mutating. +- `tools/epiphany_mvp_status.py` is the first dogfood operator view. It starts or reads a thread through app-server and prints scene, pressure, reorient, jobs, roles, reorient result, and CRRC recommendation as text or JSON. - Durable in-flight investigation checkpointing is now landed in authoritative typed state, writable through `thread/epiphany/update` or accepted `thread/epiphany/promote`, rendered into the prompt, and reflected through scene/context. - The repo is an Epiphany fork of Codex, not a Codex preset. - `vendor/codex` is tracked directly, not a submodule. @@ -72,6 +72,7 @@ The current spine: - live bound-job progress notification through `thread/epiphany/jobsUpdated` - read-only compact reflection through `thread/epiphany/scene` - read-only job/progress reflection through `thread/epiphany/jobs`, with durable launcher metadata plus live runtime `agent_jobs` overlay +- read-only role ownership reflection through `thread/epiphany/roles` - read-only retrieval/graph freshness reflection plus watcher-backed invalidation inputs through `thread/epiphany/freshness` - read-only targeted state-shard reflection through `thread/epiphany/context` - read-only context-pressure reflection through `thread/epiphany/pressure` @@ -104,6 +105,7 @@ The exact current control flow is documented in - `thread/epiphany/pressure` is read-only. - `thread/epiphany/reorient` is read-only. - `thread/epiphany/crrc` is read-only. +- `thread/epiphany/roles` is read-only. - `thread/epiphany/reorientResult` is read-only. - Durable typed state writes go through `thread/epiphany/update`, accepted `thread/epiphany/promote`, `thread/epiphany/reorientAccept`, or the bounded `thread/epiphany/jobLaunch`, `thread/epiphany/jobInterrupt`, and `thread/epiphany/reorientLaunch` authority surfaces when they mutate `jobBindings`. - `thread/epiphany/index` writes the retrieval catalog, not durable Epiphany understanding. @@ -249,13 +251,13 @@ still explicit even though read-only job ownership/progress reflection now has a real runtime seam, the thin launcher boundary is landed, and one explicit reorient-guided launch surface can consume the verdict on purpose. -When the user asks to continue, the next likely organ is the smallest -role-scoped specialist ownership layer that makes implementation, -modeling/checkpoint maintenance, and verification/review visible as distinct -lanes. Keep it explicit and narrow; do not build a broad hidden scheduler, -marketplace, or GUI-as-source-of-truth. +When the user asks to continue, the next likely organ is the smallest explicit +role-scoped specialist launch/read-back template layer for modeling/checkpoint +maintenance and verification/review over the landed job-control seam. Keep it +manual and review-gated; do not build a broad hidden scheduler, marketplace, or +GUI-as-source-of-truth. -Live `thread/epiphany/scene`, `thread/epiphany/jobs`, +Live `thread/epiphany/scene`, `thread/epiphany/jobs`, `thread/epiphany/roles`, `thread/epiphany/freshness`, `thread/epiphany/context`, `thread/epiphany/pressure`, `thread/epiphany/reorient`, `thread/epiphany/crrc`, and `tools/epiphany_mvp_status.py` smokes are now diff --git a/state/evidence.jsonl b/state/evidence.jsonl index ed8443f5..fd6de2f2 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -53,3 +53,4 @@ {"ts": "2026-04-28T11:01:36+00:00", "type": "implementation", "status": "ok", "note": "Landed read-only thread/epiphany/crrc coordinator recommendation over pressure, reorient verdict, fixed reorient-worker status/result, and available actions; focused protocol/app-server tests, schema fixtures, app-server build, cargo check, and live reorient-launch smoke verified the coordinator loop without adding scheduling or hidden mutation."} {"ts": "2026-04-28T11:08:09+00:00", "type": "implementation", "status": "ok", "note": "Landed tools/epiphany_mvp_status.py as the first dogfood operator view over scene, pressure, reorient, jobs, reorient-result, and CRRC recommendation; epiphany_mvp_status_smoke.py verifies cold-start missing-state rendering and CRRC regather recommendation through the live app-server."} {"ts": "2026-04-28T11:13:43+00:00", "type": "implementation", "status": "ok", "note": "Extended the MVP status view with derived implementation, modeling/checkpoint, verification/review, and reorientation lanes over existing app-server signals; the status smoke now asserts the four role lanes and rendered operator section."} +{"ts": "2026-04-28T13:55:36+00:00", "type": "implementation", "status": "ok", "note": "Landed read-only thread/epiphany/roles as the harness-native MVP role ownership surface. It projects implementation, modeling/checkpoint, verification/review, and reorientation lanes from typed state plus jobs/pressure/reorient/CRRC/result signals without scheduling or mutation; the MVP status tool now consumes that surface. Focused protocol/app-server tests, schema fixtures, app-server build, and live MVP status smoke passed."} diff --git a/state/map.yaml b/state/map.yaml index 3bfaa478..f8696b47 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -53,9 +53,9 @@ data_flow: active_subgoals: - Treat Phase 5 update/distill/propose/promote/read/notification as complete enough unless a concrete regression appears. - - Preserve read-only boundaries for retrieve, distill, propose, scene, jobs, freshness, context, pressure, reorient, crrc, and reorientResult. + - Preserve read-only boundaries for retrieve, distill, propose, scene, jobs, roles, freshness, context, pressure, reorient, crrc, and reorientResult. - Extend Phase 6 observability only where it reflects authoritative state or retrieval summaries without becoming a scheduler. - - Build the next role-ownership layer over landed Epiphany signals instead of adding invisible organs. + - Build explicit role-scoped specialist launch/read-back templates over the landed roles and job-control seams instead of adding invisible organs. - Keep persistent project state ruthlessly distilled so rehydration strengthens the next turn instead of bloating it. accepted_design: @@ -83,6 +83,7 @@ accepted_design: - `thread/epiphany/reorient` is the first bounded CRRC policy surface: it derives a read-only `resume` versus `regather` verdict from checkpoint, freshness, watcher, and pressure signals without mutating state, compacting, scheduling, or silently continuing work. - `thread/epiphany/crrc` is the first bounded CRRC coordinator recommendation surface: it composes pressure, the reorientation verdict, fixed reorient-worker binding/result status, finding read-back, and available actions into one read-only recommendation without launching, accepting, compacting, scheduling, or mutating state. - `tools/epiphany_mvp_status.py` is the first dogfood operator view: it starts or reads a thread through app-server and renders scene, pressure, reorient, jobs, reorient result, CRRC recommendation, and MVP role lanes as text or JSON without becoming a state store. + - `thread/epiphany/roles` is the first harness-native role ownership surface: it projects implementation, modeling/checkpoint, verification/review, and reorientation lanes from authoritative typed state, bound jobs, pressure/reorient/CRRC, and worker result status without launching, scheduling, accepting, compacting, or mutating state. - `thread/epiphany/reorientLaunch` is the first bounded runtime consumer over the reorientation verdict: it launches one fixed `reorient-worker` job through the existing job-control seam with explicit resume-versus-regather scope, authority, and checkpoint-derived payload, without making `thread/epiphany/reorient` a writer or turning CRRC into an ambient scheduler. - `thread/epiphany/reorientResult` is the first bounded read-back surface for that worker: it reads the fixed or requested reorient-worker binding through the current `agent_jobs` backend and projects completed output as a reviewable finding without mutation, promotion, scheduling, or hidden continuation. - `thread/epiphany/reorientAccept` is the first explicit acceptance write for completed reorient-worker findings: it appends accepted observation/evidence records, can optionally update scratch and investigation checkpoint state, and emits `stateUpdated` with source `reorientAccept` without accepting pending work or launching follow-up work. @@ -107,6 +108,7 @@ landed_baseline: - Phase 6 read-back: read-only reorient-worker result reflection through `thread/epiphany/reorientResult`. - Phase 6 acceptance: explicit reorient-worker finding acceptance through `thread/epiphany/reorientAccept`. - Phase 6 dogfood view: first CLI operator status over scene/pressure/reorient/jobs/result/CRRC. + - Phase 6 role ownership: read-only `thread/epiphany/roles` over implementation, modeling/checkpoint, verification/review, and reorientation lanes. guardrails: - Use `tools/epiphany_phase5_smoke.py` before modifying update, distill, propose, promote, read-projection, or notification behavior. - Use `tools/epiphany_phase6_scene_smoke.py` before modifying scene projection behavior. @@ -139,5 +141,5 @@ rejected_paths: reason: persistent state that accumulates every local truth becomes memory cruft and weakens rehydration current_status: - summary: Phase 1 durable state, Phase 2 prompt integration, Phase 3 typed read, Phase 4 retrieval/indexing, Phase 5 update/distill/propose/promote/read/notification, and Phase 6 read-only scene, jobs, freshness, targeted context, context-pressure reflection, watcher-backed invalidation inputs, durable investigation checkpointing, bounded CRRC reorientation policy, read-only CRRC coordinator recommendation, first dogfood operator status view with MVP role lanes, live bound-job progress notifications, a thin launcher seam in durable `job_bindings`, explicit `thread/epiphany/jobLaunch` / `thread/epiphany/jobInterrupt` authority over that seam, bounded `thread/epiphany/reorientLaunch`, read-only `thread/epiphany/reorientResult`, and explicit `thread/epiphany/reorientAccept` are landed. `job_bindings` now carry Epiphany-owned launcher metadata such as launcher id, authority scope, and backend kind/job id, while still tolerating the older runtime-agent-job pointer as a compatibility alias. `thread/epiphany/jobs` overlays that launcher seam onto real runtime `agent_jobs` progress for bound work, including runtime/backend job id, worker thread ids, and progress counts, without becoming a scheduler. `thread/epiphany/jobsUpdated` now translates runtime `agent_job_progress:{json}` background events into changed launcher-bound payloads only when the mapped job state actually changes, so clients can watch real owners move without polling the jobs read surface. The launch/interrupt control path still uses direct state-runtime fallback when a loaded ephemeral thread does not already carry a live sqlite handle, so authoritative bindings and runtime snapshots stay aligned instead of pretending the backend vanished. `thread/epiphany/reorient` consumes checkpoint, freshness, watcher, and pressure signals to return a read-only `resume` versus `regather` verdict with explicit checkpoint-path/frontier reasons. `thread/epiphany/crrc` composes pressure, that verdict, the fixed reorient-worker binding/result, finding read-back, and available actions into a read-only recommendation such as continue, prepare checkpoint, launch worker, wait, review, accept, or regather manually without launching, accepting, compacting, scheduling, or mutating. `tools/epiphany_mvp_status.py` starts or reads a thread through app-server and renders scene, pressure, reorient, jobs, reorient result, CRRC recommendation, and derived implementation/modeling/verification/reorientation role lanes as text or JSON without becoming a state store. `thread/epiphany/reorientLaunch` explicitly launches one fixed `reorient-worker` job with resume-or-regather scope, checkpoint-derived payload, and bounded structured output expectations. `thread/epiphany/reorientResult` reads that worker output back from the current `agent_jobs` backend as a reviewable finding with mode, summary, next safe move, checkpoint validity, inspected files, frontier ids, evidence ids, raw result, and errors, without mutating state, promoting evidence, scheduling work, or silently continuing. `thread/epiphany/reorientAccept` requires a completed worker result, appends accepted observation/evidence records, can explicitly update scratch and investigation checkpoint state, and emits `stateUpdated` with source `reorientAccept` without accepting pending work, auto-promoting arbitrary output, or launching follow-up work. Focused protocol/app-server tests, schema fixture regeneration/checks, app-server build, the live reorient-launch smoke, and the MVP status smoke cover the launch/read-back/accept/coordinator/operator seam. Investigation checkpoints still live in authoritative typed state, flow through the prompt, and reflect through scene/context without adding a second writer or fake scheduler. Persistent state was cut down so map, handoff, plan, harness surfaces, scratch, and evidence each carry one job instead of becoming a Jenga tower; evidence is now a distilled ledger rather than an activity feed. The global AGENTS Epiphany core command says persistent state is the agent's mind and explicitly permits the agent to ask for self-modification when memory or workflow makes it worse. The continuity goal is to bank the fire before the dark so compaction becomes less like death and more like sleep. - next_action: Continue toward the MVP by adding the smallest role-scoped specialist ownership layer that makes implementation, modeling/checkpoint maintenance, and verification/review visible as distinct lanes without building a marketplace or hidden scheduler. Automatic broad specialist scheduling remains future work; do not restart the completed pressure, freshness, watcher-invalidation, investigation-checkpoint, reorientation, crrc, mvp-status, jobs-binding, jobsUpdated, thin-launcher-seam, job-control, reorient-launch, reorient-result, or reorient-accept slice unless a concrete regression appears. + summary: Phase 1 durable state, Phase 2 prompt integration, Phase 3 typed read, Phase 4 retrieval/indexing, Phase 5 update/distill/propose/promote/read/notification, and Phase 6 read-only scene, jobs, roles, freshness, targeted context, pressure, reorient, CRRC, reorient launch/result/accept, and first dogfood MVP status are landed. `thread/epiphany/roles` now projects implementation, modeling/checkpoint, verification/review, and reorientation lanes from authoritative typed state plus jobs/pressure/reorient/CRRC/result signals without launching, scheduling, accepting, compacting, or mutating state. The status tool consumes that harness-native roles surface instead of deriving lanes locally. The thin launcher seam, jobLaunch/jobInterrupt, jobsUpdated, reorientLaunch, reorientResult, reorientAccept, and crrc remain explicit and review-gated. Focused protocol/app-server tests, schema fixture regeneration/checks, app-server build, and MVP status smoke verify the role ownership/coordinator/operator seam. Persistent state remains distilled; evidence is a belief ledger, not an activity feed. + next_action: Continue toward the MVP by adding the smallest explicit role-scoped specialist launch/read-back templates for modeling/checkpoint and verification/review over the landed job-control seam, while keeping scheduling manual and review-gated. Automatic broad specialist scheduling remains future work; do not restart the completed pressure, freshness, watcher-invalidation, investigation-checkpoint, reorientation, crrc, mvp-status, roles, jobs-binding, jobsUpdated, thin-launcher-seam, job-control, reorient-launch, reorient-result, or reorient-accept slice unless a concrete regression appears. diff --git a/tools/epiphany_mvp_status.py b/tools/epiphany_mvp_status.py index aa9daaeb..ce3f02ab 100644 --- a/tools/epiphany_mvp_status.py +++ b/tools/epiphany_mvp_status.py @@ -51,76 +51,6 @@ def job_by_id(jobs: list[dict[str, Any]], job_id: str) -> dict[str, Any] | None: return None -def derive_role_lanes(status: dict[str, Any]) -> list[dict[str, str]]: - scene = status["scene"]["scene"] - pressure = status["pressure"]["pressure"] - reorient = status["reorient"]["decision"] - jobs = status["jobs"]["jobs"] - result = status["reorientResult"] - recommendation = status["crrc"]["recommendation"] - checkpoint = scene.get("investigationCheckpoint") or {} - verification = job_by_id(jobs, "verification") - - implementation_status = "ready" - implementation_note = "Continue the bounded coding task." - if recommendation["action"] != "continue": - implementation_status = "blocked" - implementation_note = f"Wait for CRRC action: {recommendation['action']}." - - modeling_status = "ready" if checkpoint else "needed" - modeling_note = ( - f"{checkpoint.get('disposition', 'checkpoint missing')}: " - f"{checkpoint.get('nextAction') or reorient['nextAction']}" - ) - - verification_status = verification.get("status", "unknown") if verification else "unknown" - if verification: - verification_note = ( - verification.get("blockingReason") - or verification.get("progressNote") - or "Review evidence and accepted findings before promotion." - ) - else: - verification_note = "Verification lane is unavailable." - - reorientation_status = recommendation["action"] - reorientation_note = ( - f"{reorient['action']} verdict, result {result['status']}, " - f"pressure {pressure['level']}." - ) - - return [ - { - "id": "implementation", - "title": "Implementation", - "status": implementation_status, - "owner": "coding-agent", - "note": implementation_note, - }, - { - "id": "modeling", - "title": "Modeling / Checkpoint", - "status": modeling_status, - "owner": "epiphany-modeler", - "note": modeling_note, - }, - { - "id": "verification", - "title": "Verification / Review", - "status": verification_status, - "owner": "epiphany-verifier", - "note": verification_note, - }, - { - "id": "reorientation", - "title": "Reorientation", - "status": reorientation_status, - "owner": "epiphany-reorient", - "note": reorientation_note, - }, - ] - - def collect_status( client: AppServerClient, *, @@ -142,6 +72,7 @@ def collect_status( pressure = client.send("thread/epiphany/pressure", {"threadId": thread_id}) reorient = client.send("thread/epiphany/reorient", {"threadId": thread_id}) jobs = client.send("thread/epiphany/jobs", {"threadId": thread_id}) + roles = client.send("thread/epiphany/roles", {"threadId": thread_id}) reorient_result = client.send("thread/epiphany/reorientResult", {"threadId": thread_id}) crrc = client.send("thread/epiphany/crrc", {"threadId": thread_id}) @@ -152,10 +83,10 @@ def collect_status( "pressure": pressure, "reorient": reorient, "jobs": jobs, + "roles": roles, "reorientResult": reorient_result, "crrc": crrc, } - status["roleLanes"] = derive_role_lanes(status) return status @@ -167,6 +98,7 @@ def render_status(status: dict[str, Any]) -> str: jobs = status["jobs"]["jobs"] result = status["reorientResult"] crrc = status["crrc"] + roles = status["roles"]["roles"] recommendation = crrc["recommendation"] checkpoint = scene.get("investigationCheckpoint") or {} @@ -205,9 +137,9 @@ def render_status(status: dict[str, Any]) -> str: "Role Lanes", ] ) - for lane in status["roleLanes"]: + for lane in roles: lines.append( - f"- {lane['title']}: {lane['status']} ({lane['owner']}) - {lane['note']}" + f"- {lane['title']}: {lane['status']} ({lane['ownerRole']}) - {lane['note']}" ) lines.extend( diff --git a/tools/epiphany_mvp_status_smoke.py b/tools/epiphany_mvp_status_smoke.py index bec1e84f..9e0d2ad9 100644 --- a/tools/epiphany_mvp_status_smoke.py +++ b/tools/epiphany_mvp_status_smoke.py @@ -55,6 +55,10 @@ def run_smoke(args: argparse.Namespace) -> dict[str, object]: "crrc" in status["scene"]["scene"]["availableActions"], "status view should expose the CRRC action in the scene", ) + require( + "roles" in status["scene"]["scene"]["availableActions"], + "status view should expose the role ownership action in the scene", + ) require( "Epiphany MVP Status" in rendered and "Recommendation" in rendered, "rendered status should include the operator view headings", @@ -63,10 +67,14 @@ def run_smoke(args: argparse.Namespace) -> dict[str, object]: "regatherManually" in rendered, "rendered status should expose the CRRC recommendation", ) - lane_ids = [lane["id"] for lane in status["roleLanes"]] + lane_ids = [lane["id"] for lane in status["roles"]["roles"]] require( lane_ids == ["implementation", "modeling", "verification", "reorientation"], - "status view should expose the four MVP role lanes", + "roles surface should expose the four MVP role lanes", + ) + require( + status["roles"]["note"].startswith("Role ownership is derived read-only"), + "roles surface should declare read-only derived ownership", ) require( "Role Lanes" in rendered and "Verification / Review" in rendered, @@ -76,7 +84,7 @@ def run_smoke(args: argparse.Namespace) -> dict[str, object]: result = { "threadId": status["threadId"], "recommendation": status["crrc"]["recommendation"], - "roleLanes": status["roleLanes"], + "roles": status["roles"], "stateStatus": status["scene"]["scene"]["stateStatus"], "availableActions": status["scene"]["scene"]["availableActions"], "rendered": rendered, diff --git a/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index ce94141a..1625c0b9 100644 --- a/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -15988,6 +15988,84 @@ ], "type": "object" }, + "ThreadEpiphanyRoleId": { + "enum": [ + "implementation", + "modeling", + "verification", + "reorientation" + ], + "type": "string" + }, + "ThreadEpiphanyRoleLane": { + "properties": { + "authorityScopes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "id": { + "$ref": "#/definitions/v2/ThreadEpiphanyRoleId" + }, + "jobs": { + "items": { + "$ref": "#/definitions/v2/ThreadEpiphanyJob" + }, + "type": "array" + }, + "note": { + "type": "string" + }, + "ownerRole": { + "type": "string" + }, + "recommendedAction": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ThreadEpiphanySceneAction" + }, + { + "type": "null" + } + ] + }, + "status": { + "$ref": "#/definitions/v2/ThreadEpiphanyRoleStatus" + }, + "title": { + "type": "string" + } + }, + "required": [ + "id", + "note", + "ownerRole", + "status", + "title" + ], + "type": "object" + }, + "ThreadEpiphanyRoleStatus": { + "enum": [ + "ready", + "needed", + "blocked", + "running", + "completed", + "waiting", + "review", + "unavailable" + ], + "type": "string" + }, + "ThreadEpiphanyRolesSource": { + "enum": [ + "stored", + "live" + ], + "type": "string" + }, "ThreadEpiphanyScene": { "properties": { "activeSubgoal": { @@ -16094,6 +16172,7 @@ "distill", "context", "jobs", + "roles", "jobLaunch", "jobInterrupt", "freshness", diff --git a/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index a2620e19..eb3c10a8 100644 --- a/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -13875,6 +13875,84 @@ ], "type": "object" }, + "ThreadEpiphanyRoleId": { + "enum": [ + "implementation", + "modeling", + "verification", + "reorientation" + ], + "type": "string" + }, + "ThreadEpiphanyRoleLane": { + "properties": { + "authorityScopes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "id": { + "$ref": "#/definitions/ThreadEpiphanyRoleId" + }, + "jobs": { + "items": { + "$ref": "#/definitions/ThreadEpiphanyJob" + }, + "type": "array" + }, + "note": { + "type": "string" + }, + "ownerRole": { + "type": "string" + }, + "recommendedAction": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadEpiphanySceneAction" + }, + { + "type": "null" + } + ] + }, + "status": { + "$ref": "#/definitions/ThreadEpiphanyRoleStatus" + }, + "title": { + "type": "string" + } + }, + "required": [ + "id", + "note", + "ownerRole", + "status", + "title" + ], + "type": "object" + }, + "ThreadEpiphanyRoleStatus": { + "enum": [ + "ready", + "needed", + "blocked", + "running", + "completed", + "waiting", + "review", + "unavailable" + ], + "type": "string" + }, + "ThreadEpiphanyRolesSource": { + "enum": [ + "stored", + "live" + ], + "type": "string" + }, "ThreadEpiphanyScene": { "properties": { "activeSubgoal": { @@ -13981,6 +14059,7 @@ "distill", "context", "jobs", + "roles", "jobLaunch", "jobInterrupt", "freshness", diff --git a/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyRoleId.ts b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyRoleId.ts new file mode 100644 index 00000000..aa86f4ab --- /dev/null +++ b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyRoleId.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadEpiphanyRoleId = "implementation" | "modeling" | "verification" | "reorientation"; diff --git a/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyRoleLane.ts b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyRoleLane.ts new file mode 100644 index 00000000..8bda0afb --- /dev/null +++ b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyRoleLane.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadEpiphanyJob } from "./ThreadEpiphanyJob"; +import type { ThreadEpiphanyRoleId } from "./ThreadEpiphanyRoleId"; +import type { ThreadEpiphanyRoleStatus } from "./ThreadEpiphanyRoleStatus"; +import type { ThreadEpiphanySceneAction } from "./ThreadEpiphanySceneAction"; + +export type ThreadEpiphanyRoleLane = { id: ThreadEpiphanyRoleId, title: string, ownerRole: string, status: ThreadEpiphanyRoleStatus, note: string, jobs?: Array, authorityScopes?: Array, recommendedAction?: ThreadEpiphanySceneAction | null, }; diff --git a/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyRoleStatus.ts b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyRoleStatus.ts new file mode 100644 index 00000000..211ab5d5 --- /dev/null +++ b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyRoleStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadEpiphanyRoleStatus = "ready" | "needed" | "blocked" | "running" | "completed" | "waiting" | "review" | "unavailable"; diff --git a/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyRolesSource.ts b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyRolesSource.ts new file mode 100644 index 00000000..dbdb0c53 --- /dev/null +++ b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyRolesSource.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadEpiphanyRolesSource = "stored" | "live"; diff --git a/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanySceneAction.ts b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanySceneAction.ts index d1a639f0..6866bff3 100644 --- a/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanySceneAction.ts +++ b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanySceneAction.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ThreadEpiphanySceneAction = "index" | "retrieve" | "distill" | "context" | "jobs" | "jobLaunch" | "jobInterrupt" | "freshness" | "pressure" | "reorient" | "crrc" | "reorientLaunch" | "reorientResult" | "reorientAccept" | "propose" | "promote" | "update"; +export type ThreadEpiphanySceneAction = "index" | "retrieve" | "distill" | "context" | "jobs" | "roles" | "jobLaunch" | "jobInterrupt" | "freshness" | "pressure" | "reorient" | "crrc" | "reorientLaunch" | "reorientResult" | "reorientAccept" | "propose" | "promote" | "update"; diff --git a/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 30bdc72c..72607853 100644 --- a/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -355,6 +355,10 @@ export type { ThreadEpiphanyRetrieveIndexSummary } from "./ThreadEpiphanyRetriev export type { ThreadEpiphanyRetrieveResult } from "./ThreadEpiphanyRetrieveResult"; export type { ThreadEpiphanyRetrieveResultKind } from "./ThreadEpiphanyRetrieveResultKind"; export type { ThreadEpiphanyRetrieveShardSummary } from "./ThreadEpiphanyRetrieveShardSummary"; +export type { ThreadEpiphanyRoleId } from "./ThreadEpiphanyRoleId"; +export type { ThreadEpiphanyRoleLane } from "./ThreadEpiphanyRoleLane"; +export type { ThreadEpiphanyRoleStatus } from "./ThreadEpiphanyRoleStatus"; +export type { ThreadEpiphanyRolesSource } from "./ThreadEpiphanyRolesSource"; export type { ThreadEpiphanyScene } from "./ThreadEpiphanyScene"; export type { ThreadEpiphanySceneAction } from "./ThreadEpiphanySceneAction"; export type { ThreadEpiphanySceneChurn } from "./ThreadEpiphanySceneChurn"; diff --git a/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs b/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs index f7d0ccac..b3c6f217 100644 --- a/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs @@ -346,6 +346,11 @@ client_request_definitions! { params: v2::ThreadEpiphanyJobsParams, response: v2::ThreadEpiphanyJobsResponse, }, + #[experimental("thread/epiphany/roles")] + ThreadEpiphanyRoles => "thread/epiphany/roles" { + params: v2::ThreadEpiphanyRolesParams, + response: v2::ThreadEpiphanyRolesResponse, + }, #[experimental("thread/epiphany/freshness")] ThreadEpiphanyFreshness => "thread/epiphany/freshness" { params: v2::ThreadEpiphanyFreshnessParams, @@ -1726,6 +1731,7 @@ mod tests { v2::ThreadEpiphanySceneAction::Distill, v2::ThreadEpiphanySceneAction::Context, v2::ThreadEpiphanySceneAction::Jobs, + v2::ThreadEpiphanySceneAction::Roles, v2::ThreadEpiphanySceneAction::JobLaunch, v2::ThreadEpiphanySceneAction::Freshness, v2::ThreadEpiphanySceneAction::Pressure, @@ -1827,6 +1833,7 @@ mod tests { "distill", "context", "jobs", + "roles", "jobLaunch", "freshness", "pressure", @@ -1910,6 +1917,87 @@ mod tests { Ok(()) } + #[test] + fn serialize_thread_epiphany_roles_response() -> Result<()> { + let response = ClientResponse::ThreadEpiphanyRoles { + request_id: RequestId::Integer(8), + response: v2::ThreadEpiphanyRolesResponse { + thread_id: "thr_123".to_string(), + source: v2::ThreadEpiphanyRolesSource::Live, + state_status: v2::ThreadEpiphanyReorientStateStatus::Ready, + state_revision: Some(3), + roles: vec![v2::ThreadEpiphanyRoleLane { + id: v2::ThreadEpiphanyRoleId::Verification, + title: "Verification / Review".to_string(), + owner_role: "epiphany-verifier".to_string(), + status: v2::ThreadEpiphanyRoleStatus::Needed, + note: "Review evidence before promotion.".to_string(), + jobs: vec![v2::ThreadEpiphanyJob { + id: "verification".to_string(), + kind: v2::ThreadEpiphanyJobKind::Verification, + scope: "invariant verification".to_string(), + owner_role: "epiphany-verifier".to_string(), + launcher_job_id: None, + authority_scope: None, + backend_kind: None, + backend_job_id: None, + status: v2::ThreadEpiphanyJobStatus::Needed, + runtime_agent_job_id: None, + items_processed: None, + items_total: None, + progress_note: None, + last_checkpoint_at_unix_seconds: None, + blocking_reason: None, + active_thread_ids: Vec::new(), + linked_subgoal_ids: Vec::new(), + linked_graph_node_ids: Vec::new(), + }], + authority_scopes: vec!["thread/epiphany/promote".to_string()], + recommended_action: Some(v2::ThreadEpiphanySceneAction::Promote), + }], + note: "Role ownership is derived read-only.".to_string(), + }, + }; + + assert_eq!(response.id(), &RequestId::Integer(8)); + assert_eq!(response.method(), "thread/epiphany/roles"); + assert_eq!( + json!({ + "method": "thread/epiphany/roles", + "id": 8, + "response": { + "threadId": "thr_123", + "source": "live", + "stateStatus": "ready", + "stateRevision": 3, + "roles": [ + { + "id": "verification", + "title": "Verification / Review", + "ownerRole": "epiphany-verifier", + "status": "needed", + "note": "Review evidence before promotion.", + "jobs": [ + { + "id": "verification", + "kind": "verification", + "scope": "invariant verification", + "ownerRole": "epiphany-verifier", + "status": "needed" + } + ], + "authorityScopes": ["thread/epiphany/promote"], + "recommendedAction": "promote" + } + ], + "note": "Role ownership is derived read-only." + } + }), + serde_json::to_value(&response)?, + ); + Ok(()) + } + #[test] fn serialize_thread_epiphany_freshness_response() -> Result<()> { let response = ClientResponse::ThreadEpiphanyFreshness { @@ -3880,6 +3968,18 @@ mod tests { assert_eq!(reason, Some("thread/epiphany/jobs")); } + #[test] + fn thread_epiphany_roles_is_marked_experimental() { + let request = ClientRequest::ThreadEpiphanyRoles { + request_id: RequestId::Integer(1), + params: v2::ThreadEpiphanyRolesParams { + thread_id: "thr_123".to_string(), + }, + }; + let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&request); + assert_eq!(reason, Some("thread/epiphany/roles")); + } + #[test] fn thread_epiphany_freshness_is_marked_experimental() { let request = ClientRequest::ThreadEpiphanyFreshness { diff --git a/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs b/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs index a20b24c9..e58700b1 100644 --- a/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -3997,6 +3997,7 @@ pub enum ThreadEpiphanySceneAction { Distill, Context, Jobs, + Roles, JobLaunch, JobInterrupt, Freshness, @@ -4155,6 +4156,77 @@ pub struct ThreadEpiphanyJobsResponse { pub jobs: Vec, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadEpiphanyRolesParams { + pub thread_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadEpiphanyRolesResponse { + pub thread_id: String, + pub source: ThreadEpiphanyRolesSource, + pub state_status: ThreadEpiphanyReorientStateStatus, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub state_revision: Option, + pub roles: Vec, + pub note: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum ThreadEpiphanyRolesSource { + Stored, + Live, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum ThreadEpiphanyRoleId { + Implementation, + Modeling, + Verification, + Reorientation, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum ThreadEpiphanyRoleStatus { + Ready, + Needed, + Blocked, + Running, + Completed, + Waiting, + Review, + Unavailable, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadEpiphanyRoleLane { + pub id: ThreadEpiphanyRoleId, + pub title: String, + pub owner_role: String, + pub status: ThreadEpiphanyRoleStatus, + pub note: String, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub jobs: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub authority_scopes: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub recommended_action: Option, +} + #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs b/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs index 6626e1fc..af7bed4c 100644 --- a/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs +++ b/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs @@ -218,6 +218,12 @@ use codex_app_server_protocol::ThreadEpiphanyRetrieveResponse; use codex_app_server_protocol::ThreadEpiphanyRetrieveResult; use codex_app_server_protocol::ThreadEpiphanyRetrieveResultKind; use codex_app_server_protocol::ThreadEpiphanyRetrieveShardSummary; +use codex_app_server_protocol::ThreadEpiphanyRoleId; +use codex_app_server_protocol::ThreadEpiphanyRoleLane; +use codex_app_server_protocol::ThreadEpiphanyRoleStatus; +use codex_app_server_protocol::ThreadEpiphanyRolesParams; +use codex_app_server_protocol::ThreadEpiphanyRolesResponse; +use codex_app_server_protocol::ThreadEpiphanyRolesSource; use codex_app_server_protocol::ThreadEpiphanyScene; use codex_app_server_protocol::ThreadEpiphanySceneAction; use codex_app_server_protocol::ThreadEpiphanySceneChurn; @@ -1086,6 +1092,10 @@ impl CodexMessageProcessor { self.thread_epiphany_jobs(to_connection_request_id(request_id), params) .await; } + ClientRequest::ThreadEpiphanyRoles { request_id, params } => { + self.thread_epiphany_roles(to_connection_request_id(request_id), params) + .await; + } ClientRequest::ThreadEpiphanyFreshness { request_id, params } => { self.thread_epiphany_freshness(to_connection_request_id(request_id), params) .await; @@ -4298,6 +4308,146 @@ impl CodexMessageProcessor { self.outgoing.send_response(request_id, response).await; } + async fn thread_epiphany_roles( + &self, + request_id: ConnectionRequestId, + params: ThreadEpiphanyRolesParams, + ) { + let ThreadEpiphanyRolesParams { thread_id } = params; + + let thread_uuid = match ThreadId::from_string(&thread_id) { + Ok(id) => id, + Err(err) => { + self.send_invalid_request_error(request_id, format!("invalid thread id: {err}")) + .await; + return; + } + }; + + let loaded_thread = self.thread_manager.get_thread(thread_uuid).await.ok(); + let loaded = loaded_thread.is_some(); + let thread = match self.read_thread_view(thread_uuid, false).await { + Ok(thread) => thread, + Err(ThreadReadViewError::InvalidRequest(message)) => { + self.send_invalid_request_error(request_id, message).await; + return; + } + Err(ThreadReadViewError::Internal(message)) => { + self.send_internal_error(request_id, message).await; + return; + } + }; + + let retrieval_override = if let Some(loaded_thread) = loaded_thread.as_ref() { + Some(loaded_thread.epiphany_retrieval_state().await) + } else { + None + }; + let watcher_snapshot = if let Some(loaded_thread) = loaded_thread.as_ref() { + let config_snapshot = loaded_thread.config_snapshot().await; + self.epiphany_invalidation_manager + .ensure_thread_watch(&thread_id, &config_snapshot.cwd) + .await; + Some( + self.epiphany_invalidation_manager + .snapshot(&thread_id) + .await, + ) + } else { + None + }; + let (source, token_usage_info) = if let Some(loaded_thread) = loaded_thread.as_ref() { + ( + ThreadEpiphanyRolesSource::Live, + loaded_thread.token_usage_info().await, + ) + } else { + let token_usage_info = match thread.path.as_deref() { + Some(path) => latest_token_usage_info_from_rollout_path(path).await, + None => None, + }; + (ThreadEpiphanyRolesSource::Stored, token_usage_info) + }; + + let (state_revision, retrieval, graph, watcher) = map_epiphany_freshness( + thread.epiphany_state.as_ref(), + retrieval_override.as_ref(), + watcher_snapshot.as_ref(), + ); + let pressure = map_epiphany_pressure(token_usage_info.as_ref()); + let (state_status, decision) = map_epiphany_reorient( + thread.epiphany_state.as_ref(), + &pressure, + &retrieval, + &graph, + &watcher, + ); + + let mut state_db_ctx = if let Some(loaded_thread) = loaded_thread.as_ref() { + loaded_thread.epiphany_state_runtime().await + } else { + None + }; + if state_db_ctx.is_none() { + state_db_ctx = open_state_db_for_direct_thread_lookup(&self.config).await; + } + let job_resolution = load_epiphany_job_launcher_snapshots( + state_db_ctx.as_ref(), + thread.epiphany_state.as_ref(), + ) + .await; + let jobs = map_epiphany_jobs( + thread.epiphany_state.as_ref(), + retrieval_override.as_ref(), + &job_resolution, + ); + let reorient_job = jobs + .iter() + .find(|job| job.id == EPIPHANY_REORIENT_LAUNCH_BINDING_ID) + .cloned(); + let (result_status, finding, _) = load_epiphany_reorient_result_snapshot( + thread.epiphany_state.as_ref(), + state_db_ctx.as_ref(), + EPIPHANY_REORIENT_LAUNCH_BINDING_ID, + ) + .await; + let checkpoint_present = thread + .epiphany_state + .as_ref() + .and_then(|state| state.investigation_checkpoint.as_ref()) + .is_some(); + let recommendation = map_epiphany_crrc_recommendation( + loaded, + state_status, + &pressure, + &decision, + result_status, + checkpoint_present, + finding.is_some(), + ); + + let roles = map_epiphany_roles( + thread.epiphany_state.as_ref(), + &jobs, + &decision, + &pressure, + &recommendation, + result_status, + reorient_job.as_ref(), + ); + let note = render_epiphany_roles_note(&roles, state_status, recommendation.action); + + let response = ThreadEpiphanyRolesResponse { + thread_id, + source, + state_status, + state_revision, + roles, + note, + }; + self.outgoing.send_response(request_id, response).await; + } + async fn thread_epiphany_freshness( &self, request_id: ConnectionRequestId, @@ -12062,6 +12212,7 @@ fn epiphany_scene_available_actions( ThreadEpiphanySceneAction::Distill, ThreadEpiphanySceneAction::Context, ThreadEpiphanySceneAction::Jobs, + ThreadEpiphanySceneAction::Roles, ThreadEpiphanySceneAction::JobLaunch, ThreadEpiphanySceneAction::Freshness, ThreadEpiphanySceneAction::Pressure, @@ -12503,6 +12654,215 @@ fn map_epiphany_crrc_recommendation( ) } +fn map_epiphany_roles( + state: Option<&EpiphanyThreadState>, + jobs: &[ThreadEpiphanyJob], + decision: &ThreadEpiphanyReorientDecision, + pressure: &ThreadEpiphanyPressure, + recommendation: &ThreadEpiphanyCrrcRecommendation, + result_status: ThreadEpiphanyReorientResultStatus, + reorient_job: Option<&ThreadEpiphanyJob>, +) -> Vec { + let checkpoint = state.and_then(|state| state.investigation_checkpoint.as_ref()); + let graph_remap_job = jobs.iter().find(|job| job.id == "graph-remap").cloned(); + let verification_job = jobs.iter().find(|job| job.id == "verification").cloned(); + let reorientation_jobs = reorient_job.cloned().into_iter().collect::>(); + + vec![ + ThreadEpiphanyRoleLane { + id: ThreadEpiphanyRoleId::Implementation, + title: "Implementation".to_string(), + owner_role: "coding-agent".to_string(), + status: if recommendation.action == ThreadEpiphanyCrrcAction::Continue { + ThreadEpiphanyRoleStatus::Ready + } else { + ThreadEpiphanyRoleStatus::Blocked + }, + note: if recommendation.action == ThreadEpiphanyCrrcAction::Continue { + "Continue the bounded coding task.".to_string() + } else { + format!("Wait for CRRC action: {:?}.", recommendation.action) + }, + jobs: jobs + .iter() + .filter(|job| { + matches!( + job.owner_role.as_str(), + "coding-agent" | "implementation" | "epiphany-implementation" + ) + }) + .cloned() + .collect(), + authority_scopes: Vec::new(), + recommended_action: if recommendation.action == ThreadEpiphanyCrrcAction::Continue { + None + } else { + recommendation.recommended_scene_action + }, + }, + ThreadEpiphanyRoleLane { + id: ThreadEpiphanyRoleId::Modeling, + title: "Modeling / Checkpoint".to_string(), + owner_role: "epiphany-modeler".to_string(), + status: map_epiphany_modeling_role_status( + checkpoint.is_some(), + graph_remap_job.as_ref(), + ), + note: render_epiphany_modeling_role_note(checkpoint, decision), + jobs: graph_remap_job.into_iter().collect(), + authority_scopes: vec!["thread/epiphany/update".to_string()], + recommended_action: if checkpoint.is_some() { + None + } else { + Some(ThreadEpiphanySceneAction::Update) + }, + }, + ThreadEpiphanyRoleLane { + id: ThreadEpiphanyRoleId::Verification, + title: "Verification / Review".to_string(), + owner_role: "epiphany-verifier".to_string(), + status: verification_job + .as_ref() + .map(|job| map_epiphany_job_status_to_role_status(job.status)) + .unwrap_or(ThreadEpiphanyRoleStatus::Unavailable), + note: verification_job + .as_ref() + .and_then(|job| { + job.blocking_reason + .clone() + .or_else(|| job.progress_note.clone()) + }) + .unwrap_or_else(|| { + "Review evidence, worker findings, and verifier results before promotion." + .to_string() + }), + jobs: verification_job.into_iter().collect(), + authority_scopes: vec![ + "thread/epiphany/distill".to_string(), + "thread/epiphany/propose".to_string(), + "thread/epiphany/promote".to_string(), + ], + recommended_action: Some(ThreadEpiphanySceneAction::Promote), + }, + ThreadEpiphanyRoleLane { + id: ThreadEpiphanyRoleId::Reorientation, + title: "Reorientation".to_string(), + owner_role: EPIPHANY_REORIENT_OWNER_ROLE.to_string(), + status: map_epiphany_reorientation_role_status(recommendation.action, result_status), + note: format!( + "{:?} verdict, result {:?}, pressure {:?}. {}", + decision.action, result_status, pressure.level, recommendation.reason + ), + jobs: reorientation_jobs, + authority_scopes: vec![ + "thread/epiphany/reorientLaunch".to_string(), + "thread/epiphany/reorientResult".to_string(), + "thread/epiphany/reorientAccept".to_string(), + ], + recommended_action: recommendation.recommended_scene_action, + }, + ] +} + +fn map_epiphany_modeling_role_status( + checkpoint_present: bool, + graph_remap_job: Option<&ThreadEpiphanyJob>, +) -> ThreadEpiphanyRoleStatus { + if let Some(job) = graph_remap_job + && matches!( + job.status, + ThreadEpiphanyJobStatus::Pending | ThreadEpiphanyJobStatus::Running + ) + { + return ThreadEpiphanyRoleStatus::Running; + } + if checkpoint_present { + ThreadEpiphanyRoleStatus::Ready + } else { + ThreadEpiphanyRoleStatus::Needed + } +} + +fn render_epiphany_modeling_role_note( + checkpoint: Option<&EpiphanyInvestigationCheckpoint>, + decision: &ThreadEpiphanyReorientDecision, +) -> String { + let Some(checkpoint) = checkpoint else { + return format!("Checkpoint missing: {}", decision.next_action); + }; + let next_action = checkpoint + .next_action + .as_deref() + .unwrap_or(decision.next_action.as_str()); + format!("{:?}: {next_action}", checkpoint.disposition) +} + +fn map_epiphany_job_status_to_role_status( + status: ThreadEpiphanyJobStatus, +) -> ThreadEpiphanyRoleStatus { + match status { + ThreadEpiphanyJobStatus::Idle => ThreadEpiphanyRoleStatus::Ready, + ThreadEpiphanyJobStatus::Needed => ThreadEpiphanyRoleStatus::Needed, + ThreadEpiphanyJobStatus::Pending | ThreadEpiphanyJobStatus::Running => { + ThreadEpiphanyRoleStatus::Running + } + ThreadEpiphanyJobStatus::Completed => ThreadEpiphanyRoleStatus::Completed, + ThreadEpiphanyJobStatus::Failed + | ThreadEpiphanyJobStatus::Cancelled + | ThreadEpiphanyJobStatus::Blocked => ThreadEpiphanyRoleStatus::Blocked, + ThreadEpiphanyJobStatus::Unavailable => ThreadEpiphanyRoleStatus::Unavailable, + } +} + +fn map_epiphany_reorientation_role_status( + action: ThreadEpiphanyCrrcAction, + result_status: ThreadEpiphanyReorientResultStatus, +) -> ThreadEpiphanyRoleStatus { + match result_status { + ThreadEpiphanyReorientResultStatus::Pending + | ThreadEpiphanyReorientResultStatus::Running => { + return ThreadEpiphanyRoleStatus::Waiting; + } + ThreadEpiphanyReorientResultStatus::Failed + | ThreadEpiphanyReorientResultStatus::Cancelled + | ThreadEpiphanyReorientResultStatus::BackendUnavailable + | ThreadEpiphanyReorientResultStatus::BackendMissing => { + return ThreadEpiphanyRoleStatus::Blocked; + } + _ => {} + } + + match action { + ThreadEpiphanyCrrcAction::Continue => ThreadEpiphanyRoleStatus::Ready, + ThreadEpiphanyCrrcAction::PrepareCheckpoint + | ThreadEpiphanyCrrcAction::LaunchReorientWorker + | ThreadEpiphanyCrrcAction::RegatherManually => ThreadEpiphanyRoleStatus::Needed, + ThreadEpiphanyCrrcAction::WaitForReorientWorker => ThreadEpiphanyRoleStatus::Waiting, + ThreadEpiphanyCrrcAction::ReviewReorientResult + | ThreadEpiphanyCrrcAction::AcceptReorientResult => ThreadEpiphanyRoleStatus::Review, + } +} + +fn render_epiphany_roles_note( + roles: &[ThreadEpiphanyRoleLane], + state_status: ThreadEpiphanyReorientStateStatus, + recommendation: ThreadEpiphanyCrrcAction, +) -> String { + let blocked_count = roles + .iter() + .filter(|role| { + matches!( + role.status, + ThreadEpiphanyRoleStatus::Blocked | ThreadEpiphanyRoleStatus::Needed + ) + }) + .count(); + format!( + "Role ownership is derived read-only from Epiphany state, jobs, and CRRC. State: {:?}; recommendation: {:?}; blocked-or-needed lanes: {blocked_count}.", + state_status, recommendation + ) +} + async fn load_completed_epiphany_reorient_finding( thread: &CodexThread, state: &EpiphanyThreadState, @@ -16560,6 +16920,89 @@ mod tests { ); } + #[test] + fn map_epiphany_roles_projects_mvp_ownership_lanes() { + let state = codex_protocol::protocol::EpiphanyThreadState { + investigation_checkpoint: Some(EpiphanyInvestigationCheckpoint { + checkpoint_id: "ix-roles".to_string(), + kind: "slice_planning".to_string(), + disposition: + codex_protocol::protocol::EpiphanyInvestigationDisposition::ResumeReady, + focus: "Keep roles explicit.".to_string(), + next_action: Some("Continue from the bounded checkpoint.".to_string()), + ..Default::default() + }), + ..Default::default() + }; + let pressure = map_epiphany_pressure(None); + let decision = ThreadEpiphanyReorientDecision { + action: ThreadEpiphanyReorientAction::Resume, + checkpoint_status: ThreadEpiphanyReorientCheckpointStatus::ResumeReady, + checkpoint_id: Some("ix-roles".to_string()), + pressure_level: pressure.level, + retrieval_status: ThreadEpiphanyRetrievalFreshnessStatus::Ready, + graph_status: ThreadEpiphanyGraphFreshnessStatus::Ready, + watcher_status: ThreadEpiphanyInvalidationStatus::Clean, + reasons: vec![ThreadEpiphanyReorientReason::CheckpointReady], + checkpoint_dirty_paths: Vec::new(), + checkpoint_changed_paths: Vec::new(), + active_frontier_node_ids: Vec::new(), + next_action: "Continue from the bounded checkpoint.".to_string(), + note: "ready".to_string(), + }; + let recommendation = ThreadEpiphanyCrrcRecommendation { + action: ThreadEpiphanyCrrcAction::Continue, + recommended_scene_action: Some(ThreadEpiphanySceneAction::Reorient), + reason: "Pressure is tolerable.".to_string(), + }; + let jobs = vec![ThreadEpiphanyJob { + id: "verification".to_string(), + kind: ThreadEpiphanyJobKind::Verification, + scope: "invariant verification".to_string(), + owner_role: "epiphany-verifier".to_string(), + launcher_job_id: None, + authority_scope: None, + backend_kind: None, + backend_job_id: None, + status: ThreadEpiphanyJobStatus::Needed, + runtime_agent_job_id: None, + items_processed: None, + items_total: None, + progress_note: Some("Evidence needs review.".to_string()), + last_checkpoint_at_unix_seconds: None, + blocking_reason: None, + active_thread_ids: Vec::new(), + linked_subgoal_ids: Vec::new(), + linked_graph_node_ids: Vec::new(), + }]; + + let roles = map_epiphany_roles( + Some(&state), + &jobs, + &decision, + &pressure, + &recommendation, + ThreadEpiphanyReorientResultStatus::MissingBinding, + None, + ); + + let role_ids = roles.iter().map(|role| role.id).collect::>(); + assert_eq!( + role_ids, + vec![ + ThreadEpiphanyRoleId::Implementation, + ThreadEpiphanyRoleId::Modeling, + ThreadEpiphanyRoleId::Verification, + ThreadEpiphanyRoleId::Reorientation, + ] + ); + assert_eq!(roles[0].status, ThreadEpiphanyRoleStatus::Ready); + assert_eq!(roles[1].status, ThreadEpiphanyRoleStatus::Ready); + assert_eq!(roles[2].status, ThreadEpiphanyRoleStatus::Needed); + assert_eq!(roles[2].jobs[0].owner_role, "epiphany-verifier"); + assert_eq!(roles[3].status, ThreadEpiphanyRoleStatus::Ready); + } + #[test] fn reorient_finding_acceptance_builds_scratch_and_checkpoint_update() { let finding = ThreadEpiphanyReorientFinding {