From b83c2fb5bccfea087d8b4c0fa2b71eeed80d5f65 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 13 Jun 2026 06:17:24 +0100 Subject: [PATCH] Seal work-loop receipt failures --- .../src/bin/epiphany-hands-action.rs | 61 +++++++++++++++++- epiphany-core/src/surfaces/coordinator.rs | 29 +++++++++ .../src/bin/epiphany-openai-runtime.rs | 28 +++++++- notes/fresh-workspace-handoff.md | 1 + state/ledgers.msgpack | Bin 181606 -> 181972 bytes state/map.yaml | 2 +- 6 files changed, 117 insertions(+), 4 deletions(-) diff --git a/epiphany-core/src/bin/epiphany-hands-action.rs b/epiphany-core/src/bin/epiphany-hands-action.rs index 7ee3f079..d0e3a4b4 100644 --- a/epiphany-core/src/bin/epiphany-hands-action.rs +++ b/epiphany-core/src/bin/epiphany-hands-action.rs @@ -75,6 +75,7 @@ struct RecordCommitArgs { commit_sha: String, branch: String, changed_paths: Vec, + validate_commit_sha: bool, } #[derive(Debug)] @@ -87,6 +88,7 @@ struct RecordPassArgs { commit_sha: String, branch: String, changed_paths: Vec, + validate_commit_sha: bool, } impl Args { @@ -157,6 +159,7 @@ fn parse_record_commit(tokens: impl Iterator) -> Result) -> Result Result Result { let (intent, review) = load_gate(store, &args.gate, "commit")?; validate_paths_within_gate(&intent, &args.changed_paths)?; + let commit_sha = if args.validate_commit_sha { + resolve_git_commit_sha(&args.commit_sha)? + } else { + args.commit_sha + }; let receipt_id = args .gate .receipt_id @@ -333,7 +342,7 @@ fn record_commit(store: &PathBuf, args: RecordCommitArgs) -> Result Result String { path.trim().replace('\\', "/") } +fn resolve_git_commit_sha(commit_sha: &str) -> Result { + let output = std::process::Command::new("git") + .arg("rev-parse") + .arg("--verify") + .arg(format!("{commit_sha}^{{commit}}")) + .output() + .map_err(|err| anyhow!("failed to start git rev-parse for Hands commit receipt: {err}"))?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + return Err(anyhow!( + "Hands commit receipt references non-existent commit {commit_sha:?}: {stderr}" + )); + } + let resolved = String::from_utf8(output.stdout) + .map_err(|err| anyhow!("git rev-parse returned non-UTF8 commit sha: {err}"))? + .trim() + .to_string(); + if resolved.is_empty() { + return Err(anyhow!( + "git rev-parse returned an empty commit sha for {commit_sha:?}" + )); + } + Ok(resolved) +} + fn generated_receipt_id(prefix: &str) -> String { format!("{prefix}-{}", uuid::Uuid::new_v4()) } @@ -582,6 +617,7 @@ mod tests { commit_sha: "abc123".to_string(), branch: "codex/test".to_string(), changed_paths: vec!["src/lib.rs".to_string()], + validate_commit_sha: false, }, )?; @@ -626,6 +662,7 @@ mod tests { commit_sha: "def456".to_string(), branch: "codex/test".to_string(), changed_paths: vec!["src/lib.rs".to_string()], + validate_commit_sha: false, }, )?; @@ -663,6 +700,28 @@ mod tests { Ok(()) } + #[test] + fn refuses_commit_receipt_for_unknown_git_sha() -> Result<()> { + let temp = tempfile::tempdir()?; + let store = temp.path().join("runtime-spine.msgpack"); + seed_gate(&store, vec!["src".to_string()])?; + + let result = record_commit( + &store, + RecordCommitArgs { + gate: gate_args("hands-commit-test"), + commit_sha: "0000000000000000000000000000000000000000".to_string(), + branch: "codex/test".to_string(), + changed_paths: vec!["src/lib.rs".to_string()], + validate_commit_sha: true, + }, + ); + + assert!(result.is_err()); + assert!(runtime_hands_commit_receipt(&store, "hands-commit-test")?.is_none()); + Ok(()) + } + #[test] fn refuses_patch_receipt_outside_requested_paths() -> Result<()> { let temp = tempfile::tempdir()?; diff --git a/epiphany-core/src/surfaces/coordinator.rs b/epiphany-core/src/surfaces/coordinator.rs index 4a93e498..618f1669 100644 --- a/epiphany-core/src/surfaces/coordinator.rs +++ b/epiphany-core/src/surfaces/coordinator.rs @@ -815,6 +815,19 @@ pub fn recommend_coordinator_action( ); } + if input.signals.verification_result_status == EpiphanyCoordinatorRoleResultStatus::Failed + && !input.verification_result_accepted + { + return build( + EpiphanyCoordinatorAction::ReviewVerificationResult, + Some(EpiphanyCoordinatorRoleId::Verification), + Some(EpiphanyCoordinatorSceneAction::RoleResult), + false, + false, + "The verification/review worker failed; inspect the failed Soul result before relaunching verification or continuing implementation.", + ); + } + if input.verification_result_accepted && input.implementation_evidence_after_verification { return build( EpiphanyCoordinatorAction::LaunchVerification, @@ -1471,6 +1484,22 @@ mod tests { EpiphanyCoordinatorAction::LaunchVerification ); + let review_failed_verification = recommend_coordinator_action(EpiphanyCoordinatorInput { + signals: EpiphanyCoordinatorSignals { + research_result_status: EpiphanyCoordinatorRoleResultStatus::MissingBinding, + modeling_result_status: EpiphanyCoordinatorRoleResultStatus::Completed, + verification_result_status: EpiphanyCoordinatorRoleResultStatus::Failed, + }, + modeling_result_accepted: true, + modeling_result_reviewable: true, + ..input() + }); + assert_eq!( + review_failed_verification.action, + EpiphanyCoordinatorAction::ReviewVerificationResult + ); + assert!(!review_failed_verification.can_auto_run); + let verification_done = EpiphanyCoordinatorSignals { research_result_status: EpiphanyCoordinatorRoleResultStatus::MissingBinding, modeling_result_status: EpiphanyCoordinatorRoleResultStatus::Completed, diff --git a/epiphany-openai-runtime/src/bin/epiphany-openai-runtime.rs b/epiphany-openai-runtime/src/bin/epiphany-openai-runtime.rs index 22bfe2db..efc1b122 100644 --- a/epiphany-openai-runtime/src/bin/epiphany-openai-runtime.rs +++ b/epiphany-openai-runtime/src/bin/epiphany-openai-runtime.rs @@ -596,11 +596,35 @@ async fn run_worker_launch_with_tool_continuation( } if !openai_summary.tool_intent_ids.is_empty() { - return Err(anyhow!( + let summary = format!( "worker {} still requested tools after {} automatic tool rounds", launch_request.job_id, options.max_tool_rounds - )); + ); + let result = fail_worker_job( + &options.store_path, + &launch_request.job_id, + summary.clone(), + "Inspect the worker request, tool receipts, and model/tool loop before relaunching." + .to_string(), + )?; + return Ok(json!({ + "status": "tool-round-limit", + "store": options.store_path, + "jobId": launch_request.job_id, + "bindingId": launch_request.binding_id, + "role": launch_request.role, + "requestId": current_request_id, + "openaiResultId": openai_summary.result_id, + "openaiVerdict": openai_summary.verdict, + "openaiSummary": openai_summary.summary, + "workerResultId": result.result_id, + "verdict": result.verdict, + "summary": summary, + "nextSafeMove": result.next_safe_move, + "pendingToolIntentIds": openai_summary.tool_intent_ids, + "toolRounds": tool_rounds, + })); } let assistant_text = diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 711d2f21..b86e14de 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -36,6 +36,7 @@ remembers doctrine; the branch remembers the blade. - Canonical anatomy now lives in `notes/epiphany-anatomy.md`. Embodied sub-agents are Self, Face, Imagination, Eyes, Proprioception, Hands, and Soul. Body, Mind, Continuity, and Substrate Gate are substrate/state/protocol surfaces, not sub-agent identities. State is split by layer: `work_organ` for resident Epiphany sub-agents, `persona` for portable public/person-shaped state shared by Epiphany Face, VoidBot repo Faces, and Ghostlight characters, and heartbeat/swarm state for scheduling and coordination physiology. Persona state now carries provenance, required public presentation, `date-time` timestamps, typed `candidateActions`, strict anchored thoughts with non-authoritative extensions, custom enum companion labels, and typed affect records for bonds/status reads/doctrine stances. - The repo now has a one-command local operator path: `.\tools\epiphany_local_run.ps1`. Default mode is `smoke`; it builds the retained Codex app-server compatibility edge, builds native Epiphany operator binaries, runs the coordinator smoke, and writes launcher artifacts under `.epiphany-run/` plus coordinator artifacts under `.epiphany-dogfood/`. `-Mode status` is now Epiphany-native: it builds only Epiphany operator binaries, reads `state/thread-state.msgpack` when present, derives status through `epiphany-core`, and does not start or require `codex-app-server`. Use `-Mode plan` or `-Mode run -MaxSteps 4` for the raw bridge-equipped coordinator entrypoints; use `-Mode mvp -FaceInput "" -MaxSteps 4` for the local product cycle that projects Face's character turn, writes a Face/Aquarium bubble, bootstraps a minimal local checkpoint for fresh threads, runs the coordinator with auto-tools, then runs heartbeat sleep/dream maintenance. Live plan/run/mvp modes use the workspace `state/runtime-spine.msgpack`, because CodexThread launch writes worker requests there; the run bundle remains artifact evidence, not runtime lifecycle authority. Add `-ThreadId ` and `-Workspace ` to inspect or continue an existing thread. `status`, `plan`, and `smoke` do not spend model calls; `run` and `mvp` build and use `epiphany-openai-runtime`. Coordinator workers now launch detached and are polled through runtime-spine instead of blocking the coordinator. MVP runs are non-ephemeral by default so `-ThreadId ` can resume and poll later. `-MaxRuntimeSeconds ` is passed into worker runtime; timeout writes a failed worker receipt and exits. Failed modeling now blocks downstream verification until the failed result is reviewed. - Current Mom-ready status: the normal MVP pass reaches Face -> coordinator -> detached runtime -> heartbeat sleep, and modeling workers now complete through the ChatGPT subscription transport. The first failure was a stack of violated observability/runtime invariants: worker prompts prepended the whole shared persistent-memory rite, Responses frame liveness was invisible until completion, and the runtime tried to persist hundreds of text deltas before completing the worker job. Role/reorient workers now get a compact Epiphany worker boundary instead of full persistent memory; runtime-spine records sampled raw Responses frame kinds with small previews; contiguous reasoning/text deltas compact before durable storage; and live proofs completed `2fe38dcd-d99e-4c73-9a58-afcc81f851c7`, `241e3851-e3ea-4877-a63b-0d2bbd96a7e2`, and `85149a26-6975-403f-ac93-092b717c5825` instead of timing out. The second failure was stale lifecycle authority: completed runtime jobs remained as unresolved `runtime_links`, replacement launches treated historical active links as current, and one bad validation path opened an orphan job before rejecting the state patch. Latest-link-wins is now the runtime-link invariant, terminal links project completed jobs, stale active history cannot veto replacement, launch validation happens before opening runtime jobs, and completed-but-unreviewable modeling findings stop for review instead of relaunching the same lane. The Eyes/source-gather fork is now landed: Research is a fixed Eyes lane with launch/result/accept/status/coordinator/protocol support, and `regather-needed` modeling findings route to `LaunchResearch` instead of relaunching blind Proprioception. Research statePatch admission is Eyes-shaped only: observations, evidence, scratch, and optional investigationCheckpoint; no graph, planning, objective, or checkpoint authority. Mind admission now leaves typed runtime-spine proof: role/reorient acceptance persists `epiphany.mind.gateway_review` before state admission and `epiphany.mind.state_commit_receipt` after admission with the resulting durable revision. Launch contracts now split the broad receipt catalogue from effect-specific proof profiles for state admission, evidence promotion, repo action, verification, and continuity recovery. Research evidence promotion enforces Substrate Gate read/snapshot grant, Eyes evidence packet, and Mind review before state admission; Verification acceptance enforces Soul verdict and Mind review; Reorient acceptance enforces Continuity recovery receipt and Mind review. `epiphany-mvp-coordinator` now records a typed Substrate Gate grant plus Hands intent/review gate when the coordinator selects `continueImplementation`, including a structured `recordPassCommand` template; `epiphany-hands-action` records patch/command/commit receipts against that gate and refuses mismatched, unapproved, or out-of-scope consequences. `epiphany-mvp-coordinator-smoke` now proves the deterministic path: forced `continueImplementation`, gate emission, and `record-pass` receipts in the smoke runtime-spine store. Live run `local-20260612-133920` proved a receipt-only Verification `needs-evidence` finding can be accepted through `roleAccept` and advance to a Hands gate without granting Soul statePatch authority. Runtime-spine now detects and summarizes a complete post-verification Hands patch/command/commit receipt chain so the coordinator can route Soul after Hands. Verification launches now write that chain into the internal CultMesh Verse as `epiphany.cultmesh.work_loop_telemetry.v0` and render a Soul packet from it; Modeling launches read the latest packet, attach accepted Soul receipt ids, and render the Proprioception packet from the same typed state. The telemetry now carries resolved receipt payload previews, stdout/stderr artifact previews, commit diff preview, source proof notes, and verification assertions. The corrected work loop is Hands -> Soul -> Proprioception -> Hands: Soul verifies concrete consequence receipts and changed source, then Proprioception updates the machine model from verified consequence before another implementation turn. +- Fresh scar from the receipt proof dogfood: `epiphany-model-runtime run-worker --auto-tools` must seal the outer runtime job when the model still requests tools after the configured round limit; a failed Soul/verification result is a real lane result and must stop for review instead of being treated as no current finding; and `epiphany-hands-action` must resolve commit receipts through `git rev-parse --verify` before storing them, because an almost-correct SHA poisoned Soul's `git show` proof and made the receipt chain untrustworthy. - The investment dossier readiness target is now explicit: `E:\Projects\gamecult-site\docs\gamecult_integrated_dossier.tex` expects Epiphany to be part of a Bifrost-first labor loop, not only a local Mom demo. The external proof bar is: Bifrost owns topics/work items/dispatch packets/receipts/credit/reward pressure; Epiphany executes bounded work; maintainers review artifacts; public/operator evidence records scope, consent, roles, model spend, review load, artifacts, accepted/rejected outcome, memory lessons, and case-study permission; and private worker/operator/agent context stays sealed. Diligence also asks for a live fresh-repo Epiphany demo, Bifrost work item/topic/agent transport/receipt/ledger demo, CultMesh status, VoidBot/Discord swarm demo, compute/output metrics, and security model for secrets/write permissions/public-private exports. - This Codex operator's own roadmap/todo posture has been retuned for this scale target, with the Epiphany Imagination specialist as an optional sub-agent rail rather than the whole point. `notes/epiphany-investor-readiness-roadmap.md` now says: make the local Mom MVP a repeatable proof bundle; build the Bifrost-first external-work bridge; add compute/review/accepted-artifact accounting; prove fresh-repo operation without supervisor contamination; keep public/operator-safe exports sealed; and treat angel capital as fuel for capacity, not command authority. The Imagination specialist prompt was also updated so investor-scale Objective Drafts must name Bifrost provenance, bounded Epiphany proof, maintainer review, receipts/ledger evidence, compute/output metrics, public/private export safety, consent, and legal/security gates. - Local operator runs now write typed CultMesh intent/receipt documents to `.epiphany-run/cultmesh/operator-runs.ccmp` through `epiphany-operator-run`. The wrapper writes intent before acting and a completion receipt after success; this is the local consent/trace rail, not a hidden scheduler queue. diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 4f6e9dba57c4346fd8a613adecf3a37fc16d1cb9..b02d127e9873ccab742208c41ee32f59b6f39759 100644 GIT binary patch delta 385 zcmXw!F-rqM5QV*~#9(1#7Z|WUH3S1DAOtMZSjEb2b2oRly4g8*W-m#V!a@+NB=|QX z7S;;d3;qBre}{X5%@l9ueeccBUF++nO z^s2P!1ELLkWjQd7G947%7`Eq%@ss337_M zY5>S&>VPN%swkld%L(Q*bO;qX)`4eJ1g5~QmC+fDN5jBT){;O)n!|rh!N!_!89uEX HQNI5R)ftRB delta 43 zcmcc8%l)j2dxIDwuZfkBg_Wsg)G-F8BhH(Z84DR11)8TawNGVY+&-0w>Ea;(Fv$(u diff --git a/state/map.yaml b/state/map.yaml index edcfa8fd..2ba6bc7f 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -592,4 +592,4 @@ current_status: cargo and rejects modeling/planning/graph/checkpoint authority. MVP status and coordinator smokes pass with six lanes, and the smoke harness now rebuilds stale coordinator/app-server binaries before using them. - next_action: Exercise the corrected Hands -> Soul -> Proprioception loop: record the current bounded edit/check/commit against the latest Hands gate, rerun the coordinator so Soul verifies the Hands receipts, accept the Soul result when reviewable, then confirm the next routed stage is Proprioception/modeling before another Hands turn. If the run exposes split-brained state admission instead, collapse Mind review/state mutation/commit into one storage-owner primitive. + next_action: Commit the runtime/Soul/Hands receipt integrity fixes, record the resulting pass with `epiphany-hands-action` so the commit SHA is git-validated, rerun the coordinator so Soul reviews the clean Hands receipts, accept the Soul result when reviewable, then confirm the next routed stage is Proprioception/modeling before another Hands turn.