From f780984b9aa84d2b863146c4ea25e828141a5ac6 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 20 Jun 2026 19:19:06 +0100 Subject: [PATCH] Prove repo swarm stop classification cases --- ...piphany-swarm-stop-classification-smoke.rs | 393 ++++++++++++++++++ epiphany-core/src/bin/epiphany-swarm.rs | 97 +++-- notes/epiphany-repo-swarm-mvp-contract.md | 15 +- notes/fresh-workspace-handoff.md | 2 +- state/ledgers.msgpack | Bin 346672 -> 347147 bytes state/map.yaml | 4 +- 6 files changed, 475 insertions(+), 36 deletions(-) create mode 100644 epiphany-core/src/bin/epiphany-swarm-stop-classification-smoke.rs diff --git a/epiphany-core/src/bin/epiphany-swarm-stop-classification-smoke.rs b/epiphany-core/src/bin/epiphany-swarm-stop-classification-smoke.rs new file mode 100644 index 00000000..60cb6d8d --- /dev/null +++ b/epiphany-core/src/bin/epiphany-swarm-stop-classification-smoke.rs @@ -0,0 +1,393 @@ +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(Clone, Debug)] +struct Args { + root: PathBuf, + smoke_root: PathBuf, +} + +impl Args { + fn parse() -> Result { + let mut root = env::current_dir().context("failed to resolve current directory")?; + let mut smoke_root = root.join(".epiphany-smoke"); + let mut args = env::args().skip(1).peekable(); + while let Some(arg) = args.next() { + match arg.as_str() { + "--root" => root = take_path(&mut args, "--root")?, + "--smoke-root" => smoke_root = take_path(&mut args, "--smoke-root")?, + other => return Err(anyhow!("unexpected argument {other:?}")), + } + } + Ok(Self { root, smoke_root }) + } +} + +fn run_smoke(args: Args) -> Result { + let root = args + .root + .canonicalize() + .with_context(|| format!("failed to resolve {}", args.root.display()))?; + let manifest = root.join("epiphany-core").join("Cargo.toml"); + if !manifest.exists() { + return Err(anyhow!( + "could not find epiphany-core manifest at {}", + manifest.display() + )); + } + fs::create_dir_all(&args.smoke_root) + .with_context(|| format!("failed to create {}", args.smoke_root.display()))?; + let stamp = Utc::now().format("%Y%m%d-%H%M%S").to_string(); + let smoke_dir = args + .smoke_root + .join(format!("swarm-stop-classification-{stamp}")); + if smoke_dir.exists() { + fs::remove_dir_all(&smoke_dir) + .with_context(|| format!("failed to clear {}", smoke_dir.display()))?; + } + fs::create_dir_all(&smoke_dir) + .with_context(|| format!("failed to create {}", smoke_dir.display()))?; + + let repo = smoke_dir.join("repo-body"); + fs::create_dir_all(&repo).with_context(|| format!("failed to create {}", repo.display()))?; + git(["init"], &repo)?; + git( + ["config", "user.email", "epiphany-smoke@example.invalid"], + &repo, + )?; + git(["config", "user.name", "Epiphany Smoke"], &repo)?; + fs::write( + repo.join("README.md"), + "# Swarm Stop Classification Smoke\n\nThis repository proves typed repo-swarm run stop classification.\n", + ) + .with_context(|| format!("failed to seed {}", repo.join("README.md").display()))?; + git(["add", "README.md"], &repo)?; + git( + ["commit", "-m", "Seed swarm stop classification smoke"], + &repo, + )?; + + let runtime_id = "swarm-stop-classification-smoke"; + let local_verse = repo.join(".epiphany").join("local-verse.ccmp"); + cargo_json( + &manifest, + "epiphany-repo", + &[ + "init", + "--workspace", + path_str(&repo)?, + "--epiphany-root", + path_str(&root)?, + "--swarm-id", + runtime_id, + "--topic", + "swarm-stop-classification", + ], + &root, + )?; + cargo_json( + &manifest, + "epiphany-swarm", + &[ + "online", + "--workspace", + path_str(&repo)?, + "--epiphany-root", + path_str(&root)?, + "--runtime-id", + runtime_id, + "--local-verse-store", + path_str(&local_verse)?, + ], + &root, + )?; + + let empty_run = cargo_json( + &manifest, + "epiphany-swarm", + &[ + "run", + "--workspace", + path_str(&repo)?, + "--epiphany-root", + path_str(&root)?, + "--runtime-id", + runtime_id, + "--local-verse-store", + path_str(&local_verse)?, + "--max-iterations", + "1", + "--max-items", + "1", + "--cooldown-seconds", + "0", + ], + &root, + )?; + require_eq( + &empty_run, + &["stopClassification", "schemaVersion"], + "epiphany.repo_swarm_run_stop_classification.v0", + )?; + require_eq( + &empty_run, + &["stopClassification", "category"], + "queue-empty", + )?; + require_eq(&empty_run, &["stopClassification", "owner"], "Gjallar")?; + require_eq( + &empty_run, + &["stopClassification", "authorityGate"], + "repo.work.overview", + )?; + require_eq( + &empty_run, + &["stopClassification", "blocker"], + "no-repo-work-rows", + )?; + require_bool(&empty_run, &["stopClassification", "mutatesState"], false)?; + require_bool( + &empty_run, + &["stopClassification", "requiresElevatedAuthority"], + false, + )?; + require_bool( + &empty_run, + &["stopClassification", "privateStateExposed"], + false, + )?; + + let item = "dry-run-preview"; + cargo_json( + &manifest, + "epiphany-work", + &[ + "accept", + "--workspace", + path_str(&repo)?, + "--from", + "persona", + "--item", + item, + "--summary", + "Prove a dry-run swarm pulse can classify a ready-to-run queue row without mutation.", + "--candidate-action-ref", + "candidate-action://swarm-stop-classification/dry-run-preview", + "--public-discussion-ref", + "eve://epiphany/persona/swarm-stop-classification", + ], + &root, + )?; + cargo_json( + &manifest, + "epiphany-work", + &[ + "derive-plan", + "--workspace", + path_str(&repo)?, + "--item", + item, + "--action-family", + "planning-note", + "--model-ref", + "swarm-stop-classification-smoke-imagination-v1", + "--model-authored", + ], + &root, + )?; + cargo_json( + &manifest, + "epiphany-work", + &["overview", "--workspace", path_str(&repo)?, "--item", item], + &root, + )?; + let dry_run = cargo_json( + &manifest, + "epiphany-swarm", + &[ + "run", + "--workspace", + path_str(&repo)?, + "--epiphany-root", + path_str(&root)?, + "--runtime-id", + runtime_id, + "--local-verse-store", + path_str(&local_verse)?, + "--max-iterations", + "1", + "--max-items", + "1", + "--cooldown-seconds", + "0", + "--dry-run", + ], + &root, + )?; + require_eq( + &dry_run, + &["stopClassification", "schemaVersion"], + "epiphany.repo_swarm_run_stop_classification.v0", + )?; + require_eq( + &dry_run, + &["stopClassification", "category"], + "dry-run-preview", + )?; + require_eq(&dry_run, &["stopClassification", "owner"], "Self")?; + require_eq( + &dry_run, + &["stopClassification", "authorityGate"], + "ready-to-run", + )?; + require_eq(&dry_run, &["stopClassification", "selectedItem"], item)?; + require_bool(&dry_run, &["stopClassification", "mutatesState"], false)?; + require_bool( + &dry_run, + &["stopClassification", "requiresElevatedAuthority"], + false, + )?; + require_bool( + &dry_run, + &["stopClassification", "privateStateExposed"], + false, + )?; + + let summary = json!({ + "schemaVersion": "epiphany.repo_swarm_stop_classification_smoke.v0", + "status": "ok", + "smokeDir": smoke_dir, + "repo": repo, + "emptyStopCategory": empty_run["stopClassification"]["category"], + "emptyStopOwner": empty_run["stopClassification"]["owner"], + "emptyStopGate": empty_run["stopClassification"]["authorityGate"], + "dryRunStopCategory": dry_run["stopClassification"]["category"], + "dryRunStopOwner": dry_run["stopClassification"]["owner"], + "dryRunStopGate": dry_run["stopClassification"]["authorityGate"], + "dryRunSelectedItem": dry_run["stopClassification"]["selectedItem"], + "mutatesState": false, + "requiresElevatedAuthority": false, + "privateStateExposed": false, + }); + write_json(&smoke_dir.join("summary.json"), &summary)?; + Ok(summary) +} + +fn cargo_json(manifest: &Path, bin: &str, args: &[&str], cwd: &Path) -> Result { + let output = Command::new("cargo") + .arg("run") + .arg("--quiet") + .arg("--manifest-path") + .arg(manifest) + .arg("--bin") + .arg(bin) + .arg("--") + .args(args) + .current_dir(cwd) + .output() + .with_context(|| format!("failed to run cargo bin {bin}"))?; + if !output.status.success() { + return Err(anyhow!( + "{bin} failed with status {:?}\nstdout:\n{}\nstderr:\n{}", + output.status.code(), + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + )); + } + serde_json::from_slice(&output.stdout).with_context(|| { + format!( + "{bin} did not emit JSON on stdout:\n{}", + String::from_utf8_lossy(&output.stdout) + ) + }) +} + +fn git(args: [&str; N], cwd: &Path) -> Result<()> { + let output = Command::new("git") + .args(args) + .current_dir(cwd) + .output() + .with_context(|| format!("failed to run git in {}", cwd.display()))?; + if output.status.success() { + Ok(()) + } else { + Err(anyhow!( + "git failed in {} with status {:?}\nstdout:\n{}\nstderr:\n{}", + cwd.display(), + output.status.code(), + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + )) + } +} + +fn write_json(path: &Path, value: &Value) -> Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("failed to create {}", parent.display()))?; + } + fs::write(path, serde_json::to_vec_pretty(value)?) + .with_context(|| format!("failed to write {}", path.display())) +} + +fn require_eq(value: &Value, path: &[&str], expected: &str) -> Result<()> { + let actual = path + .iter() + .try_fold(value, |current, key| current.get(*key)) + .and_then(Value::as_str) + .unwrap_or(""); + if actual == expected { + Ok(()) + } else { + Err(anyhow!( + "expected {} to be {expected:?}, got {actual:?}", + path.join(".") + )) + } +} + +fn require_bool(value: &Value, path: &[&str], expected: bool) -> Result<()> { + let actual = path + .iter() + .try_fold(value, |current, key| current.get(*key)) + .and_then(Value::as_bool); + if actual == Some(expected) { + Ok(()) + } else { + Err(anyhow!( + "expected {} to be {expected}, got {:?}", + path.join("."), + actual + )) + } +} + +fn take_path( + args: &mut std::iter::Peekable>, + flag: &str, +) -> Result { + args.next() + .map(PathBuf::from) + .with_context(|| format!("missing value for {flag}")) +} + +fn path_str(path: &Path) -> Result<&str> { + path.to_str() + .ok_or_else(|| anyhow!("path is not valid UTF-8: {}", path.display())) +} diff --git a/epiphany-core/src/bin/epiphany-swarm.rs b/epiphany-core/src/bin/epiphany-swarm.rs index 46110238..47f68e91 100644 --- a/epiphany-core/src/bin/epiphany-swarm.rs +++ b/epiphany-core/src/bin/epiphany-swarm.rs @@ -543,34 +543,54 @@ fn classify_run_stop( .and_then(|iteration| iteration.get("selectedRows")) .and_then(Value::as_array) .and_then(|rows| rows.first()); - let selected_gate = selected_row - .and_then(|row| row.get("gate")) - .and_then(Value::as_str) - .unwrap_or("none"); - let selected_blocker = selected_row - .and_then(|row| row.get("blocker")) - .and_then(Value::as_str) - .unwrap_or("none"); - let selected_next_safe_move = selected_row - .and_then(|row| row.get("nextSafeMove")) - .and_then(Value::as_str) - .unwrap_or(next_safe_move); - let selected_item = selected_row - .and_then(|row| row.get("item")) - .and_then(Value::as_str) - .unwrap_or("none"); - let selected_branch = selected_row - .and_then(|row| row.get("branch")) - .and_then(Value::as_str) - .unwrap_or("none"); + let selected_output = final_iteration + .and_then(|iteration| iteration.get("outputs")) + .and_then(Value::as_array) + .and_then(|outputs| outputs.first()); + let refreshed_overview = selected_output.and_then(|output| output.get("refreshedOverview")); + let selected_gate = selected_output + .and_then(|output| non_empty_string(output.get("gateBefore"))) + .or_else(|| { + refreshed_overview.and_then(|overview| non_empty_string(overview.get("currentGate"))) + }) + .or_else(|| selected_row.and_then(|row| non_empty_string(row.get("gate")))) + .or_else(|| compact_row_field(selected_row, "gate")) + .unwrap_or_else(|| "none".to_string()); + let selected_blocker = selected_output + .and_then(|output| non_empty_string(output.get("blockerBefore"))) + .or_else(|| { + refreshed_overview.and_then(|overview| non_empty_string(overview.get("blocker"))) + }) + .or_else(|| selected_row.and_then(|row| non_empty_string(row.get("blocker")))) + .or_else(|| compact_row_field(selected_row, "blocker")) + .unwrap_or_else(|| "none".to_string()); + let selected_next_safe_move = selected_output + .and_then(|output| output.get("tick")) + .and_then(|tick| non_empty_string(tick.get("nextSafeMove"))) + .or_else(|| { + refreshed_overview.and_then(|overview| non_empty_string(overview.get("nextSafeMove"))) + }) + .or_else(|| selected_row.and_then(|row| non_empty_string(row.get("nextSafeMove")))) + .or_else(|| compact_row_field(selected_row, "next")) + .unwrap_or_else(|| next_safe_move.to_string()); + let selected_item = selected_output + .and_then(|output| non_empty_string(output.get("item"))) + .or_else(|| selected_row.and_then(|row| non_empty_string(row.get("item")))) + .or_else(|| compact_row_field(selected_row, "item")) + .unwrap_or_else(|| "none".to_string()); + let selected_branch = refreshed_overview + .and_then(|overview| non_empty_string(overview.get("branch"))) + .or_else(|| selected_row.and_then(|row| non_empty_string(row.get("branch")))) + .or_else(|| compact_row_field(selected_row, "branch")) + .unwrap_or_else(|| "none".to_string()); let (category, owner, gate, blocker, recommended_command, mutates_state, requires_elevation) = match stop_reason { "dry-run-preview-complete" => ( "dry-run-preview", "Self", - selected_gate, - selected_blocker, + selected_gate.as_str(), + selected_blocker.as_str(), "epiphany-swarm run --workspace --until blocked-or-published", false, false, @@ -594,14 +614,14 @@ fn classify_run_stop( false, ), "blocked-or-noop" if actionable_count == 0 => { - let owner = match selected_gate { + let owner = match selected_gate.as_str() { "awaiting-publication" => "Bifrost", "awaiting-sync" => "Bifrost/GitHub", "awaiting-closure" => "Soul", "ready-to-run" | "ready-to-adopt" | "ready-to-execute" => "Self", _ => "Self", }; - let recommended = match selected_gate { + let recommended = match selected_gate.as_str() { "awaiting-publication" => { "epiphany-work publish --workspace --closure-receipt " } @@ -616,8 +636,8 @@ fn classify_run_stop( ( "authority-gated", owner, - selected_gate, - selected_blocker, + selected_gate.as_str(), + selected_blocker.as_str(), recommended, false, false, @@ -626,8 +646,8 @@ fn classify_run_stop( _ => ( "unknown", "Self", - selected_gate, - selected_blocker, + selected_gate.as_str(), + selected_blocker.as_str(), "epiphany-work overview --workspace ", false, false, @@ -663,6 +683,27 @@ fn classify_run_stop( }) } +fn non_empty_string(value: Option<&Value>) -> Option { + value + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToString::to_string) +} + +fn compact_row_field(row: Option<&Value>, field: &str) -> Option { + let prefix = format!("{field}="); + row.and_then(Value::as_str) + .and_then(|row| { + row.split('|') + .map(str::trim) + .find_map(|part| part.strip_prefix(&prefix)) + }) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToString::to_string) +} + fn queue_run_args( args: &RunArgs, workspace: &Path, diff --git a/notes/epiphany-repo-swarm-mvp-contract.md b/notes/epiphany-repo-swarm-mvp-contract.md index a9bab56b..3e9073ba 100644 --- a/notes/epiphany-repo-swarm-mvp-contract.md +++ b/notes/epiphany-repo-swarm-mvp-contract.md @@ -1464,8 +1464,9 @@ Build the remaining MVP in this order: `epiphany.repo_swarm_run_stop_classification.v0` into the run receipt so agents can see stop category, owner, authority gate, blocker, recommended command, mutability/elevation flags, queue status, and private-state seal - without parsing prose. Remaining work is broader stop-case proof coverage and - safe-family depth. + without parsing prose. Fresh proof now covers empty-queue and dry-run + planning-preview stops; remaining stop-depth work belongs to gated + publication/sync/service cases and safe-family depth. 5. **Execute-to-close handoff.** Let the scheduler route from branch-local Hands execution into Soul/Modeling/Mind closure when the required execute receipts exist. The closure may be deterministic for mechanical work and @@ -1816,9 +1817,13 @@ Required organs before MVP: `.epiphany-smoke\fresh-repo-mvp-20260620-180024` shows the bounded run stops with category `iteration-limit`, owner `Self`, gate `self.scheduler-iteration-limit`, non-mutating/non-elevated flags, and - `privateStateExposed=false`. Remaining work is broader stop-case proof - coverage and richer closure depth rather than basic safe-family planning - cargo. + `privateStateExposed=false`. Focused stop proof + `.epiphany-smoke\swarm-stop-classification-20260620-181606` now also proves + `queue-empty` / Gjallar / `repo.work.overview` for an empty repo queue and + `dry-run-preview` / Self / `ready-to-run` for a planning item preview, both + non-mutating, non-elevated, and private-state sealed. Remaining work is + gated publication/sync/service stop depth and richer closure depth rather + than basic safe-family planning cargo. - Proof bundle depth: maintainers and future agents can inspect local operator-safe receipt chains, artifact schema/status rows, SHA-256 receipt hashes, compact TUI rows, commit refs, verification verdicts, map admission, diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index df652d89..5cf52ac2 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -109,7 +109,7 @@ remembers doctrine; the branch remembers the blade. - 2026-06-20: Typed Imagination action-item receipts are live in `epiphany-work derive-plan`. The command now writes `work-action-items-.json` with schema `epiphany.repo_work_imagination_action_items_receipt.v0` before writing `work-plan-.json`; the receipt carries model provenance, safe action family, requested paths, verification asks, stop conditions, escalation reasons, rollback hints, public/candidate refs, `operatorAuthoredShellDetails=false`, and no Hands/durable-state/publication/service/cross-repo authority. The plan derivation cites the action-item receipt, while command text remains deterministic safe-family lowering. Disposable proof `.epiphany-smoke\action-items-20260620-023240` wrote action receipt `repo-work-action-items-action-items-request` with `modelAuthored=true`, `safeActionFamily=repo.markdown_planning_note`, requested path `notes/epiphany-work/action-items-request.md`, plan citation present, and `privateStateExposed=false`. The next cut is deeper model-authored closure review, further non-planning action classes, or Idunn-owned queue-run service execution; not another basic action-item schema. - 2026-06-20: The first richer model-authored safe family is live. `epiphany-work derive-plan --action-family checklist-note` lowers accepted Persona/Bifrost pressure into a contained markdown checklist under `notes/epiphany-work/-checklist.md` with safe family `repo.checklist_note`, model provenance, candidate/public refs, branch-local checklist items, verification asks, rollback hints, and authority seals forbidding publication, merge, service lifecycle, cross-repo mutation, durable-state admission, and private-state exposure. Disposable proof `.epiphany-smoke\checklist-note-20260620-031347` ran accept -> derive-plan with `--model-ref smoke-imagination-v0 --model-authored` -> run -> adopt -> execute on an `epiphany/*` branch; the derivation and action-item receipt both reported `modelAuthored=true`, Hands created a branch-local commit, overview reported `awaiting-closure`, and `privateStateExposed=false`. - 2026-06-20: `epiphany-swarm run` is live as the bounded repo-swarm operator mouth over Self's queue/tick physiology. It requires the online receipt/local Verse queue, delegates queue selection to `epiphany-work queue-run`, delegates item actuation to `epiphany-work tick`, writes `epiphany.repo_swarm_run_receipt.v0` at `.epiphany\swarm-run\repo-swarm-run-receipt.json`, and keeps publication, merge, service lifecycle, elevation, cross-repo mutation, and private-state exposure sealed. Wrapper mode `tools/epiphany_local_run.ps1 -Mode repo-swarm-run` exposes the surface with brake preflight and compact pulse summaries. Disposable proof `.epiphany-smoke\swarm-run-20260620-024252` first returned dry-run preview `would-advance/run-from-plan`; wrapper artifact `local-20260620-024413-360-85548195` printed the same queue/tick delegation; a non-dry single pulse advanced `swarm-run-request` from `ready-to-run` to refreshed overview `ready-to-adopt` with `privateStateExposed=false`. The next cut is deeper model-authored closure review, further non-planning action classes, or Idunn-owned queue-run service execution; not another queue mouth. -- 2026-06-20: `epiphany-swarm run` stop classification is now typed. The run receipt/result embeds `epiphany.repo_swarm_run_stop_classification.v0` with status, stop reason, category, owner, authority gate, blocker, selected queue context, recommended command, mutability/elevation flags, and private-state seal. Fresh-repo proof `.epiphany-smoke\fresh-repo-mvp-20260620-180024` ran init -> online -> Persona intake -> model-authored repo-status plan -> swarm run -> close -> publish -> upstream-main sync -> export proof; the run stopped as `paused-at-iteration-limit` with stop category `iteration-limit`, owner `Self`, authority gate `self.scheduler-iteration-limit`, no state mutation/elevation in the stop packet, and `privateStateExposed=false`. Remaining repo-swarm work is broader stop-case proof coverage, deeper interpreter/closure semantics, more useful repo-owned configuration/action families, and Idunn-owned queue-run lifecycle under explicit operator authority. +- 2026-06-20: `epiphany-swarm run` stop classification is now typed. The run receipt/result embeds `epiphany.repo_swarm_run_stop_classification.v0` with status, stop reason, category, owner, authority gate, blocker, selected queue context, recommended command, mutability/elevation flags, and private-state seal. Fresh-repo proof `.epiphany-smoke\fresh-repo-mvp-20260620-180024` ran init -> online -> Persona intake -> model-authored repo-status plan -> swarm run -> close -> publish -> upstream-main sync -> export proof; the run stopped as `paused-at-iteration-limit` with stop category `iteration-limit`, owner `Self`, authority gate `self.scheduler-iteration-limit`, no state mutation/elevation in the stop packet, and `privateStateExposed=false`. Focused proof `.epiphany-smoke\swarm-stop-classification-20260620-181606` now also proves `queue-empty` / Gjallar / `repo.work.overview` for an empty repo queue and `dry-run-preview` / Self / `ready-to-run` for a planning item preview, both non-mutating, non-elevated, and `privateStateExposed=false`. Remaining repo-swarm work is gated publication/sync/service stop depth, deeper interpreter/closure semantics, more useful repo-owned configuration/action families, and Idunn-owned queue-run lifecycle under explicit operator authority. - 2026-06-20: Execute-to-close scheduler handoff is live. `epiphany-work tick` now treats `awaiting-closure` as tick-actionable, delegates executed branch-local work to the existing `epiphany-work close` gate, writes Soul verdict plus Modeling summary plus Mind state-commit receipts, then stops at `awaiting-publication` with Bifrost as blocker. Disposable proof `.epiphany-smoke\tick-close-20260620-025526` ran `epiphany-swarm run --max-iterations 4` through `run-from-plan,adopt-from-plan,execute-from-plan,close-from-execute`; the final overview reported `awaiting-publication` / `bifrost-publication-missing`, and the close receipt was `closed`, Soul `passed`, Mind commit `repo-work-close-tick-close-request-mind-commit`, `durableStateAdmitted=true`, and `privateStateExposed=false` with publication/merge/service/cross-repo authority sealed. The next cut is deeper model-authored closure review, further non-planning action classes, or Idunn-owned queue-run service execution; not another scheduler handoff. - 2026-06-20: Local proof-bundle packaging is live. `epiphany-work overview` now embeds `epiphany.repo_work_proof_bundle.v0` with generated time, gate/blocker/next-safe-move, compact TUI rows, expected artifact paths, present/missing artifact status, document schema/status, SHA-256 receipt hashes, and `privateStateExposed=false`. Disposable proof `.epiphany-smoke\tick-close-20260620-025526\08-overview-proof-bundle.json` reported six present artifacts from accept through close with hashes, missing publish/sync rows, `awaiting-publication` / `bifrost-publication-missing`, and no private-state exposure. The next cut is deeper model-authored closure review, further non-planning action classes, or Idunn-owned queue-run service execution; not another local proof-bundle wrapper. - 2026-06-20: Published/credited proof-bundle rows are live. `epiphany-work overview` now adds `publicationRows` to `epiphany.repo_work_proof_bundle.v0`, lifting Bifrost intent/publication/GitHub/ledger/credit/PR fields, Hands commit/PR fields, and upstream-main ancestry fields from publish/sync receipts without granting publication, merge, scheduler, service lifecycle, cross-repo, or private-state authority. Disposable proof `.epiphany-smoke\checklist-note-20260620-031347\12-overview-published-proof.json` ran close -> publish -> sync -> overview after the checklist safe-family proof; overview reported `complete-or-awaiting-new-work`, `upstreamMainSynced=true` from `authority.upstreamMainSynced`, three publication rows, and `privateStateExposed=false`. The next cut is deeper model-authored closure review, further non-planning action classes, or Idunn-owned queue-run service execution; not basic Bifrost/GitHub/sync row visibility. diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 3e8347de7e632559995499ea46d4db76d7e68f5b..4b4541486dad6c50f2402ec9e0c50a1bd6097e40 100644 GIT binary patch delta 470 zcmY+Ay-ve06opA?SrBaeOOcUF+orX*u7#LWoio`P@ zAu;d<%sd87-yIva2Y{l3T84=BwR`%ha|;`z`O;hg}&jkSe%tAhX{?Bpe2hd z@)&Z5{vc7|2#qfsrVuLW@XkSOR0^)p6s&Tp98~6X<}3`5av*6_%|i!9)I_HVb+|=df3Tm4SX;DmMT8macK*mzA_~WkxNMn3Cx) zFk5i?mzcz)