From 2d346bfe7274b8d886130f1387f5e81401649db0 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 20 Jun 2026 20:15:25 +0100 Subject: [PATCH] Guard repo work Mind safe families --- ...phany-repo-mind-safe-family-guard-smoke.rs | 441 ++++++++++++++++++ epiphany-core/src/bin/epiphany-work.rs | 96 +++- notes/epiphany-repo-swarm-mvp-contract.md | 36 +- notes/fresh-workspace-handoff.md | 1 + state/ledgers.msgpack | Bin 348713 -> 349238 bytes state/map.yaml | 5 +- 6 files changed, 565 insertions(+), 14 deletions(-) create mode 100644 epiphany-core/src/bin/epiphany-repo-mind-safe-family-guard-smoke.rs diff --git a/epiphany-core/src/bin/epiphany-repo-mind-safe-family-guard-smoke.rs b/epiphany-core/src/bin/epiphany-repo-mind-safe-family-guard-smoke.rs new file mode 100644 index 00000000..f2b7390f --- /dev/null +++ b/epiphany-core/src/bin/epiphany-repo-mind-safe-family-guard-smoke.rs @@ -0,0 +1,441 @@ +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); + 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!("repo-mind-safe-family-guard-{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"), + "# Repo Mind Safe Family Guard Smoke\n\nThis repository proves Mind refuses alien safe-family labels before Hands authority.\n", + ) + .with_context(|| format!("failed to seed {}", repo.join("README.md").display()))?; + git(["add", "README.md"], &repo)?; + git( + [ + "commit", + "-m", + "Seed repo mind safe family guard smoke body", + ], + &repo, + )?; + git( + ["switch", "-c", "epiphany/repo-mind-safe-family-guard"], + &repo, + )?; + + let item = "repo-mind-safe-family-guard"; + 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", + "repo-mind-safe-family-guard-smoke", + "--topic", + "repo-mind-safe-family-guard", + ], + &root, + )?; + cargo_json( + &manifest, + "epiphany-swarm", + &[ + "online", + "--workspace", + path_str(&repo)?, + "--epiphany-root", + path_str(&root)?, + "--runtime-id", + "repo-mind-safe-family-guard-smoke", + "--local-verse-store", + path_str(&local_verse)?, + ], + &root, + )?; + cargo_json( + &manifest, + "epiphany-work", + &[ + "accept", + "--workspace", + path_str(&repo)?, + "--from", + "persona", + "--item", + item, + "--summary", + "Ask Mind to reject a tampered safe-family label before Hands receives authority.", + "--candidate-action-ref", + "candidate-action://repo-mind-safe-family-guard/tampered-family", + "--public-discussion-ref", + "epiphany-global/persona-collaboration/repo-mind-safe-family-guard", + ], + &root, + )?; + let plan = cargo_json( + &manifest, + "epiphany-work", + &[ + "derive-plan", + "--workspace", + path_str(&repo)?, + "--item", + item, + "--action-family", + "planning-note", + "--model-ref", + "repo-mind-safe-family-guard-smoke-imagination-v1", + "--model-authored", + ], + &root, + )?; + let run = cargo_json( + &manifest, + "epiphany-work", + &[ + "run", + "--workspace", + path_str(&repo)?, + "--epiphany-root", + path_str(&root)?, + "--item", + item, + "--path", + "notes/epiphany-work/repo-mind-safe-family-guard.md", + ], + &root, + )?; + + let plan_path = value_at_path(&plan, &["receiptPath"]) + .and_then(Value::as_str) + .ok_or_else(|| anyhow!("plan output had no receiptPath"))?; + let mut tampered_plan = read_json(Path::new(plan_path))?; + tampered_plan["derivation"]["actionItemReceipt"]["safeActionFamily"] = + json!("idunn.service_lifecycle"); + let tampered_path = smoke_dir.join("tampered-plan.json"); + write_json(&tampered_path, &tampered_plan)?; + + let adopt_failure = cargo_json_expect_failure( + &manifest, + "epiphany-work", + &[ + "adopt", + "--workspace", + path_str(&repo)?, + "--epiphany-root", + path_str(&root)?, + "--item", + item, + "--run-receipt", + value_at_path(&run, &["receiptPath"]) + .and_then(Value::as_str) + .ok_or_else(|| anyhow!("run output had no receiptPath"))?, + "--from-plan", + path_str(&tampered_path)?, + ], + &root, + )?; + if !adopt_failure.contains("unsupported repo-work safe family") { + return Err(anyhow!( + "adopt failure did not name unsupported safe family: {adopt_failure}" + )); + } + + let refusal_path = repo + .join(".epiphany") + .join("work") + .join("work-mind-adopt-repo-mind-safe-family-guard.json"); + let refusal = read_json(&refusal_path)?; + require_eq( + &refusal, + &["schemaVersion"], + "epiphany.repo_work_mind_adoption_decision.v0", + )?; + require_eq(&refusal, &["status"], "refused-unsupported-safe-family")?; + require_eq( + &refusal, + &["interpretation", "inputSummary", "safeActionFamily"], + "idunn.service_lifecycle", + )?; + require_bool( + &refusal, + &["interpretation", "classification", "actionItemAccepted"], + false, + )?; + require_bool( + &refusal, + &["interpretation", "classification", "safeFamilyRecognized"], + false, + )?; + require_text_field( + &refusal, + &["interpretation", "refusalReasons", "0"], + "Unsupported repo-work safe action family idunn.service_lifecycle", + )?; + require_bool(&refusal, &["authority", "handsAuthorityGranted"], false)?; + require_bool(&refusal, &["authority", "durableStateAdmitted"], false)?; + require_bool(&refusal, &["authority", "serviceLifecycleAuthority"], false)?; + require_bool(&refusal, &["authority", "privateStateExposed"], false)?; + require_eq( + &refusal, + &["authority", "nextGate"], + "imagination.replan_with_allowed_safe_family", + )?; + require_bool(&refusal, &["privateStateExposed"], false)?; + let adoption_receipt_path = repo + .join(".epiphany") + .join("work") + .join("work-adopt-repo-mind-safe-family-guard.json"); + if adoption_receipt_path.exists() { + return Err(anyhow!( + "adoption receipt was written despite unsupported safe-family refusal: {}", + adoption_receipt_path.display() + )); + } + + Ok(json!({ + "schemaVersion": "epiphany.repo_work_mind_safe_family_guard_smoke.v0", + "status": "ok", + "smokeDir": smoke_dir, + "repo": repo, + "item": item, + "tamperedSafeActionFamily": "idunn.service_lifecycle", + "mindDecisionStatus": refusal["status"], + "actionItemAccepted": false, + "safeFamilyRecognized": false, + "handsAuthorityGranted": false, + "durableStateAdmitted": false, + "serviceLifecycleAuthority": false, + "adoptionReceiptWritten": false, + "privateStateExposed": false + })) +} + +fn cargo_json(manifest_path: &Path, bin_name: &str, args: &[&str], cwd: &Path) -> Result { + let output = Command::new("cargo") + .arg("run") + .arg("--quiet") + .arg("--manifest-path") + .arg(manifest_path) + .arg("--bin") + .arg(bin_name) + .arg("--") + .args(args) + .current_dir(cwd) + .output() + .with_context(|| format!("failed to spawn cargo run --bin {bin_name}"))?; + if !output.status.success() { + return Err(anyhow!( + "cargo run --bin {bin_name} failed:\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + )); + } + serde_json::from_slice(&output.stdout) + .with_context(|| format!("{bin_name} returned invalid JSON")) +} + +fn cargo_json_expect_failure( + manifest_path: &Path, + bin_name: &str, + args: &[&str], + cwd: &Path, +) -> Result { + let output = Command::new("cargo") + .arg("run") + .arg("--quiet") + .arg("--manifest-path") + .arg(manifest_path) + .arg("--bin") + .arg(bin_name) + .arg("--") + .args(args) + .current_dir(cwd) + .output() + .with_context(|| format!("failed to spawn cargo run --bin {bin_name}"))?; + if output.status.success() { + return Err(anyhow!("{bin_name} unexpectedly succeeded")); + } + Ok(format!( + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + )) +} + +fn git(args: [&str; N], cwd: &Path) -> Result<()> { + let output = Command::new("git") + .args(args) + .current_dir(cwd) + .output() + .with_context(|| format!("failed to spawn git in {}", cwd.display()))?; + if output.status.success() { + Ok(()) + } else { + Err(anyhow!( + "git failed in {}:\n{}", + cwd.display(), + String::from_utf8_lossy(&output.stderr) + )) + } +} + +fn read_json(path: &Path) -> Result { + let raw = + fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?; + serde_json::from_str(&raw).with_context(|| format!("failed to decode {}", path.display())) +} + +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 = value_at_path(value, path) + .and_then(Value::as_str) + .ok_or_else(|| anyhow!("missing string at {}", path.join(".")))?; + if actual == expected { + Ok(()) + } else { + Err(anyhow!( + "expected {} to be {:?}, got {:?}", + path.join("."), + expected, + actual + )) + } +} + +fn require_bool(value: &Value, path: &[&str], expected: bool) -> Result<()> { + let actual = value_at_path(value, path) + .and_then(Value::as_bool) + .ok_or_else(|| anyhow!("missing bool at {}", path.join(".")))?; + if actual == expected { + Ok(()) + } else { + Err(anyhow!( + "expected {} to be {}, got {}", + path.join("."), + expected, + actual + )) + } +} + +fn require_text_field(value: &Value, path: &[&str], needle: &str) -> Result<()> { + let actual = value_at_path(value, path) + .and_then(Value::as_str) + .ok_or_else(|| anyhow!("missing string at {}", path.join(".")))?; + if actual.contains(needle) { + Ok(()) + } else { + Err(anyhow!( + "expected {} to contain {:?}, got {:?}", + path.join("."), + needle, + actual + )) + } +} + +fn value_at_path<'a>(value: &'a Value, path: &[&str]) -> Option<&'a Value> { + let mut cursor = value; + for segment in path { + if let Ok(index) = segment.parse::() { + cursor = cursor.as_array()?.get(index)?; + } else { + cursor = cursor.get(*segment)?; + } + } + Some(cursor) +} + +fn path_str(path: &Path) -> Result<&str> { + path.to_str() + .ok_or_else(|| anyhow!("path is not valid UTF-8: {}", path.display())) +} + +fn take_path(args: &mut impl Iterator, name: &str) -> Result { + Ok(PathBuf::from( + args.next() + .ok_or_else(|| anyhow!("missing value for {name}"))?, + )) +} diff --git a/epiphany-core/src/bin/epiphany-work.rs b/epiphany-core/src/bin/epiphany-work.rs index 0000a5a8..4ce1942c 100644 --- a/epiphany-core/src/bin/epiphany-work.rs +++ b/epiphany-core/src/bin/epiphany-work.rs @@ -7158,6 +7158,15 @@ fn run_adopt(args: AdoptArgs) -> Result { .and_then(Value::as_bool) .unwrap_or(false); let evidence_ref_count = adoption_evidence_refs.len(); + let safe_family_recognized = repo_work_safe_family_is_recognized(action_item_safe_family); + let unsupported_plan_family = plan_receipt.is_some() && !safe_family_recognized; + let refusal_reasons = if unsupported_plan_family { + vec![format!( + "Unsupported repo-work safe action family {action_item_safe_family}; Mind refused to convert this plan into branch-local Hands authority." + )] + } else { + Vec::new() + }; let mind_interpretation = json!({ "schemaVersion": "epiphany.repo_work_mind_interpretation.v0", "owner": "Mind", @@ -7176,8 +7185,8 @@ fn run_adopt(args: AdoptArgs) -> Result { }, "classification": { "decisionKind": "branch-local-hands-adoption", - "actionItemAccepted": true, - "safeFamilyRecognized": action_item_safe_family != "manual-plan", + "actionItemAccepted": !unsupported_plan_family, + "safeFamilyRecognized": safe_family_recognized, "requestedPathsDeclared": requested_path_count > 0, "evidenceRefsPresent": evidence_ref_count > 0, "durableStateAdmission": "not-admitted", @@ -7196,10 +7205,60 @@ fn run_adopt(args: AdoptArgs) -> Result { "cross_body_mutation", "private_verse_export" ], - "refusalReasons": [], + "refusalReasons": refusal_reasons, "privateStateExposed": false }); let mind_adoption_id = format!("repo-work-mind-adoption-{item_slug}"); + if unsupported_plan_family { + let refusal_decision = json!({ + "schemaVersion": "epiphany.repo_work_mind_adoption_decision.v0", + "createdAt": now, + "workspace": workspace, + "runtimeId": run_receipt["runtimeId"], + "runtimeStore": runtime_store, + "decisionId": mind_adoption_id, + "item": item, + "status": "refused-unsupported-safe-family", + "owner": "Mind", + "interpreter": "Mind", + "router": "Self", + "sourcePlanReceiptPath": plan_receipt_path, + "sourceRunReceiptPath": run_receipt_path, + "adoptedActionItemReceiptId": action_item_receipt_id, + "adoptedActionItem": adopted_action_item, + "adoptionEvidenceRefs": adoption_evidence_refs, + "interpretation": mind_interpretation, + "rationale": refusal_reasons.join(" "), + "gates": { + "selfPresentedActionItem": true, + "mindReviewedEvidence": true, + "mindInterpretedActionItem": true, + "safeFamilyRequired": true, + "safeFamilyRecognized": false, + "branchLocalOnly": false, + "bifrostPublicationRequired": true, + "soulClosureRequired": true + }, + "authority": { + "durableStateAdmitted": false, + "handsAuthorityGranted": false, + "publicationAuthorized": false, + "mergeAuthorized": false, + "serviceLifecycleAuthority": false, + "crossRepoMutation": false, + "privateStateExposed": false, + "nextGate": "imagination.replan_with_allowed_safe_family" + }, + "privateStateExposed": false, + "nextSafeMove": "Ask Imagination for an allowed repo-work safe family before Hands authority can be granted." + }); + let mind_adoption_path = artifact_dir.join(format!("work-mind-adopt-{item_slug}.json")); + write_json(&mind_adoption_path, &refusal_decision)?; + return Err(anyhow!( + "Mind refused adoption for unsupported repo-work safe family {action_item_safe_family}; refusal decision written to {}", + mind_adoption_path.display() + )); + } let mind_adoption_rationale = args.mind_adoption_rationale.unwrap_or_else(|| { format!( "Mind adopted the selected Imagination action item for branch-local Hands work because Self presented explicit adoption evidence and the safe family remains bounded: {}", @@ -10654,6 +10713,37 @@ fn adopted_action_item_from_plan(plan: &Value) -> Value { }) } +fn repo_work_safe_family_is_recognized(safe_family: &str) -> bool { + matches!( + safe_family, + "repo.append_worklog" + | "repo.markdown_planning_note" + | "repo.checklist_note" + | "repo.markdown_managed_section" + | "repo.status_section" + | "repo.task_card" + | "repo.body_manifest" + | "repo.tool_capabilities" + | "repo.tool_request" + | "repo.eve_surface" + | "repo.collaboration_policy" + | "repo.collaboration_topic" + | "repo.consensus_brief" + | "repo.objective_draft" + | "repo.adoption_request" + | "repo.scheduling_request" + | "repo.work_order" + | "repo.verification_request" + | "repo.publication_request" + | "repo.sync_request" + | "repo.maintainer_review_request" + | "repo.pr_request" + | "repo.credit_request" + | "repo.artifact_acceptance_request" + | "repo.metrics_request" + ) +} + fn compact_text(text: &str, limit: usize) -> String { let compact = text.split_whitespace().collect::>().join(" "); if compact.chars().count() <= limit { diff --git a/notes/epiphany-repo-swarm-mvp-contract.md b/notes/epiphany-repo-swarm-mvp-contract.md index 19a00106..cbe8b67f 100644 --- a/notes/epiphany-repo-swarm-mvp-contract.md +++ b/notes/epiphany-repo-swarm-mvp-contract.md @@ -1446,12 +1446,20 @@ Build the remaining MVP in this order: artery before granting branch-local Hands authority. `adopt` now writes a separate Mind interpreter decision receipt, embeds an `epiphany.repo_work_mind_interpretation.v0` packet that classifies the action - item, requested paths, safe family, allowed Hands transition, and forbidden - durable-state/publication/service/private transitions, then cites it from the - main adoption receipt. Banter remains banter unless Mind/Interpreter extracts - a work candidate. The remaining cut is richer model-authored families and - deeper interpreter semantics beyond the first Mind interpretation packet, not - basic action-item or adoption receipt shape. + item, requested paths, recognized safe family, allowed Hands transition, and + forbidden durable-state/publication/service/private transitions, then cites it + from the main adoption receipt. The first deeper interpreter guard is now + live: if a plan-derived action item presents a safe-family label outside the + repo-work allowlist, Mind writes a refused adoption decision and grants no + Hands, durable-state, service-lifecycle, publication, cross-body, or + private-state authority. Proof + `.epiphany-smoke\repo-mind-safe-family-guard-20260620-190811` tampers a + planning note into `idunn.service_lifecycle`, receives + `refused-unsupported-safe-family`, and writes no adoption receipt. Banter + remains banter unless Mind/Interpreter extracts a work candidate. The + remaining cut is richer model-authored families and deeper interpreter + semantics beyond the first allowlist refusal, not basic action-item or + adoption receipt shape. 3. **Model-authored safe plan families.** Promote `derive-plan` beyond the deterministic `append-worklog` and `planning-note` reliquaries by letting Imagination author typed plans over allowlisted repo-local families. Shell @@ -1787,7 +1795,17 @@ Required organs before MVP: lifecycle, cross-body, or private-state authority. The main adoption receipt cites that Mind decision, then grants only branch-local Hands authority; the branch-local item still closes through Soul with family assertions and path - scope passing. + scope passing. The Mind decision now refuses unsupported plan-derived + safe-family labels before Hands review can open: smoke + `.epiphany-smoke\repo-mind-safe-family-guard-20260620-190811` tampered a valid + planning-note action item into `idunn.service_lifecycle` and proved + `actionItemAccepted=false`, `safeFamilyRecognized=false`, + `refused-unsupported-safe-family`, no adoption receipt, no branch-local Hands + authority, no durable-state admission, no service-lifecycle authority, and no + private-state exposure. Positive regression smokes + `.epiphany-smoke\repo-planning-facets-20260620-190916` and + `.epiphany-smoke\closure-model-gate-20260620-190916` prove recognized + planning cargo and closure model gates still pass. `repo-eve-surface` now proves branch-local Eve/CultUI surface contract cargo under `.epiphany/eve-surfaces/.toml`, with compact agent TUI and GUI lowering contracts, local/public Verse routing, Odin discovery, Persona/human @@ -1814,8 +1832,8 @@ Required organs before MVP: `.epiphany-smoke\repo-tool-request-family-20260620-174500` proved the request closes through Soul with family assertions and path scope passing. Remaining work is later model-authored action classes that can change more - useful repo-owned configuration, plus deeper interpreter semantics after Mind - adoption, without arbitrary shell authority. + useful repo-owned configuration, plus deeper interpreter semantics after the + first Mind safe-family allowlist refusal, without arbitrary shell authority. - Closure depth: `close` now writes a structured `epiphany.repo_work_closure_review.v0` packet for Hands commits, records model-review provenance when supplied, refuses actual-vs-declared path-scope diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 7178b58d..78d246eb 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -140,6 +140,7 @@ remembers doctrine; the branch remembers the blade. - 2026-06-20: `repo-artifact-acceptance-request` is now live as accepted-artifact request cargo. `epiphany-work derive-plan --action-family repo-artifact-acceptance-request` writes `.epiphany/artifact-acceptance-requests/.toml` with safe family `repo.artifact_acceptance_request`, status `awaiting-artifact-acceptance-review`, closure/Soul/Mind/public-proof, maintainer-review, and Hands-commit antecedents, artifact/commit/path/review/proof/rationale/redaction packet requirements, and denials for artifact acceptance, credit ledger, GitHub PR, merge, publication, upstream sync, Hands action, cross-body mutation, and private Verse rummaging. Soul closure assertions inspect schema, family, summary, request state, antecedents, receipt contract, artifact packet, authority gates, and private-state seal. Focused smoke `.epiphany-smoke\repo-artifact-acceptance-request-family-20260620-161559` ran init -> online -> accept -> repo-artifact-acceptance-request derive-plan -> tick run/adopt/execute/close; Hands committed only `.epiphany/artifact-acceptance-requests/repo-artifact-acceptance-request-family.toml`, Soul passed with `pathScopeMatched=true`, `familyAssertions.status=passed`, `awaitingArtifactAcceptanceReview=true`, `artifactAcceptanceAuthorized=false`, `creditLedgerAuthorized=false`, `githubPrAuthorized=false`, `mergeAuthorized=false`, `publicationAuthorized=false`, `upstreamSyncAuthorized=false`, `handsActionAuthorized=false`, and `privateStateExposed=false`. Current remaining repo-swarm MVP organs are deeper interpreter/adoption semantics, more useful repo-owned configuration/action families, and Idunn-owned queue-run service lifecycle under explicit operator authority. - 2026-06-20: `repo-metrics-request` is now live as Bifrost/maintainer accounting request cargo. `epiphany-work derive-plan --action-family repo-metrics-request` writes `.epiphany/metrics-requests/.toml` with safe family `repo.metrics_request`, status `awaiting-metrics-review`, closure/Soul/Mind/public-proof, accepted-artifact, and credit-request antecedents, model-call/token-cost/review-load/artifact/proof/credit-readback/redaction packet requirements, and denials for metrics ledger, spend, review-load, credit ledger, GitHub PR, merge, publication, upstream sync, Hands action, cross-body mutation, and private Verse rummaging. Soul closure assertions inspect schema, family, summary, request state, antecedents, receipt contract, metrics packet, authority gates, and private-state seal. Focused smoke `.epiphany-smoke\repo-metrics-request-family-20260620-162919` ran init -> online -> accept -> repo-metrics-request derive-plan -> tick run/adopt/execute/close; Hands committed only `.epiphany/metrics-requests/repo-metrics-request-family.toml`, Soul passed with `pathScopeMatched=true`, `familyAssertions.status=passed`, `awaitingMetricsReview=true`, `metricsLedgerAuthorized=false`, `spendAuthorized=false`, `reviewLoadAuthorized=false`, `creditLedgerAuthorized=false`, `githubPrAuthorized=false`, `mergeAuthorized=false`, `publicationAuthorized=false`, `upstreamSyncAuthorized=false`, `handsActionAuthorized=false`, and `privateStateExposed=false`. Current remaining repo-swarm MVP organs are deeper interpreter/adoption semantics, more useful repo-owned configuration/action families, and Idunn-owned queue-run service lifecycle under explicit operator authority. - 2026-06-20: `derive-plan` action-item receipts now carry richer model-authored planning facets without widening command authority, and `adopt --from-plan` now preserves the chosen Imagination action item in the Mind adoption receipt. New CLI fields `--assumption`, `--constraint`, `--non-goal`, `--open-question`, `--decision-point`, and `--evidence-need` are recorded under `planningFacets` in `epiphany.repo_work_imagination_action_items_receipt.v0`, mirrored into the plan derivation's `actionItemReceipt`, then copied into `epiphany.repo_work_adoption_receipt.v0` as `adoptedActionItem`; deterministic defaults are emitted when a model does not supply them. Focused smoke `.epiphany-smoke\repo-planning-facets-20260620-164958` ran init -> online -> accept -> model-authored `derive-plan --action-family planning-note` with all facet fields -> tick run/adopt/execute/close; the action-item receipt preserved assumptions, constraints, non-goals, open questions, decision points, and evidence needs while `handsAuthorityGranted=false`, `durableStateAdmitted=false`, `handsCommandAuthority=false`, `durableStateAuthority=false`, and `privateStateExposed=false`; the adoption receipt preserved `adoptedActionItem.safeActionFamily=repo.markdown_planning_note`, planning facets, `adoptedActionItem.handsCommandAuthority=false`, `adoptedActionItem.durableStateAuthority=false`, `adoptedActionItem.publicationAuthorized=false`, `authority.handsAuthorityGranted=true`, `authority.durableStateAdmitted=false`, and `privateStateExposed=false`; Soul closure passed with family assertions and path scope matched. Current remaining repo-swarm MVP organs are deeper interpreter/adoption semantics, more useful repo-owned configuration/action families, and Idunn-owned queue-run service lifecycle under explicit operator authority. +- 2026-06-20: Mind now has an explicit repo-work safe-family allowlist guard before branch-local Hands authority. `adopt --from-plan` treats unsupported plan-derived safe-family labels as refused interpreter decisions, writes `epiphany.repo_work_mind_adoption_decision.v0` with `status=refused-unsupported-safe-family`, keeps `actionItemAccepted=false` and `safeFamilyRecognized=false`, grants no Hands/durable-state/service/publication/cross-body/private-state authority, and writes no normal adoption receipt. Focused smoke `.epiphany-smoke\repo-mind-safe-family-guard-20260620-190811` tampered a valid planning-note plan into `idunn.service_lifecycle` and proved the refusal path; positive smokes `.epiphany-smoke\repo-planning-facets-20260620-190916` and `.epiphany-smoke\closure-model-gate-20260620-190916` prove recognized planning facets and closure model gates still pass. Current remaining repo-swarm MVP organs are deeper interpreter semantics beyond the allowlist refusal, more useful repo-owned configuration/action families, richer closure depth, and Idunn-owned queue-run service lifecycle under explicit operator authority. - 2026-06-20: `repo-eve-surface` is now live as repo-owned Eve/CultUI surface contract cargo. `epiphany-work derive-plan --action-family repo-eve-surface` writes `.epiphany/eve-surfaces/.toml` with safe family `repo.eve_surface`, compact agent TUI and GUI lowering contracts, local/public Verse routing, Odin discovery, provider-owned surface identity, Persona/human collaboration flags, feedback routing to Imagination, and denials for renderer ownership, rendering/state/publication/service/cross-body authority, private Verse rummaging, and private-state exposure. Soul closure assertions inspect schema, family, summary, surface routing, Verse routing, CultUI contracts, collaboration route, authority seals, and private-state seal. Focused smoke `.epiphany-smoke\repo-eve-surface-family-20260620-165927` ran init -> online -> accept -> repo-eve-surface derive-plan -> tick run/adopt/execute/close; Hands committed only `.epiphany/eve-surfaces/repo-eve-surface-family.toml`, Soul passed with `pathScopeMatched=true`, `familyAssertions.status=passed`, `hasCultUiContracts=true`, `hasVerseRouting=true`, `odinDiscoverable=true`, `feedbackRoutesToImagination=true`, `rendererOwnsTruth=false`, `renderingAuthority=false`, `stateAuthority=false`, `publicationAuthority=false`, `serviceLifecycleAuthority=false`, `crossBodyMutationAuthority=false`, `privateVerseRummaging=false`, and `privateStateExposed=false`. Current remaining repo-swarm MVP organs are deeper interpreter/adoption semantics, more useful repo-owned configuration/action families, and Idunn-owned queue-run service lifecycle under explicit operator authority. - 2026-06-20: `repo-collaboration-policy` is now live as repo-owned collaboration law cargo. `epiphany-work derive-plan --action-family repo-collaboration-policy` writes `.epiphany/collaboration-policy.toml` with safe family `repo.collaboration_policy`, private/local/public Verse boundaries, Odin discovery, Eve connection receipt requirements, Persona/human/peer discussion flags, speech-audit requirement, Imagination feedback routing, Mind/Bifrost gates, and denials for direct Hands/Mind state, publication, merge, service lifecycle, cross-body mutation, private Verse rummaging, and private-state exposure. Soul closure assertions inspect schema, family, summary, provider truth, Verse boundaries, Eve connection, Persona feedback, Imagination route, authority seals, and private-state seal. Focused smoke `.epiphany-smoke\repo-collaboration-policy-family-20260620-172808` ran init -> online -> accept -> repo-collaboration-policy derive-plan -> tick run/adopt/execute/close; Hands committed only `.epiphany/collaboration-policy.toml`, Soul passed with `pathScopeMatched=true`, `familyAssertions.status=passed`, `hasVerseBoundaries=true`, `odinDiscoverable=true`, `hasEveConnectionContract=true`, `feedbackRoutesToImagination=true`, `candidateActionsNonAuthoritative=true`, and `privateStateExposed=false`. Current remaining repo-swarm MVP organs are deeper interpreter semantics, more useful repo-owned configuration/action families, and Idunn-owned queue-run service lifecycle under explicit operator authority. - `service-execution-runbook` makes that next cut repeatable: it generates the exact elevated wrapper command sequence without mutating the service manager, continues after per-step failure to preserve more receipts, runs `service-execution-audit` in `finally`, exits nonzero if any step failed, and seals a typed runbook lifecycle receipt. Run the generated artifact only with explicit operator authority. diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index d636c5a507cb8ed6db5c70e960e24c8906001316..1bf920f564d3dae928bf4152163d6af4696951bb 100644 GIT binary patch delta 493 zcmY+A&q^FY5XPO=@eeH<;vu&n#CE!7x3H(X?LWKh$%BWy zK$dv{4|)>>eS@5R17F47LHu(lnyUW3uYTRvmz#H2n>Qbm5Bay3xNU-NC|MfLRRsbBh4X03(wv;H6(^fQ(h+4npL&ufD= zF5@uak^~sZl_E7_N_%WOAmufX2ugJw+6k5TM^c65tds~mGPa-=tKeeL2}UeDhhe;N zz=I!aOMV7Z@?&tG``{vUt7SpCV2rjN-?4rLrC**C#U+yQ?N9{EPb9KM4)&gDddAWu z_OHwKVBKg(o+b#c)~D#2{hf5HdFZ9v!y+s4LCf7mLuSm?jJ2YpQfe10vRVt2Y!oBk P{nx_gJV^zC|0W6 delta 53 zcmdnCLUiR4(G3!e)BpRkva&@TV_*$r+pNr3$jIp4Je8?^Dib3RgJ@