From 24afc4eabfdbb4871dec51a5aefbcc4e2fcd5606 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 21 Jun 2026 18:25:35 +0100 Subject: [PATCH] Add repo live-fire closure verifier --- .../epiphany-repo-livefire-closure-smoke.rs | 299 ++++++++++++++++++ notes/fresh-workspace-handoff.md | 1 + state/ledgers.msgpack | Bin 382702 -> 383132 bytes state/map.yaml | 2 +- 4 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 epiphany-core/src/bin/epiphany-repo-livefire-closure-smoke.rs diff --git a/epiphany-core/src/bin/epiphany-repo-livefire-closure-smoke.rs b/epiphany-core/src/bin/epiphany-repo-livefire-closure-smoke.rs new file mode 100644 index 00000000..af9be675 --- /dev/null +++ b/epiphany-core/src/bin/epiphany-repo-livefire-closure-smoke.rs @@ -0,0 +1,299 @@ +use anyhow::Context; +use anyhow::Result; +use anyhow::anyhow; +use chrono::Utc; +use serde_json::Value; +use serde_json::json; +use std::env; +use std::fs; +use std::path::Path; +use std::path::PathBuf; +use std::process::Command; + +fn main() -> Result<()> { + let args = Args::parse()?; + let result = run_smoke(args)?; + println!("{}", serde_json::to_string_pretty(&result)?); + Ok(()) +} + +#[derive(Debug)] +struct Args { + workspace: PathBuf, + item: String, + expected_path: String, + expected_family: String, + expected_gate: String, + expected_blocker: String, +} + +impl Args { + fn parse() -> Result { + let mut workspace = env::current_dir().context("failed to resolve current directory")?; + let mut item = None; + let mut expected_path = "README.md".to_string(); + let mut expected_family = "repo.status_section".to_string(); + let mut expected_gate = "awaiting-publication".to_string(); + let mut expected_blocker = "bifrost-publication-missing".to_string(); + let mut args = env::args().skip(1).peekable(); + while let Some(arg) = args.next() { + match arg.as_str() { + "--workspace" => workspace = take_path(&mut args, "--workspace")?, + "--item" => item = Some(take_value(&mut args, "--item")?), + "--expected-path" => expected_path = take_value(&mut args, "--expected-path")?, + "--expected-family" => { + expected_family = take_value(&mut args, "--expected-family")? + } + "--expected-gate" => expected_gate = take_value(&mut args, "--expected-gate")?, + "--expected-blocker" => { + expected_blocker = take_value(&mut args, "--expected-blocker")? + } + other => return Err(anyhow!("unexpected argument {other:?}")), + } + } + let item = item.ok_or_else(|| anyhow!("--item is required"))?; + Ok(Self { + workspace, + item, + expected_path, + expected_family, + expected_gate, + expected_blocker, + }) + } +} + +fn run_smoke(args: Args) -> Result { + let workspace = args + .workspace + .canonicalize() + .with_context(|| format!("failed to resolve {}", args.workspace.display()))?; + let work_dir = workspace.join(".epiphany").join("work"); + let close_path = work_dir.join(format!("work-close-{}.json", args.item)); + let overview_path = work_dir.join(format!("work-overview-{}.json", args.item)); + let close = read_json(&close_path)?; + let overview = read_json(&overview_path)?; + + require_str( + &close, + &["schemaVersion"], + "epiphany.repo_work_closure_receipt.v0", + )?; + require_str(&close, &["status"], "closed")?; + require_str(&close, &["item"], &args.item)?; + require_bool(&close, &["privateStateExposed"], false)?; + require_str(&close, &["soul", "verdict"], "passed")?; + require_bool(&close, &["authority", "branchLocalOnly"], true)?; + require_bool(&close, &["authority", "publicationAuthorized"], false)?; + require_bool(&close, &["authority", "mergeAuthorized"], false)?; + require_bool(&close, &["authority", "serviceLifecycleAuthorized"], false)?; + require_bool(&close, &["authority", "crossRepoMutationAuthorized"], false)?; + require_bool(&close, &["authority", "privateStateExposed"], false)?; + require_str( + &close, + &["mind", "repoMapEntry", "safeActionFamily"], + &args.expected_family, + )?; + require_str( + &close, + &["mind", "repoMapEntry", "publicationGate"], + "Bifrost", + )?; + let commit_sha = require_present_str(&close, &["handsReceipts", "commitSha"])?; + require_single_path( + &close, + &["mind", "repoMapEntry", "changedPaths"], + &args.expected_path, + )?; + require_single_path( + &close, + &["closureReview", "sourceGrounding", "actualChangedPaths"], + &args.expected_path, + )?; + require_bool( + &close, + &["closureReview", "sourceGrounding", "pathScopeMatched"], + true, + )?; + require_bool( + &close, + &["closureReview", "sourceGrounding", "familyAssertionsPassed"], + true, + )?; + require_bool( + &close, + &["closureReview", "sourceGrounding", "mindAdoptionPassed"], + true, + )?; + + let overview_schema = require_one_of_str( + &overview, + &["schemaVersion"], + &[ + "epiphany.repo_work_overview.v0", + "epiphany.repo_work_overview_receipt.v0", + ], + )?; + if overview_schema == "epiphany.repo_work_overview.v0" { + require_str(&overview, &["status"], "overview-ready")?; + } + require_str(&overview, &["item"], &args.item)?; + require_str(&overview, &["currentGate"], &args.expected_gate)?; + require_str(&overview, &["blocker"], &args.expected_blocker)?; + require_bool(&overview, &["privateStateExposed"], false)?; + require_str(&overview, &["proofBundle", "soulVerdict"], "passed")?; + require_single_path( + &overview, + &["proofBundle", "changedPaths"], + &args.expected_path, + )?; + + let git_paths = git_changed_paths(&workspace, commit_sha)?; + if git_paths != vec![args.expected_path.clone()] { + return Err(anyhow!( + "expected git commit {commit_sha} to change only {:?}, got {:?}", + args.expected_path, + git_paths + )); + } + + Ok(json!({ + "schemaVersion": "epiphany.repo_livefire_closure_smoke.v0", + "status": "ok", + "workspace": workspace, + "item": args.item, + "branch": string_at(&close, &["mind", "repoMapEntry", "branch"]), + "commitSha": commit_sha, + "safeActionFamily": args.expected_family, + "changedPaths": git_paths, + "currentGate": args.expected_gate, + "blocker": args.expected_blocker, + "publicationGate": "Bifrost", + "soulVerdict": "passed", + "mindStateCommitReceiptId": string_at(&close, &["mind", "stateCommitReceiptId"]), + "closeReceipt": close_path, + "overviewReceipt": overview_path, + "verifiedAt": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true), + "privateStateExposed": false, + })) +} + +fn read_json(path: &Path) -> Result { + let bytes = fs::read(path).with_context(|| format!("failed to read {}", path.display()))?; + serde_json::from_slice(&bytes).with_context(|| format!("failed to parse {}", path.display())) +} + +fn take_value( + args: &mut std::iter::Peekable>, + name: &str, +) -> Result { + args.next() + .ok_or_else(|| anyhow!("missing value for {name}")) +} + +fn take_path( + args: &mut std::iter::Peekable>, + name: &str, +) -> Result { + Ok(PathBuf::from(take_value(args, name)?)) +} + +fn value_at<'a>(value: &'a Value, path: &[&str]) -> Option<&'a Value> { + path.iter() + .try_fold(value, |current, key| current.get(*key)) +} + +fn string_at(value: &Value, path: &[&str]) -> String { + value_at(value, path) + .and_then(Value::as_str) + .unwrap_or("") + .to_string() +} + +fn require_present_str<'a>(value: &'a Value, path: &[&str]) -> Result<&'a str> { + value_at(value, path) + .and_then(Value::as_str) + .filter(|actual| !actual.is_empty()) + .ok_or_else(|| anyhow!("expected {} to be a non-empty string", path.join("."))) +} + +fn require_str(value: &Value, path: &[&str], expected: &str) -> Result<()> { + let actual = string_at(value, path); + if actual == expected { + Ok(()) + } else { + Err(anyhow!( + "expected {} to be {expected:?}, got {actual:?}", + path.join(".") + )) + } +} + +fn require_one_of_str<'a>(value: &Value, path: &[&str], expected: &'a [&str]) -> Result<&'a str> { + let actual = string_at(value, path); + if let Some(matched) = expected.iter().find(|candidate| **candidate == actual) { + Ok(*matched) + } else { + Err(anyhow!( + "expected {} to be one of {:?}, got {actual:?}", + path.join("."), + expected + )) + } +} + +fn require_bool(value: &Value, path: &[&str], expected: bool) -> Result<()> { + let actual = value_at(value, path).and_then(Value::as_bool); + if actual == Some(expected) { + Ok(()) + } else { + Err(anyhow!( + "expected {} to be {expected}, got {:?}", + path.join("."), + actual + )) + } +} + +fn require_single_path(value: &Value, path: &[&str], expected: &str) -> Result<()> { + let paths = value_at(value, path) + .and_then(Value::as_array) + .ok_or_else(|| anyhow!("expected {} to be an array", path.join(".")))? + .iter() + .map(|entry| entry.as_str().unwrap_or("").to_string()) + .collect::>(); + if paths == vec![expected.to_string()] { + Ok(()) + } else { + Err(anyhow!( + "expected {} to contain only {expected:?}, got {:?}", + path.join("."), + paths + )) + } +} + +fn git_changed_paths(workspace: &Path, commit_sha: &str) -> Result> { + let output = Command::new("git") + .arg("-C") + .arg(workspace) + .arg("show") + .arg("--format=") + .arg("--name-only") + .arg(commit_sha) + .output() + .with_context(|| format!("failed to run git show in {}", workspace.display()))?; + if !output.status.success() { + return Err(anyhow!( + "git show failed with exit {:?}: {}", + output.status.code(), + String::from_utf8_lossy(&output.stderr) + )); + } + Ok(String::from_utf8_lossy(&output.stdout) + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .map(|line| line.replace('\\', "/")) + .collect()) +} diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 8058beb3..8caffe41 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -196,6 +196,7 @@ remembers doctrine; the branch remembers the blade. - `daemon-survival-rehearsal` is the bounded Idunn physiology proof. `epiphany-daemon-survival-rehearsal-smoke` writes a durable restart policy for `epiphany-daemon-self`, runs `epiphany-daemon-supervisor serve --max-iterations 2 --force --loop-interval-seconds 0`, verifies two scheduler pulses and the scheduler receipt row, and keeps service-manager mutation/elevation/private-state exposure false. Wrapper proof `tools/epiphany_local_run.ps1 -Mode daemon-survival-rehearsal -SkipBuild` emitted `.epiphany-smoke\daemon-survival-rehearsal-20260621-135723` via artifact `local-20260621-145723-011-1175e273`. - `repo-swarm-mvp-gate` is the compact aggregate MVP proof rite. `epiphany-repo-swarm-mvp-gate-smoke` consumes the latest fresh-repo MVP, repo-work readiness, Bifrost accounting-bundle, Idunn deployment handoff, daemon survival, Weksa interlingua, and Persona semantic memory recall proofs, verifies the green proof rows, and leaves only Idunn elevated service/install/start/remote execution as an explicit warning gate instead of laundering it into completion. Direct proof `.epiphany-smoke\repo-swarm-mvp-gate-20260621-170445` reports `demoReady=true`, `mvpReady=false`, `green=15`, `warnings=1`, `blockers=0`, `personaMemoryRecall.cacheStatus=qdrant-unavailable`, `personaMemoryRecall.status=fallback`, `personaMemoryRecall.heartbeatRecallWired=true`, Projector/Persona/Interpreter prompt coverage, and `privateStateExposed=false`. This is live-fire ready for a controlled repo Body; unattended/elevated production service or deployment execution remains an operator-granted Idunn rite. - Huginn is the first controlled live-fire repo Body past closure. On branch `epiphany/huginn/first-awakening`, item `huginn-inspection-readback-livefire` went through Persona intake, Imagination `repo.status_section` planning, Self run, Mind adoption, Hands README-only commit `e9b07c38fceabbdc89ea35c2156326c80c0393a2`, Soul closure, and Mind map admission. Refreshed overview reports `gate=awaiting-publication`, `blocker=bifrost-publication-missing`, `soulVerdict=passed`, `changedPaths=[README.md]`, and `privateStateExposed=false`; concrete TypeScript build verification passed via `C:\Program Files\nodejs\node.exe E:\Projects\Huginn\node_modules\typescript\bin\tsc -p tsconfig.json`. Do not push, publish, merge, or sync Huginn without Bifrost/maintainer authority. +- `epiphany-repo-livefire-closure-smoke` is the repeatable proof for a controlled repo Body that has reached closure. It checks the close receipt, Mind map entry, overview gate/blocker, authority seals, and actual `git show --name-only` changed paths. Huginn proof with `--workspace E:\Projects\Huginn --item huginn-inspection-readback-livefire --expected-path README.md --expected-family repo.status_section --expected-gate awaiting-publication --expected-blocker bifrost-publication-missing` returned `status=ok`, `soulVerdict=passed`, `publicationGate=Bifrost`, changed paths `[README.md]`, and `privateStateExposed=false`. - `bifrost-ledger` is the compact Bifrost/Imagination readback rite; use it after collaboration, publication, accepted-artifact, or metrics writes to see the latest body-change, public-proof, accepted-artifact, metrics, GitHub, feedback, consensus, credit/review-count, and accounting closure rows without opening the full local Verse. - `tool-directory` is the non-mutating daemon tool sight rite; it shows which globally available daemon-hosted tools any agent can invoke through typed intents and receipts, while still exposing host readiness. - `tool-invoke` now embeds compact readbacks for common daemon-hosted tools. Status calls return `daemonStatusReadback`, Eve calls return `eveConnectionReadback`, Self `service-health` returns Idunn-owned `serviceHealthReadback`, Self `service-policy-directory` returns Idunn-owned `servicePolicyDirectoryReadback`, Self `swarm-online-runbook` returns Idunn-owned `swarmOnlineRunbookReadback`, and Hands/Soul authority tools return `authorityToolReadback` with gate, input contract, receipt contract, intent/receipt routes, host cluster, and private-state seal. This proves global tool ergonomics without moving action, verification, or daemon lifecycle ownership into the wrapper; live proofs routed Persona to Hands `repo-action` (`local-20260619-021628-143-7bf7f2e0`), Soul `verify` (`local-20260619-021701-458-852cfa6f`), Self `swarm-online-runbook` (`local-20260619-032244-273-acb3c6a4`), and Self `service-policy-directory` (`local-20260619-035238-138-633f3ff0`) with `privateStateExposed=false`. diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 27cf9b07e645c9dd9eeee1ab71970377b61b1bb2..5c2c2ee846331fcbd84f0a183d03df953640800a 100644 GIT binary patch delta 353 zcmXwyu}Z^G6oyS}78k*xRTR14q$ITsv_d6F#i~m|rHhKgZE~6%n&ifNZz7pg9Gpc< z90Z?0P#nY;@Chu=zJSl*E$DFg;hg{b&d*c%;iddMr%lzHwd&Ewg#MMiE@`Klc5^i> zTn`JHnsFA@Ouh{Z`r+ry-Mdw_8fLX&TD5L%t!dS7FBj)GE?TY&E`b$w9Dn-Z!KxpKW6gc)#5xa?pQIu;VefhjXx<34$IB|S%2C6cqfpBmDV`-2M zDn6t!fk^VqAWXSZgfrq%39G$;DTo_{CemjdWkv+hK&XYnJd{dHBTs2#Dt)o8ydnuC z5t2^JMA&R#5Ql{!N;|(F=_|8;^}TW1K0au-wmR)f=$_=?+j^-KC%ph2Dv?Dy>b<>* Nea=L_|5#kmD!+D8d_e#J delta 52 zcmbQUQT*Ln@ePuUTn1K#hEc~DSg)Smtjt)*$T*>SDpUJZCPp9z(ab>1vVAHOYvdIG D;M);$ diff --git a/state/map.yaml b/state/map.yaml index 477752e9..95494aac 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -649,5 +649,5 @@ current_status: - `tools/epiphany_local_run.ps1 -Mode swarm-online-runbook` now emits an explicit Idunn preflight summary before the operator elevation boundary. Its JSON and one-line summary report `preflightStatus`, child artifact present/hash-verified/mismatch/missing counts, aggregate service execution failed/missing check counts, child preflight rows with expected/actual SHA-256s, and private-state exposure. Live proof `local-20260619-025627-410-9724e2f6` generated the non-mutating parent artifact with `preflightStatus=ready-for-elevated-operator`, two child runbooks present, two hashes verified, zero mismatches, zero missing artifacts, eleven failed sealed service checks, zero missing checks, explicit aftercare audits, and `privateStateExposed=false`. The remaining scar is still explicit elevated operator execution followed by complete Idunn audits, not missing parent/child artifact readiness. - Service lifecycle receipt history is now event-only: `load_epiphany_cultmesh_daemon_service_lifecycle_receipts` filters out the `epiphany-local/daemon-service-lifecycle-receipt/latest` mirror, while `load_latest_epiphany_cultmesh_daemon_service_lifecycle_receipt` remains the latest-state reader. Focused unit test `service_lifecycle_receipt_history_excludes_latest_mirror` proves two per-receipt writes return exactly two history records and latest points at the second. Fresh smoke `verse-query-lifecycle-history-filter-smoke` and live overview/triage `local-20260618-145632-497-efac6f0d` / `local-20260618-145632-321-867947af` prove compact swarm sights still expose service lifecycle attention plus the present elevated runbook artifact without daemon pokes or service mutation. Do not use a broad unscoped cargo test filter for this check on Windows; it fans out across bin harnesses and hit a broken pipe during this pass. - `epiphany-verse-query seed-compact` is the compact seed receipt for wrappers and read-only rites. The older `seed` command still prints the full local Verse context for explicit debugging, but compact wrapper modes now avoid full `seed`/final `query` readback after the combined overview proof exposed Windows debug stack overflow from full-context serialization pressure. - last_completed_repo_swarm_cut: Huginn live-fire has crossed the first real repo Body from Persona pressure to branch-local Hands commit and Soul/Mind closure. On `E:\Projects\Huginn` branch `epiphany/huginn/first-awakening`, `tools/epiphany_local_run.ps1 -Mode repo-persona-intake` accepted item `huginn-inspection-readback-livefire`, Weksa/Persona intake remained sealed, and `epiphany-work derive-plan` lowered it to model-authored safe family `repo.status_section` for `README.md` only. `tools/epiphany_local_run.ps1 -Mode repo-swarm-run` advanced four bounded pulses: run-from-plan, adopt-from-plan, execute-from-plan, and close-from-execute. Huginn commit `e9b07c38fceabbdc89ea35c2156326c80c0393a2` updated only `README.md`; closure receipt `.epiphany\work\work-close-huginn-inspection-readback-livefire.json` records status `closed`, Soul verdict `passed`, Mind state commit `repo-work-close-huginn-inspection-readback-livefire-mind-commit`, safe family `repo.status_section`, path scope `README.md`, Bifrost publication gate, and `privateStateExposed=false`. Verification also ran Huginn's TypeScript compiler through concrete Node path: `C:\Program Files\nodejs\node.exe E:\Projects\Huginn\node_modules\typescript\bin\tsc -p tsconfig.json` passed. Refreshed overview reports gate `awaiting-publication`, blocker `bifrost-publication-missing`, artifacts present through close, and no publication/sync yet. + last_completed_repo_swarm_cut: Huginn live-fire has crossed the first real repo Body from Persona pressure to branch-local Hands commit and Soul/Mind closure, and now has a repeatable verifier. On `E:\Projects\Huginn` branch `epiphany/huginn/first-awakening`, `tools/epiphany_local_run.ps1 -Mode repo-persona-intake` accepted item `huginn-inspection-readback-livefire`, Weksa/Persona intake remained sealed, and `epiphany-work derive-plan` lowered it to model-authored safe family `repo.status_section` for `README.md` only. `tools/epiphany_local_run.ps1 -Mode repo-swarm-run` advanced four bounded pulses: run-from-plan, adopt-from-plan, execute-from-plan, and close-from-execute. Huginn commit `e9b07c38fceabbdc89ea35c2156326c80c0393a2` updated only `README.md`; closure receipt `.epiphany\work\work-close-huginn-inspection-readback-livefire.json` records status `closed`, Soul verdict `passed`, Mind state commit `repo-work-close-huginn-inspection-readback-livefire-mind-commit`, safe family `repo.status_section`, path scope `README.md`, Bifrost publication gate, and `privateStateExposed=false`. Verification also ran Huginn's TypeScript compiler through concrete Node path: `C:\Program Files\nodejs\node.exe E:\Projects\Huginn\node_modules\typescript\bin\tsc -p tsconfig.json` passed. New native verifier `epiphany-repo-livefire-closure-smoke --workspace E:\Projects\Huginn --item huginn-inspection-readback-livefire --expected-path README.md --expected-family repo.status_section --expected-gate awaiting-publication --expected-blocker bifrost-publication-missing` checks closure receipt, Mind map entry, overview gate/blocker, authority seals, and `git show` changed paths, returning `status=ok`, `soulVerdict=passed`, `publicationGate=Bifrost`, and `privateStateExposed=false`. next_action: Continue repo-swarm MVP work from `notes/epiphany-repo-swarm-mvp-contract.md`. Huginn is now live-fire proven through branch-local closure and is awaiting Bifrost publication/upstream-main sync; do not push, publish, merge, or sync that branch without explicit Bifrost/maintainer authority. Useful next cuts are Bifrost publication proof for Huginn's closed item, controlled upstream sync only after publication authority, explicit elevated/operator Idunn execution proof only when the human grants that authority, or another repo-owned safe action family only if it materially improves the MVP story. Persona-to-Imagination consensus readback, Mind-owned interpreter brief cargo, safe-family planning depth and closure, Gjallar closure/readiness/readiness-review rows, receipt-directory readiness-review indexing, Bifrost ledger accounting rows including readiness-review closure plus artifact/metrics final receipt closure, readiness wrapper mouth, repo deployment handoff/readback wrappers, readiness deployment-aftercare closure, readiness Idunn/tool-directory receipt validation, readiness Bifrost/GitHub publication-chain validation, readiness upstream-main sync validation, readiness review/adoption receipt validation, aggregate fresh-repo/readiness/Bifrost/deployment-handoff/daemon-survival/Weksa/Persona-memory MVP gate reporting, bounded Idunn daemon survival rehearsal, and Huginn live-fire branch-local closure are sealed; preserve Idunn service lifecycle ownership, Bifrost publication/upstream-main ownership, Mind durable-state admission, and existing CultMesh/Odin/Eve tool directories. Windows service install/start/deployment execution still requires explicit elevated operator authority.