Bind repo work adoption to Mind authority

This commit is contained in:
Meta Vens 2026-07-13 19:38:35 +01:00
commit 2f22dbbca7
17 changed files with 1174 additions and 4232 deletions

View file

@ -1,454 +0,0 @@
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,
}
impl Args {
fn parse() -> Result<Self> {
let mut root = env::current_dir().context("failed to resolve current directory")?;
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")?,
other => return Err(anyhow!("unexpected argument {other:?}")),
}
}
Ok(Self { root })
}
}
fn run_smoke(args: Args) -> Result<Value> {
let root = args
.root
.canonicalize()
.with_context(|| format!("failed to resolve {}", args.root.display()))?;
let smoke_root = root.join(".epiphany-smoke");
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(&smoke_root)
.with_context(|| format!("failed to create {}", smoke_root.display()))?;
let stamp = Utc::now().format("%Y%m%d-%H%M%S").to_string();
let smoke_dir = smoke_root.join(format!("repo-doctrine-update-request-family-{stamp}"));
fs::create_dir(&smoke_dir)
.with_context(|| format!("failed to claim fresh smoke dir {}", 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 Doctrine Update Request Family Smoke\n\nThis repository proves branch-local doctrine update request cargo without direct doctrine mutation authority.\n",
)
.with_context(|| format!("failed to seed {}", repo.join("README.md").display()))?;
fs::write(
repo.join("AGENTS.md"),
"# Repo Agent Doctrine\n\nExisting doctrine stays untouched by the request artifact.\n",
)
.with_context(|| format!("failed to seed {}", repo.join("AGENTS.md").display()))?;
git(["add", "README.md", "AGENTS.md"], &repo)?;
git(
[
"commit",
"-m",
"Seed repo doctrine update request smoke body",
],
&repo,
)?;
git(
[
"switch",
"-c",
"epiphany/repo-doctrine-update-request-family",
],
&repo,
)?;
let item = "repo-doctrine-update-request-family";
let target_path = ".epiphany/doctrine-update-requests/repo-doctrine-update-request-family.toml";
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-doctrine-update-request-family-smoke",
"--topic",
"repo-doctrine-update-request-family",
],
&root,
)?;
cargo_json(
&manifest,
"epiphany-swarm",
&[
"online",
"--workspace",
path_str(&repo)?,
"--epiphany-root",
path_str(&root)?,
"--runtime-id",
"repo-doctrine-update-request-family-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 for a reviewed AGENTS.md doctrine update while preserving Mind, Soul, and maintainer gates.",
"--candidate-action-ref",
"candidate-action://repo-doctrine-update-request-family/agents-update",
"--public-discussion-ref",
"epiphany-global/persona-collaboration/repo-doctrine-update-request-family",
],
&root,
)?;
let plan = cargo_json(
&manifest,
"epiphany-work",
&[
"derive-plan",
"--workspace",
path_str(&repo)?,
"--item",
item,
"--action-family",
"repo-doctrine-update-request",
"--model-ref",
"repo-doctrine-update-request-family-smoke-imagination-v1",
"--model-authored",
],
&root,
)?;
for _ in 0..4 {
cargo_json(
&manifest,
"epiphany-work",
&[
"tick",
"--workspace",
path_str(&repo)?,
"--epiphany-root",
path_str(&root)?,
"--item",
item,
"--cooldown-seconds",
"0",
],
&root,
)?;
}
let close = read_json(
&repo
.join(".epiphany")
.join("work")
.join(format!("work-close-{item}.json")),
)?;
let request_text = fs::read_to_string(repo.join(target_path))
.with_context(|| format!("failed to read {}", repo.join(target_path).display()))?;
let agents_text = fs::read_to_string(repo.join("AGENTS.md"))
.with_context(|| format!("failed to read {}", repo.join("AGENTS.md").display()))?;
require_eq(
&plan,
&["derivation", "safeActionFamily"],
"repo.doctrine_update_request",
)?;
require_eq(&close, &["status"], "closed")?;
require_eq(&close, &["soul", "verdict"], "passed")?;
require_eq(
&close,
&["closureReview", "familyAssertions", "status"],
"passed",
)?;
require_bool(
&close,
&["closureReview", "sourceGrounding", "pathScopeMatched"],
true,
)?;
require_bool(&close, &["privateStateExposed"], false)?;
require_text(
&request_text,
"schema_version = \"epiphany.repo_doctrine_update_request.v0\"",
)?;
require_text(
&request_text,
"safe_action_family = \"repo.doctrine_update_request\"",
)?;
require_text(&request_text, "[request]")?;
require_text(&request_text, "status = \"awaiting-doctrine-review\"")?;
require_text(&request_text, "routing_owner = \"Self\"")?;
require_text(
&request_text,
"required_reviewers = [\"Maintainer\", \"Mind\", \"Soul\"]",
)?;
require_text(&request_text, "doctrine_admission_owner = \"Mind\"")?;
require_text(&request_text, "mutation_owner = \"Hands\"")?;
require_text(
&request_text,
"requested_effect = \"review-repo-agent-doctrine-update\"",
)?;
require_text(&request_text, "doctrine_target = \"AGENTS.md\"")?;
require_text(&request_text, "requires_source_grounding = true")?;
require_text(&request_text, "maintainer_review_required = true")?;
require_text(&request_text, "[antecedents]")?;
require_text(&request_text, "persona_or_human_feedback_required = true")?;
require_text(&request_text, "imagination_plan_required = true")?;
require_text(&request_text, "mind_adoption_required = true")?;
require_text(&request_text, "soul_review_required = true")?;
require_text(&request_text, "maintainer_review_required = true")?;
require_text(&request_text, "[required_receipts]")?;
require_text(
&request_text,
"imagination_plan = \"epiphany.repo_work_imagination_action_items_receipt.v0\"",
)?;
require_text(
&request_text,
"mind_adoption = \"epiphany.repo_work_mind_adoption_decision.v0\"",
)?;
require_text(
&request_text,
"soul_review = \"epiphany.repo_work_closure_review.v0\"",
)?;
require_text(
&request_text,
"maintainer_review = \"gamecult.maintainer.review_receipt.v0\"",
)?;
require_text(
&request_text,
"hands_commit = \"epiphany.hands.commit_receipt\"",
)?;
require_text(&request_text, "[doctrine_packet]")?;
require_text(&request_text, "requires_current_doctrine_ref = true")?;
require_text(&request_text, "requires_proposed_change_summary = true")?;
require_text(&request_text, "requires_invariant_impact = true")?;
require_text(&request_text, "requires_rehydration_impact = true")?;
require_text(&request_text, "requires_rollback_plan = true")?;
require_text(
&request_text,
"requires_private_state_redaction_check = true",
)?;
require_text(&request_text, "[authority]")?;
require_text(&request_text, "direct_doctrine_mutation_authority = false")?;
require_text(&request_text, "direct_hands_authority = false")?;
require_text(&request_text, "direct_mind_state_commit = false")?;
require_text(&request_text, "publication_authorized = false")?;
require_text(&request_text, "merge_authorized = false")?;
require_text(&request_text, "service_lifecycle_authority = false")?;
require_text(&request_text, "cross_body_mutation_authorized = false")?;
require_text(&request_text, "private_verse_rummaging = false")?;
require_text(&request_text, "maintainer_review_required = true")?;
require_text(&request_text, "mind_admission_required = true")?;
require_text(&request_text, "hands_receipts_required = true")?;
require_text(&request_text, "private_state_exposed = false")?;
require_text(
&agents_text,
"Existing doctrine stays untouched by the request artifact.",
)?;
let summary = json!({
"schemaVersion": "epiphany.repo_doctrine_update_request_family_smoke.v0",
"status": "ok",
"smokeDir": smoke_dir,
"repo": repo,
"branch": git_output(["branch", "--show-current"], &repo)?,
"item": item,
"safeActionFamily": plan["derivation"]["safeActionFamily"],
"targetPath": target_path,
"closeStatus": close["status"],
"soulVerdict": close["soul"]["verdict"],
"familyAssertionsStatus": close["closureReview"]["familyAssertions"]["status"],
"pathScopeMatched": close["closureReview"]["sourceGrounding"]["pathScopeMatched"],
"awaitingDoctrineReview": true,
"doctrineMutationAuthorized": false,
"handsAuthorityGranted": false,
"publicationAuthorized": false,
"mergeAuthorized": false,
"serviceLifecycleAuthority": 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<Value> {
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<const N: usize>(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 git_output<const N: usize>(args: [&str; N], cwd: &Path) -> Result<String> {
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(String::from_utf8_lossy(&output.stdout).trim().to_string())
} 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 read_json(path: &Path) -> Result<Value> {
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 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("<missing>");
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 require_text(haystack: &str, needle: &str) -> Result<()> {
if haystack.contains(needle) {
Ok(())
} else {
Err(anyhow!(
"expected doctrine update request text to contain {needle:?}"
))
}
}
fn take_path(
args: &mut std::iter::Peekable<impl Iterator<Item = String>>,
flag: &str,
) -> Result<PathBuf> {
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()))
}

View file

@ -1,468 +0,0 @@
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,
}
impl Args {
fn parse() -> Result<Self> {
let mut root = env::current_dir().context("failed to resolve current directory")?;
let mut args = env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--root" => root = take_path(&mut args, "--root")?,
other => return Err(anyhow!("unexpected argument {other:?}")),
}
}
Ok(Self { root })
}
}
fn run_smoke(args: Args) -> Result<Value> {
let root = args
.root
.canonicalize()
.with_context(|| format!("failed to resolve {}", args.root.display()))?;
let smoke_root = root.join(".epiphany-smoke");
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(&smoke_root)
.with_context(|| format!("failed to create {}", smoke_root.display()))?;
let stamp = Utc::now().format("%Y%m%d-%H%M%S").to_string();
let smoke_dir = smoke_root.join(format!("repo-mind-evidence-guard-{stamp}"));
fs::create_dir(&smoke_dir)
.with_context(|| format!("failed to claim fresh smoke dir {}", 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 Evidence Guard Smoke\n\nThis repository proves Mind refuses proofless action items 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 evidence guard smoke body"],
&repo,
)?;
git(["switch", "-c", "epiphany/repo-mind-evidence-guard"], &repo)?;
let item = "repo-mind-evidence-guard";
let planned_path = "notes/epiphany-work/repo-mind-evidence-guard.md";
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-evidence-guard-smoke",
"--topic",
"repo-mind-evidence-guard",
],
&root,
)?;
cargo_json(
&manifest,
"epiphany-swarm",
&[
"online",
"--workspace",
path_str(&repo)?,
"--epiphany-root",
path_str(&root)?,
"--runtime-id",
"repo-mind-evidence-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 proofless action item before Hands receives authority.",
"--candidate-action-ref",
"candidate-action://repo-mind-evidence-guard/proofless-action",
"--public-discussion-ref",
"epiphany-global/persona-collaboration/repo-mind-evidence-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-evidence-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",
planned_path,
],
&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"]["verificationAsks"] = json!([]);
tampered_plan["derivation"]["actionItemReceipt"]["planningFacets"]["evidenceNeeds"] = json!([]);
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("lacks verification or evidence needs") {
return Err(anyhow!(
"adopt failure did not name missing evidence needs: {adopt_failure}"
));
}
let refusal_path = repo
.join(".epiphany")
.join("work")
.join("work-mind-adopt-repo-mind-evidence-guard.json");
let refusal = read_json(&refusal_path)?;
require_eq(
&refusal,
&["schemaVersion"],
"epiphany.repo_work_mind_adoption_decision.v0",
)?;
require_eq(&refusal, &["status"], "refused-missing-evidence-needs")?;
require_eq(
&refusal,
&["interpretation", "inputSummary", "safeActionFamily"],
"repo.markdown_planning_note",
)?;
require_eq(
&refusal,
&["interpretation", "inputSummary", "planChangedPaths", "0"],
planned_path,
)?;
require_bool(
&refusal,
&["interpretation", "classification", "actionItemAccepted"],
false,
)?;
require_bool(
&refusal,
&["interpretation", "classification", "safeFamilyRecognized"],
true,
)?;
require_bool(
&refusal,
&["interpretation", "classification", "requestedPathsDeclared"],
true,
)?;
require_bool(
&refusal,
&[
"interpretation",
"classification",
"requestedPathsMatchPlan",
],
true,
)?;
require_bool(
&refusal,
&[
"interpretation",
"classification",
"verificationAsksPresent",
],
false,
)?;
require_bool(
&refusal,
&["interpretation", "classification", "evidenceNeedsPresent"],
false,
)?;
require_text_field(
&refusal,
&["interpretation", "refusalReasons", "0"],
"lacks verification or evidence needs",
)?;
require_bool(&refusal, &["gates", "safeFamilyRecognized"], true)?;
require_bool(&refusal, &["gates", "requestedPathsMatchPlan"], true)?;
require_bool(&refusal, &["gates", "verificationAsksPresent"], false)?;
require_bool(&refusal, &["gates", "evidenceNeedsPresent"], false)?;
require_bool(&refusal, &["gates", "branchLocalOnly"], false)?;
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_explicit_soul_evidence_needs",
)?;
require_bool(&refusal, &["privateStateExposed"], false)?;
let adoption_receipt_path = repo
.join(".epiphany")
.join("work")
.join("work-adopt-repo-mind-evidence-guard.json");
if adoption_receipt_path.exists() {
return Err(anyhow!(
"adoption receipt was written despite missing-evidence refusal: {}",
adoption_receipt_path.display()
));
}
Ok(json!({
"schemaVersion": "epiphany.repo_work_mind_evidence_guard_smoke.v0",
"status": "ok",
"smokeDir": smoke_dir,
"repo": repo,
"item": item,
"planChangedPath": planned_path,
"mindDecisionStatus": refusal["status"],
"actionItemAccepted": false,
"safeFamilyRecognized": true,
"requestedPathsMatchPlan": true,
"verificationAsksPresent": false,
"evidenceNeedsPresent": 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<Value> {
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<String> {
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<const N: usize>(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<Value> {
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::<usize>() {
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<Item = String>, name: &str) -> Result<PathBuf> {
Ok(PathBuf::from(
args.next()
.ok_or_else(|| anyhow!("missing value for {name}"))?,
))
}

View file

@ -1,457 +0,0 @@
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,
}
impl Args {
fn parse() -> Result<Self> {
let mut root = env::current_dir().context("failed to resolve current directory")?;
let mut args = env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--root" => root = take_path(&mut args, "--root")?,
other => return Err(anyhow!("unexpected argument {other:?}")),
}
}
Ok(Self { root })
}
}
fn run_smoke(args: Args) -> Result<Value> {
let root = args
.root
.canonicalize()
.with_context(|| format!("failed to resolve {}", args.root.display()))?;
let smoke_root = root.join(".epiphany-smoke");
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(&smoke_root)
.with_context(|| format!("failed to create {}", smoke_root.display()))?;
let stamp = Utc::now().format("%Y%m%d-%H%M%S").to_string();
let smoke_dir = smoke_root.join(format!("repo-mind-path-guard-{stamp}"));
fs::create_dir(&smoke_dir)
.with_context(|| format!("failed to claim fresh smoke dir {}", 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 Path Guard Smoke\n\nThis repository proves Mind refuses mismatched requested paths 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 path guard smoke body"],
&repo,
)?;
git(["switch", "-c", "epiphany/repo-mind-path-guard"], &repo)?;
let item = "repo-mind-path-guard";
let planned_path = "notes/epiphany-work/repo-mind-path-guard.md";
let tampered_requested_path = "notes/epiphany-work/alien-path.md";
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-path-guard-smoke",
"--topic",
"repo-mind-path-guard",
],
&root,
)?;
cargo_json(
&manifest,
"epiphany-swarm",
&[
"online",
"--workspace",
path_str(&repo)?,
"--epiphany-root",
path_str(&root)?,
"--runtime-id",
"repo-mind-path-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 requested path before Hands receives authority.",
"--candidate-action-ref",
"candidate-action://repo-mind-path-guard/tampered-path",
"--public-discussion-ref",
"epiphany-global/persona-collaboration/repo-mind-path-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-path-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",
planned_path,
],
&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"]["requestedPaths"] =
json!([tampered_requested_path]);
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("do not match plan changed paths") {
return Err(anyhow!(
"adopt failure did not name requested-path mismatch: {adopt_failure}"
));
}
let refusal_path = repo
.join(".epiphany")
.join("work")
.join("work-mind-adopt-repo-mind-path-guard.json");
let refusal = read_json(&refusal_path)?;
require_eq(
&refusal,
&["schemaVersion"],
"epiphany.repo_work_mind_adoption_decision.v0",
)?;
require_eq(&refusal, &["status"], "refused-requested-path-mismatch")?;
require_eq(
&refusal,
&["interpretation", "inputSummary", "safeActionFamily"],
"repo.markdown_planning_note",
)?;
require_eq(
&refusal,
&["interpretation", "inputSummary", "requestedPaths", "0"],
tampered_requested_path,
)?;
require_eq(
&refusal,
&["interpretation", "inputSummary", "planChangedPaths", "0"],
planned_path,
)?;
require_bool(
&refusal,
&["interpretation", "classification", "actionItemAccepted"],
false,
)?;
require_bool(
&refusal,
&["interpretation", "classification", "safeFamilyRecognized"],
true,
)?;
require_bool(
&refusal,
&["interpretation", "classification", "requestedPathsDeclared"],
true,
)?;
require_bool(
&refusal,
&[
"interpretation",
"classification",
"requestedPathsMatchPlan",
],
false,
)?;
require_text_field(
&refusal,
&["interpretation", "refusalReasons", "0"],
"do not match plan changed paths",
)?;
require_bool(&refusal, &["gates", "safeFamilyRecognized"], true)?;
require_bool(&refusal, &["gates", "requestedPathsMatchPlan"], false)?;
require_bool(&refusal, &["gates", "branchLocalOnly"], false)?;
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_matching_requested_paths",
)?;
require_bool(&refusal, &["privateStateExposed"], false)?;
let adoption_receipt_path = repo
.join(".epiphany")
.join("work")
.join("work-adopt-repo-mind-path-guard.json");
if adoption_receipt_path.exists() {
return Err(anyhow!(
"adoption receipt was written despite requested-path mismatch refusal: {}",
adoption_receipt_path.display()
));
}
Ok(json!({
"schemaVersion": "epiphany.repo_work_mind_path_guard_smoke.v0",
"status": "ok",
"smokeDir": smoke_dir,
"repo": repo,
"item": item,
"tamperedRequestedPath": tampered_requested_path,
"planChangedPath": planned_path,
"mindDecisionStatus": refusal["status"],
"actionItemAccepted": false,
"safeFamilyRecognized": true,
"requestedPathsMatchPlan": 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<Value> {
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<String> {
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<const N: usize>(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<Value> {
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::<usize>() {
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<Item = String>, name: &str) -> Result<PathBuf> {
Ok(PathBuf::from(
args.next()
.ok_or_else(|| anyhow!("missing value for {name}"))?,
))
}

View file

@ -1,433 +0,0 @@
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,
}
impl Args {
fn parse() -> Result<Self> {
let mut root = env::current_dir().context("failed to resolve current directory")?;
let mut args = env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--root" => root = take_path(&mut args, "--root")?,
other => return Err(anyhow!("unexpected argument {other:?}")),
}
}
Ok(Self { root })
}
}
fn run_smoke(args: Args) -> Result<Value> {
let root = args
.root
.canonicalize()
.with_context(|| format!("failed to resolve {}", args.root.display()))?;
let smoke_root = root.join(".epiphany-smoke");
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(&smoke_root)
.with_context(|| format!("failed to create {}", smoke_root.display()))?;
let stamp = Utc::now().format("%Y%m%d-%H%M%S").to_string();
let smoke_dir = smoke_root.join(format!("repo-mind-safe-family-guard-{stamp}"));
fs::create_dir(&smoke_dir)
.with_context(|| format!("failed to claim fresh smoke dir {}", 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<Value> {
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<String> {
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<const N: usize>(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<Value> {
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::<usize>() {
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<Item = String>, name: &str) -> Result<PathBuf> {
Ok(PathBuf::from(
args.next()
.ok_or_else(|| anyhow!("missing value for {name}"))?,
))
}

View file

@ -1,616 +0,0 @@
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,
}
impl Args {
fn parse() -> Result<Self> {
let mut root = env::current_dir().context("failed to resolve current directory")?;
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")?,
other => return Err(anyhow!("unexpected argument {other:?}")),
}
}
Ok(Self { root })
}
}
fn run_smoke(args: Args) -> Result<Value> {
let root = args
.root
.canonicalize()
.with_context(|| format!("failed to resolve {}", args.root.display()))?;
let smoke_root = root.join(".epiphany-smoke");
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(&smoke_root)
.with_context(|| format!("failed to create {}", smoke_root.display()))?;
let stamp = Utc::now().format("%Y%m%d-%H%M%S").to_string();
let smoke_dir = smoke_root.join(format!("repo-planning-facets-{stamp}"));
fs::create_dir(&smoke_dir)
.with_context(|| format!("failed to claim fresh smoke dir {}", 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 Planning Facets Smoke\n\nThis repository proves richer Imagination planning cargo without command authority.\n",
)
.with_context(|| format!("failed to seed {}", repo.join("README.md").display()))?;
git(["add", "README.md"], &repo)?;
git(
["commit", "-m", "Seed repo planning facets smoke body"],
&repo,
)?;
git(["switch", "-c", "epiphany/repo-planning-facets"], &repo)?;
let item = "repo-planning-facets";
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-planning-facets-smoke",
"--topic",
"repo-planning-facets",
],
&root,
)?;
cargo_json(
&manifest,
"epiphany-swarm",
&[
"online",
"--workspace",
path_str(&repo)?,
"--epiphany-root",
path_str(&root)?,
"--runtime-id",
"repo-planning-facets-smoke",
"--local-verse-store",
path_str(&local_verse)?,
],
&root,
)?;
cargo_json(
&manifest,
"epiphany-work",
&[
"accept",
"--workspace",
path_str(&repo)?,
"--from",
"persona",
"--item",
item,
"--summary",
"Let Imagination preserve richer planning facets before Self chooses the safe move.",
"--candidate-action-ref",
"candidate-action://repo-planning-facets/action-items",
"--public-discussion-ref",
"epiphany-global/persona-collaboration/repo-planning-facets",
],
&root,
)?;
let plan = cargo_json(
&manifest,
"epiphany-work",
&[
"derive-plan",
"--workspace",
path_str(&repo)?,
"--item",
item,
"--action-family",
"planning-note",
"--model-ref",
"repo-planning-facets-smoke-imagination-v1",
"--model-authored",
"--assumption",
"Persona feedback is public enough to route into Imagination.",
"--constraint",
"Hands must only write the declared planning note path.",
"--non-goal",
"Do not publish, merge, deploy, or mutate service lifecycle state.",
"--open-question",
"Should Self adopt this as the next branch-local move?",
"--decision-point",
"Self may adopt, defer, or ask Imagination for a narrower safe family.",
"--evidence-need",
"Soul needs the action-item receipt, plan receipt, changed-path proof, and private-state seal.",
],
&root,
)?;
let action_items_path = repo
.join(".epiphany")
.join("work")
.join("work-action-items-repo-planning-facets.json");
let action_items = read_json(&action_items_path)?;
require_eq(
&action_items,
&["schemaVersion"],
"epiphany.repo_work_imagination_action_items_receipt.v0",
)?;
require_eq(&action_items, &["status"], "proposed-for-self-mind-review")?;
require_eq(
&action_items,
&["actionItems", "0", "safeActionFamily"],
"repo.markdown_planning_note",
)?;
require_bool(
&action_items,
&["model", "operatorAuthoredShellDetails"],
false,
)?;
require_bool(
&action_items,
&["authority", "handsAuthorityGranted"],
false,
)?;
require_bool(&action_items, &["authority", "durableStateAdmitted"], false)?;
require_bool(&action_items, &["privateStateExposed"], false)?;
require_text_field(
&action_items,
&["actionItems", "0", "planningFacets", "assumptions", "0"],
"Persona feedback is public enough",
)?;
require_text_field(
&action_items,
&["actionItems", "0", "planningFacets", "constraints", "0"],
"declared planning note path",
)?;
require_text_field(
&action_items,
&["actionItems", "0", "planningFacets", "nonGoals", "0"],
"Do not publish",
)?;
require_text_field(
&action_items,
&["actionItems", "0", "planningFacets", "openQuestions", "0"],
"Should Self adopt",
)?;
require_text_field(
&action_items,
&["actionItems", "0", "planningFacets", "decisionPoints", "0"],
"Self may adopt",
)?;
require_text_field(
&action_items,
&["actionItems", "0", "planningFacets", "evidenceNeeds", "0"],
"Soul needs",
)?;
require_bool(
&action_items,
&[
"actionItems",
"0",
"planningFacets",
"handsCommandAuthority",
],
false,
)?;
require_bool(
&action_items,
&[
"actionItems",
"0",
"planningFacets",
"durableStateAuthority",
],
false,
)?;
require_eq(
&plan,
&[
"derivation",
"actionItemReceipt",
"planningFacets",
"constraints",
"0",
],
"Hands must only write the declared planning note path.",
)?;
for _ in 0..4 {
cargo_json(
&manifest,
"epiphany-work",
&[
"tick",
"--workspace",
path_str(&repo)?,
"--epiphany-root",
path_str(&root)?,
"--item",
item,
"--cooldown-seconds",
"0",
],
&root,
)?;
}
let adoption = read_json(
&repo
.join(".epiphany")
.join("work")
.join(format!("work-adopt-{item}.json")),
)?;
require_eq(
&adoption,
&["schemaVersion"],
"epiphany.repo_work_adoption_receipt.v0",
)?;
require_eq(
&adoption,
&["status"],
"approved-for-branch-local-hands-action",
)?;
require_eq(
&adoption,
&["mindAdoptionDecision", "schemaVersion"],
"epiphany.repo_work_mind_adoption_decision.v0",
)?;
require_eq(
&adoption,
&["mindAdoptionDecision", "status"],
"adopted-for-branch-local-hands",
)?;
require_eq(&adoption, &["mindAdoptionDecision", "owner"], "Mind")?;
require_bool(
&adoption,
&["mindAdoptionDecision", "gates", "selfPresentedActionItem"],
true,
)?;
require_bool(
&adoption,
&["mindAdoptionDecision", "gates", "mindReviewedEvidence"],
true,
)?;
require_bool(
&adoption,
&["mindAdoptionDecision", "gates", "mindInterpretedActionItem"],
true,
)?;
require_bool(
&adoption,
&["mindAdoptionDecision", "gates", "branchLocalOnly"],
true,
)?;
require_bool(
&adoption,
&["mindAdoptionDecision", "authority", "handsAuthorityGranted"],
false,
)?;
require_bool(
&adoption,
&["mindAdoptionDecision", "authority", "durableStateAdmitted"],
false,
)?;
require_bool(
&adoption,
&["mindAdoptionDecision", "authority", "publicationAuthorized"],
false,
)?;
require_bool(
&adoption,
&[
"mindAdoptionDecision",
"authority",
"serviceLifecycleAuthority",
],
false,
)?;
require_bool(
&adoption,
&["mindAdoptionDecision", "authority", "privateStateExposed"],
false,
)?;
let mind_adoption_path = value_at_path(&adoption, &["mindAdoptionDecision", "receiptPath"])
.and_then(Value::as_str)
.ok_or_else(|| anyhow!("missing Mind adoption decision receipt path"))?;
let mind_adoption = read_json(Path::new(mind_adoption_path))?;
require_eq(
&mind_adoption,
&["schemaVersion"],
"epiphany.repo_work_mind_adoption_decision.v0",
)?;
require_eq(
&mind_adoption,
&["interpretation", "schemaVersion"],
"epiphany.repo_work_mind_interpretation.v0",
)?;
require_eq(
&mind_adoption,
&["interpretation", "classification", "decisionKind"],
"branch-local-hands-adoption",
)?;
require_eq(
&mind_adoption,
&["interpretation", "inputSummary", "safeActionFamily"],
"repo.markdown_planning_note",
)?;
require_bool(
&mind_adoption,
&["interpretation", "classification", "actionItemAccepted"],
true,
)?;
require_bool(
&mind_adoption,
&["interpretation", "classification", "safeFamilyRecognized"],
true,
)?;
require_bool(
&mind_adoption,
&["interpretation", "classification", "requestedPathsDeclared"],
true,
)?;
require_eq(
&mind_adoption,
&["interpretation", "classification", "durableStateAdmission"],
"not-admitted",
)?;
require_eq(
&mind_adoption,
&["interpretation", "allowedTransitions", "0"],
"hands.branch_local_action",
)?;
require_eq(
&mind_adoption,
&["interpretation", "forbiddenTransitions", "0"],
"mind.durable_state_commit",
)?;
require_bool(
&mind_adoption,
&["interpretation", "privateStateExposed"],
false,
)?;
require_eq(
&mind_adoption,
&["adoptedActionItem", "safeActionFamily"],
"repo.markdown_planning_note",
)?;
require_text_field(&mind_adoption, &["rationale"], "Mind adopted")?;
require_bool(&mind_adoption, &["privateStateExposed"], false)?;
require_eq(
&adoption,
&["adoptedActionItem", "safeActionFamily"],
"repo.markdown_planning_note",
)?;
require_text_field(
&adoption,
&["adoptedActionItem", "planningFacets", "decisionPoints", "0"],
"Self may adopt",
)?;
require_bool(
&adoption,
&["adoptedActionItem", "handsCommandAuthority"],
false,
)?;
require_bool(
&adoption,
&["adoptedActionItem", "durableStateAuthority"],
false,
)?;
require_bool(
&adoption,
&["adoptedActionItem", "publicationAuthorized"],
false,
)?;
require_bool(
&adoption,
&["adoptedActionItem", "privateStateExposed"],
false,
)?;
require_bool(&adoption, &["authority", "handsAuthorityGranted"], true)?;
require_bool(&adoption, &["authority", "durableStateAdmitted"], false)?;
require_bool(&adoption, &["privateStateExposed"], false)?;
let close = read_json(
&repo
.join(".epiphany")
.join("work")
.join(format!("work-close-{item}.json")),
)?;
require_eq(&close, &["status"], "closed")?;
require_eq(&close, &["soul", "verdict"], "passed")?;
require_eq(
&close,
&["closureReview", "familyAssertions", "status"],
"passed",
)?;
require_bool(
&close,
&["closureReview", "sourceGrounding", "pathScopeMatched"],
true,
)?;
require_bool(&close, &["privateStateExposed"], false)?;
Ok(json!({
"schemaVersion": "epiphany.repo_planning_facets_smoke.v0",
"status": "ok",
"smokeDir": smoke_dir,
"repo": repo,
"item": item,
"safeActionFamily": "repo.markdown_planning_note",
"modelAuthored": true,
"assumptionsRecorded": true,
"constraintsRecorded": true,
"nonGoalsRecorded": true,
"openQuestionsRecorded": true,
"decisionPointsRecorded": true,
"evidenceNeedsRecorded": true,
"adoptedActionItemRecorded": true,
"mindAdoptionDecisionRecorded": true,
"mindInterpretationRecorded": true,
"mindDecisionHandsAuthorityGranted": false,
"mindDecisionDurableStateAdmitted": false,
"adoptionFacetsRecorded": true,
"handsAuthorityGranted": false,
"durableStateAdmitted": false,
"handsCommandAuthority": false,
"durableStateAuthority": false,
"adoptionHandsAuthorityGranted": true,
"adoptionDurableStateAdmitted": false,
"closeStatus": "closed",
"soulVerdict": "passed",
"familyAssertionsStatus": "passed",
"pathScopeMatched": true,
"privateStateExposed": false
}))
}
fn cargo_json(manifest_path: &Path, bin_name: &str, args: &[&str], cwd: &Path) -> Result<Value> {
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:\n{}",
String::from_utf8_lossy(&output.stderr)
));
}
serde_json::from_slice(&output.stdout)
.with_context(|| format!("{bin_name} returned invalid JSON"))
}
fn git<const N: usize>(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<Value> {
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 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::<usize>() {
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<Item = String>, name: &str) -> Result<PathBuf> {
Ok(PathBuf::from(
args.next()
.ok_or_else(|| anyhow!("missing value for {name}"))?,
))
}

File diff suppressed because it is too large Load diff

View file

@ -45,7 +45,7 @@ impl RepoDeploymentConfig {
pub(super) fn has_required_receipt_contract(&self) -> bool {
let receipts = &self.required_receipts;
receipts.mind_adoption == "epiphany.repo_work_mind_adoption_decision.v0"
receipts.mind_adoption == "epiphany.repo_work_plan_adoption_review.v0"
&& receipts.soul_review == "epiphany.repo_work_closure_review.v0"
&& receipts.maintainer_review == "gamecult.maintainer.review_receipt.v0"
&& receipts.secret_policy == "epiphany.repo_secret_policy_request.v0"
@ -226,7 +226,7 @@ impl RepoSecretPolicyRequest {
let receipts = &self.required_receipts;
receipts.source_grounding == "epiphany.eyes.evidence_packet"
&& receipts.soul_review == "epiphany.repo_work_closure_review.v0"
&& receipts.mind_adoption == "epiphany.repo_work_mind_adoption_decision.v0"
&& receipts.mind_adoption == "epiphany.repo_work_plan_adoption_review.v0"
&& receipts.maintainer_review == "gamecult.maintainer.review_receipt.v0"
&& receipts.bifrost_publication_review
== "gamecult.bifrost.publication_review_receipt.v0"
@ -382,7 +382,7 @@ impl RepoDependencyPolicyRequest {
let value = &self.required_receipts;
value.source_grounding == "epiphany.eyes.evidence_packet"
&& value.soul_review == "epiphany.repo_work_closure_review.v0"
&& value.mind_adoption == "epiphany.repo_work_mind_adoption_decision.v0"
&& value.mind_adoption == "epiphany.repo_work_plan_adoption_review.v0"
&& value.maintainer_review == "gamecult.maintainer.review_receipt.v0"
&& value.bifrost_publication_review == "gamecult.bifrost.publication_review_receipt.v0"
&& value.dependency_audit == "gamecult.supply_chain.dependency_audit_receipt.v0"
@ -536,7 +536,7 @@ impl RepoDeploymentRequest {
pub(super) fn has_receipt_contract(&self) -> bool {
let value = &self.required_receipts;
value.source_grounding == "epiphany.eyes.evidence_packet"
&& value.mind_adoption == "epiphany.repo_work_mind_adoption_decision.v0"
&& value.mind_adoption == "epiphany.repo_work_plan_adoption_review.v0"
&& value.soul_review == "epiphany.repo_work_closure_review.v0"
&& value.maintainer_review == "gamecult.maintainer.review_receipt.v0"
&& value.secret_policy == "epiphany.repo_secret_policy_request.v0"
@ -626,7 +626,7 @@ impl RepoDoctrineUpdateRequest {
),
(
"mind_adoption",
"epiphany.repo_work_mind_adoption_decision.v0",
"epiphany.repo_work_plan_adoption_review.v0",
),
("soul_review", "epiphany.repo_work_closure_review.v0"),
("maintainer_review", "gamecult.maintainer.review_receipt.v0"),

View file

@ -654,6 +654,13 @@ pub use mind_gateway::MindGatewayDecision;
pub use mind_gateway::MindGatewayReview;
pub use mind_gateway::MindInterpreterPromptInput;
pub use mind_gateway::MindStateCommitReceipt;
pub use mind_gateway::REPO_WORK_HANDS_GRANT_SCHEMA_VERSION;
pub use mind_gateway::REPO_WORK_HANDS_GRANT_TYPE;
pub use mind_gateway::REPO_WORK_PLAN_ADOPTION_REVIEW_SCHEMA_VERSION;
pub use mind_gateway::REPO_WORK_PLAN_ADOPTION_REVIEW_TYPE;
pub use mind_gateway::RepoWorkHandsGrant;
pub use mind_gateway::RepoWorkPlanAdoptionDecision;
pub use mind_gateway::RepoWorkPlanAdoptionReview;
pub use mind_gateway::build_mind_interpreter_prompt;
pub use mind_gateway::default_mind_cultnet_contracts;
pub use mind_gateway::mind_review_allows_state;
@ -787,6 +794,7 @@ pub use runtime_spine::advance_repo_work_modeling_route;
pub use runtime_spine::append_runtime_event;
pub use runtime_spine::clear_epiphany_job_binding_backend;
pub use runtime_spine::commit_initial_repo_work_modeling_route;
pub use runtime_spine::commit_repo_work_hands_grant;
pub use runtime_spine::commit_repo_work_map_admission;
pub use runtime_spine::complete_runtime_job;
pub use runtime_spine::create_runtime_job;
@ -810,6 +818,7 @@ pub use runtime_spine::put_mind_gateway_review;
pub use runtime_spine::put_mind_state_commit_receipt;
pub use runtime_spine::put_repo_work_modeling_finding;
pub use runtime_spine::put_repo_work_modeling_request;
pub use runtime_spine::put_repo_work_plan_adoption_review;
pub use runtime_spine::put_runtime_reorient_worker_result;
pub use runtime_spine::put_runtime_role_worker_result;
pub use runtime_spine::put_soul_verdict_receipt;
@ -831,10 +840,12 @@ pub use runtime_spine::runtime_mind_gateway_review;
pub use runtime_spine::runtime_mind_state_commit_receipt;
pub use runtime_spine::runtime_registered_document_types;
pub use runtime_spine::runtime_reorient_worker_result;
pub use runtime_spine::runtime_repo_work_hands_grant;
pub use runtime_spine::runtime_repo_work_map_entry;
pub use runtime_spine::runtime_repo_work_modeling_finding;
pub use runtime_spine::runtime_repo_work_modeling_request;
pub use runtime_spine::runtime_repo_work_modeling_route;
pub use runtime_spine::runtime_repo_work_plan_adoption_review;
pub use runtime_spine::runtime_role_worker_result;
pub use runtime_spine::runtime_schema_catalog_response;
pub use runtime_spine::runtime_soul_verdict_receipt;
@ -843,6 +854,7 @@ pub use runtime_spine::runtime_spine_status;
pub use runtime_spine::runtime_substrate_gate_repo_access_grant_receipt;
pub use runtime_spine::runtime_tool_invocation_statuses;
pub use runtime_spine::runtime_worker_launch_request;
pub use runtime_spine::validate_hands_action_authority;
pub use runtime_spine::write_runtime_hello_frame;
pub use runtime_spine::write_runtime_schema_catalog_json;
pub use soul_gateway::*;

View file

@ -22,6 +22,118 @@ pub const MIND_STATE_REJECTION_RECEIPT_SCHEMA_VERSION: &str =
"epiphany.mind.state_rejection_receipt.v0";
pub const MIND_VERSE_ADOPTION_RECEIPT_SCHEMA_VERSION: &str =
"epiphany.mind.verse_adoption_receipt.v0";
pub const REPO_WORK_PLAN_ADOPTION_REVIEW_TYPE: &str = "epiphany.repo_work.plan_adoption_review";
pub const REPO_WORK_PLAN_ADOPTION_REVIEW_SCHEMA_VERSION: &str =
"epiphany.repo_work_plan_adoption_review.v0";
pub const REPO_WORK_HANDS_GRANT_TYPE: &str = "epiphany.repo_work.hands_grant";
pub const REPO_WORK_HANDS_GRANT_SCHEMA_VERSION: &str = "epiphany.repo_work_hands_grant.v0";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum RepoWorkPlanAdoptionDecision {
Adopt,
Refuse,
Hold,
}
/// Mind-authored review of one immutable repo-work plan and its complete authority chain.
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.repo_work.plan_adoption_review",
schema = "RepoWorkPlanAdoptionReview"
)]
pub struct RepoWorkPlanAdoptionReview {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub review_id: String,
#[cultcache(key = 2)]
pub decision: RepoWorkPlanAdoptionDecision,
#[cultcache(key = 3)]
pub workspace_identity: String,
#[cultcache(key = 4)]
pub item: String,
#[cultcache(key = 5)]
pub plan_schema_version: String,
#[cultcache(key = 6)]
pub plan_id: String,
#[cultcache(key = 7)]
pub plan_sha256: String,
#[cultcache(key = 8)]
pub run_receipt_sha256: String,
#[cultcache(key = 9)]
pub plan_receipt_path: String,
#[cultcache(key = 10)]
pub run_receipt_path: String,
#[cultcache(key = 11)]
pub hands_intent_id: String,
#[cultcache(key = 12)]
pub queued_hands_review_id: String,
#[cultcache(key = 13)]
pub substrate_grant_receipt_id: String,
#[cultcache(key = 14)]
pub action_id: String,
#[cultcache(key = 15)]
pub action_command: String,
#[cultcache(key = 16)]
pub action_commit_message: String,
#[cultcache(key = 17)]
pub changed_paths: Vec<String>,
#[cultcache(key = 18)]
pub reviewed_at: String,
#[cultcache(key = 19)]
pub private_state_exposed: bool,
}
/// The only capability that converts an accepted Mind review into Hands authority.
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "epiphany.repo_work.hands_grant", schema = "RepoWorkHandsGrant")]
pub struct RepoWorkHandsGrant {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub grant_id: String,
#[cultcache(key = 2)]
pub adoption_review_id: String,
#[cultcache(key = 3)]
pub adoption_review_sha256: String,
#[cultcache(key = 4)]
pub workspace_identity: String,
#[cultcache(key = 5)]
pub item: String,
#[cultcache(key = 6)]
pub plan_id: String,
#[cultcache(key = 7)]
pub plan_sha256: String,
#[cultcache(key = 8)]
pub run_receipt_sha256: String,
#[cultcache(key = 9)]
pub plan_receipt_path: String,
#[cultcache(key = 10)]
pub run_receipt_path: String,
#[cultcache(key = 11)]
pub hands_intent_id: String,
#[cultcache(key = 12)]
pub queued_hands_review_id: String,
#[cultcache(key = 13)]
pub approved_hands_review_id: String,
#[cultcache(key = 14)]
pub substrate_grant_receipt_id: String,
#[cultcache(key = 15)]
pub action_id: String,
#[cultcache(key = 16)]
pub action_command: String,
#[cultcache(key = 17)]
pub action_commit_message: String,
#[cultcache(key = 18)]
pub allowed_operations: Vec<String>,
#[cultcache(key = 19)]
pub changed_paths: Vec<String>,
#[cultcache(key = 20)]
pub granted_at: String,
#[cultcache(key = 21)]
pub private_state_exposed: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]

View file

@ -39,6 +39,9 @@ use crate::mind_gateway::MIND_VERSE_ADOPTION_RECEIPT_TYPE;
use crate::mind_gateway::MindGatewayDecision;
use crate::mind_gateway::MindGatewayReview;
use crate::mind_gateway::MindStateCommitReceipt;
use crate::mind_gateway::RepoWorkHandsGrant;
use crate::mind_gateway::RepoWorkPlanAdoptionDecision;
use crate::mind_gateway::RepoWorkPlanAdoptionReview;
use crate::modeling_gateway::REPO_WORK_MAP_ENTRY_SCHEMA_VERSION;
use crate::modeling_gateway::REPO_WORK_MAP_ENTRY_TYPE;
use crate::modeling_gateway::REPO_WORK_MODELING_FINDING_SCHEMA_VERSION;
@ -106,6 +109,8 @@ use epiphany_tool_adapter::EpiphanyToolInvocationIntent;
use epiphany_tool_adapter::EpiphanyToolInvocationReceipt;
use serde::Deserialize;
use serde::Serialize;
use sha2::Digest;
use sha2::Sha256;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::fs;
@ -734,6 +739,8 @@ pub fn runtime_spine_cache(store_path: impl AsRef<Path>) -> Result<CultCache> {
cache.register_entry_type::<EpiphanyCoordinatorRunReceipt>()?;
cache.register_entry_type::<MindGatewayReview>()?;
cache.register_entry_type::<MindStateCommitReceipt>()?;
cache.register_entry_type::<RepoWorkPlanAdoptionReview>()?;
cache.register_entry_type::<RepoWorkHandsGrant>()?;
cache.register_entry_type::<RepoWorkModelingFinding>()?;
cache.register_entry_type::<RepoWorkModelingRequest>()?;
cache.register_entry_type::<RepoWorkModelingRoute>()?;
@ -1508,6 +1515,204 @@ pub fn runtime_hands_action_review(
cache.get::<HandsActionReview>(review_id)
}
pub fn put_repo_work_plan_adoption_review(
store_path: impl AsRef<Path>,
review: &RepoWorkPlanAdoptionReview,
) -> Result<()> {
validate_repo_work_plan_adoption_review(review)?;
let mut cache = runtime_spine_cache(store_path)?;
cache.pull_all_backing_stores()?;
require_identity(&cache)?;
if let Some(existing) = cache.get::<RepoWorkPlanAdoptionReview>(&review.review_id)? {
if existing == *review {
return Ok(());
}
return Err(anyhow!(
"repo-work plan adoption review ids are immutable; {} already names different bytes",
review.review_id
));
}
cache.put(&review.review_id, review)?;
Ok(())
}
pub fn runtime_repo_work_plan_adoption_review(
store_path: impl AsRef<Path>,
review_id: &str,
) -> Result<Option<RepoWorkPlanAdoptionReview>> {
validate_non_empty(review_id, "repo-work plan adoption review id")?;
let mut cache = runtime_spine_cache(store_path)?;
cache.pull_all_backing_stores()?;
cache.get::<RepoWorkPlanAdoptionReview>(review_id)
}
pub fn runtime_repo_work_hands_grant(
store_path: impl AsRef<Path>,
grant_id: &str,
) -> Result<Option<RepoWorkHandsGrant>> {
validate_non_empty(grant_id, "repo-work Hands grant id")?;
let mut cache = runtime_spine_cache(store_path)?;
cache.pull_all_backing_stores()?;
cache.get::<RepoWorkHandsGrant>(grant_id)
}
/// Atomically publishes the capability and its approved generic Hands review.
/// Neither document is observable unless both pass validation and the backing-store swap succeeds.
pub fn commit_repo_work_hands_grant(
store_path: impl AsRef<Path>,
grant: &RepoWorkHandsGrant,
approved_review: &HandsActionReview,
) -> Result<()> {
let mut cache = runtime_spine_cache(store_path)?;
cache.pull_all_backing_stores()?;
require_identity(&cache)?;
let adoption = cache
.get::<RepoWorkPlanAdoptionReview>(&grant.adoption_review_id)?
.ok_or_else(|| {
anyhow!("repo-work Hands grant requires its persisted Mind adoption review")
})?;
validate_repo_work_plan_adoption_review(&adoption)?;
let persisted_adoption_sha256 = format!("{:x}", Sha256::digest(serde_json::to_vec(&adoption)?));
if adoption.decision != RepoWorkPlanAdoptionDecision::Adopt
|| grant.schema_version != crate::mind_gateway::REPO_WORK_HANDS_GRANT_SCHEMA_VERSION
|| grant.private_state_exposed
|| !valid_lower_sha256(&grant.adoption_review_sha256)
|| grant.adoption_review_sha256 != persisted_adoption_sha256
|| !valid_lower_sha256(&grant.plan_sha256)
|| !valid_lower_sha256(&grant.run_receipt_sha256)
|| chrono::DateTime::parse_from_rfc3339(&grant.granted_at).is_err()
|| !safe_sorted_unique_paths(&grant.changed_paths)
|| grant.adoption_review_id != adoption.review_id
|| grant.workspace_identity != adoption.workspace_identity
|| grant.item != adoption.item
|| grant.plan_id != adoption.plan_id
|| grant.plan_sha256 != adoption.plan_sha256
|| grant.run_receipt_sha256 != adoption.run_receipt_sha256
|| grant.plan_receipt_path != adoption.plan_receipt_path
|| grant.run_receipt_path != adoption.run_receipt_path
|| grant.hands_intent_id != adoption.hands_intent_id
|| grant.queued_hands_review_id != adoption.queued_hands_review_id
|| grant.substrate_grant_receipt_id != adoption.substrate_grant_receipt_id
|| grant.action_id != adoption.action_id
|| grant.action_command != adoption.action_command
|| grant.action_commit_message != adoption.action_commit_message
|| grant.changed_paths != adoption.changed_paths
|| approved_review.review_id != grant.approved_hands_review_id
|| approved_review.intent_id != grant.hands_intent_id
|| approved_review.decision != "approved"
|| approved_review.allowed_operations != grant.allowed_operations
{
return Err(anyhow!(
"repo-work Hands grant does not exactly bind its accepted Mind review and approved Hands review"
));
}
let intent = cache
.get::<HandsActionIntent>(&grant.hands_intent_id)?
.ok_or_else(|| anyhow!("repo-work Hands grant requires its persisted Hands intent"))?;
let queued = cache
.get::<HandsActionReview>(&grant.queued_hands_review_id)?
.ok_or_else(|| anyhow!("repo-work Hands grant requires its queued Hands review"))?;
if intent.intent_id != queued.intent_id
|| queued.decision != "queued-for-adoption"
|| intent.substrate_gate_grant_receipt_id != grant.substrate_grant_receipt_id
|| intent.requested_paths != grant.changed_paths
{
return Err(anyhow!(
"repo-work Hands grant authority chain is inconsistent"
));
}
let existing_grant = cache.get::<RepoWorkHandsGrant>(&grant.grant_id)?;
let existing_review = cache.get::<HandsActionReview>(&approved_review.review_id)?;
match (existing_grant, existing_review) {
(None, None) => {}
(Some(existing_grant), Some(existing_review))
if existing_grant == *grant && existing_review == *approved_review =>
{
return Ok(());
}
_ => {
return Err(anyhow!(
"repo-work Hands grant and approved review ids are an immutable pair"
));
}
}
let (grant_entry, _) = cache.prepare_entry(&grant.grant_id, grant)?;
let (review_entry, _) = cache.prepare_entry(&approved_review.review_id, approved_review)?;
cache.put_prepared_batch(vec![grant_entry, review_entry])
}
fn validate_repo_work_plan_adoption_review(review: &RepoWorkPlanAdoptionReview) -> Result<()> {
validate_non_empty(&review.review_id, "repo-work plan adoption review id")?;
validate_non_empty(&review.workspace_identity, "repo-work workspace identity")?;
validate_non_empty(&review.item, "repo-work item")?;
validate_non_empty(&review.plan_id, "repo-work plan id")?;
validate_non_empty(&review.plan_sha256, "repo-work plan SHA-256")?;
validate_non_empty(&review.run_receipt_sha256, "repo-work run receipt SHA-256")?;
validate_non_empty(&review.plan_receipt_path, "repo-work plan receipt path")?;
validate_non_empty(&review.run_receipt_path, "repo-work run receipt path")?;
validate_non_empty(&review.hands_intent_id, "repo-work Hands intent")?;
validate_non_empty(
&review.queued_hands_review_id,
"repo-work queued Hands review",
)?;
validate_non_empty(
&review.substrate_grant_receipt_id,
"repo-work Substrate grant",
)?;
validate_non_empty(&review.action_id, "repo-work action id")?;
validate_non_empty(&review.action_command, "repo-work action command")?;
validate_non_empty(
&review.action_commit_message,
"repo-work action commit message",
)?;
validate_non_empty(&review.reviewed_at, "repo-work adoption review timestamp")?;
let valid_sha256 =
|value: &str| value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit());
let normalized_paths = review
.changed_paths
.iter()
.map(|path| path.replace('\\', "/"))
.collect::<Vec<_>>();
let mut sorted_paths = normalized_paths.clone();
sorted_paths.sort();
sorted_paths.dedup();
if review.schema_version != crate::mind_gateway::REPO_WORK_PLAN_ADOPTION_REVIEW_SCHEMA_VERSION
|| review.plan_schema_version != "epiphany.repo_work_action_plan_receipt.v0"
|| !valid_sha256(&review.plan_sha256)
|| !valid_sha256(&review.run_receipt_sha256)
|| review.private_state_exposed
|| review.changed_paths.is_empty()
|| !valid_lower_sha256(&review.plan_sha256)
|| !valid_lower_sha256(&review.run_receipt_sha256)
|| !Path::new(&review.plan_receipt_path).is_absolute()
|| !Path::new(&review.run_receipt_path).is_absolute()
|| chrono::DateTime::parse_from_rfc3339(&review.reviewed_at).is_err()
|| !safe_sorted_unique_paths(&review.changed_paths)
|| review.changed_paths != sorted_paths
{
return Err(anyhow!("invalid repo-work plan adoption review contract"));
}
Ok(())
}
fn valid_lower_sha256(value: &str) -> bool {
value.len() == 64
&& value
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}
fn safe_sorted_unique_paths(paths: &[String]) -> bool {
paths.windows(2).all(|pair| pair[0] < pair[1])
&& paths.iter().all(|path| {
!path.is_empty()
&& !Path::new(path).is_absolute()
&& !Path::new(path)
.components()
.any(|part| matches!(part, std::path::Component::ParentDir))
})
}
fn validate_hands_consequence_grant(
store_path: &Path,
intent_id: &str,
@ -1561,6 +1766,30 @@ fn validate_hands_consequence_grant(
Ok(())
}
/// Revalidates the persisted Hands/Substrate authority chain before an actuator
/// performs a consequence. Receipt writers call the same primitive again after
/// the consequence; this preflight prevents a stale or substituted grant from
/// authorizing the consequence in the first place.
pub fn validate_hands_action_authority(
store_path: impl AsRef<Path>,
intent_id: &str,
review_id: &str,
runtime_job_id: &str,
operation: &str,
changed_paths: &[String],
stated_grant_id: &str,
) -> Result<()> {
validate_hands_consequence_grant(
store_path.as_ref(),
intent_id,
review_id,
runtime_job_id,
operation,
changed_paths,
Some(stated_grant_id),
)
}
pub fn put_hands_patch_receipt(
store_path: impl AsRef<Path>,
receipt: &HandsPatchReceipt,
@ -4052,6 +4281,243 @@ mod tests {
use cultnet_rs::decode_cultnet_message_from_slice;
use tempfile::tempdir;
struct RepoWorkAdoptionFixture {
store: PathBuf,
review: RepoWorkPlanAdoptionReview,
grant: RepoWorkHandsGrant,
approved: HandsActionReview,
}
fn repo_work_adoption_fixture() -> Result<(tempfile::TempDir, RepoWorkAdoptionFixture)> {
let temp = tempdir()?;
let store = temp.path().join("runtime.msgpack");
initialize_runtime_spine(
&store,
RuntimeSpineInitOptions {
runtime_id: "epiphany-adoption-test".to_string(),
display_name: "Epiphany Adoption Test".to_string(),
created_at: "2026-07-13T00:00:00Z".to_string(),
},
)?;
let paths = vec!["src/lib.rs".to_string()];
let substrate = crate::substrate_gate_coordinator_implementation_grant(
"substrate-adoption-1".to_string(),
"hands-job-adoption-1".to_string(),
paths.clone(),
"2026-07-13T00:00:01Z".to_string(),
);
put_substrate_gate_repo_access_grant_receipt(&store, &substrate)?;
let intent = HandsActionIntent {
schema_version: crate::HANDS_ACTION_INTENT_SCHEMA_VERSION.to_string(),
intent_id: "hands-intent-adoption-1".to_string(),
runtime_job_id: "hands-job-adoption-1".to_string(),
binding_id: "implementation-worker".to_string(),
role: "epiphany-hands".to_string(),
authority_scope: "epiphany.role.implementation".to_string(),
requested_action: "runAcceptedWorkItem".to_string(),
requested_paths: paths.clone(),
substrate_gate_grant_receipt_id: substrate.receipt_id.clone(),
requested_at: "2026-07-13T00:00:02Z".to_string(),
contract: "Mind adoption test intent.".to_string(),
};
put_hands_action_intent(&store, &intent)?;
let queued = crate::hands_action_review_for_intent(
"hands-review-queued-adoption-1".to_string(),
&intent,
"queued-for-adoption".to_string(),
vec![
"patch".to_string(),
"command".to_string(),
"commit".to_string(),
],
vec!["Mind decision required.".to_string()],
"2026-07-13T00:00:03Z".to_string(),
);
put_hands_action_review(&store, &queued)?;
let review = RepoWorkPlanAdoptionReview {
schema_version: crate::mind_gateway::REPO_WORK_PLAN_ADOPTION_REVIEW_SCHEMA_VERSION
.to_string(),
review_id: "mind-adoption-1".to_string(),
decision: crate::mind_gateway::RepoWorkPlanAdoptionDecision::Adopt,
workspace_identity: "workspace-1".to_string(),
item: "item-1".to_string(),
plan_schema_version: "epiphany.repo_work_action_plan_receipt.v0".to_string(),
plan_id: "plan-1".to_string(),
plan_sha256: "a".repeat(64),
run_receipt_sha256: "b".repeat(64),
plan_receipt_path: temp.path().join("plan.json").display().to_string(),
run_receipt_path: temp.path().join("run.json").display().to_string(),
hands_intent_id: intent.intent_id.clone(),
queued_hands_review_id: queued.review_id.clone(),
substrate_grant_receipt_id: substrate.receipt_id.clone(),
action_id: "action-1".to_string(),
action_command: "cargo test --lib".to_string(),
action_commit_message: "Test immutable adoption".to_string(),
changed_paths: paths.clone(),
reviewed_at: "2026-07-13T00:00:04Z".to_string(),
private_state_exposed: false,
};
put_repo_work_plan_adoption_review(&store, &review)?;
let approved = crate::hands_action_review_for_intent(
"hands-review-approved-adoption-1".to_string(),
&intent,
"approved".to_string(),
vec![
"patch".to_string(),
"command".to_string(),
"commit".to_string(),
],
vec!["Bound to immutable Mind review.".to_string()],
"2026-07-13T00:00:05Z".to_string(),
);
let grant = RepoWorkHandsGrant {
schema_version: crate::mind_gateway::REPO_WORK_HANDS_GRANT_SCHEMA_VERSION.to_string(),
grant_id: "hands-grant-adoption-1".to_string(),
adoption_review_id: review.review_id.clone(),
adoption_review_sha256: format!("{:x}", Sha256::digest(serde_json::to_vec(&review)?)),
workspace_identity: review.workspace_identity.clone(),
item: review.item.clone(),
plan_id: review.plan_id.clone(),
plan_sha256: review.plan_sha256.clone(),
run_receipt_sha256: review.run_receipt_sha256.clone(),
plan_receipt_path: review.plan_receipt_path.clone(),
run_receipt_path: review.run_receipt_path.clone(),
hands_intent_id: review.hands_intent_id.clone(),
queued_hands_review_id: review.queued_hands_review_id.clone(),
approved_hands_review_id: approved.review_id.clone(),
substrate_grant_receipt_id: review.substrate_grant_receipt_id.clone(),
action_id: review.action_id.clone(),
action_command: review.action_command.clone(),
action_commit_message: review.action_commit_message.clone(),
allowed_operations: approved.allowed_operations.clone(),
changed_paths: review.changed_paths.clone(),
granted_at: "2026-07-13T00:00:05Z".to_string(),
private_state_exposed: false,
};
Ok((
temp,
RepoWorkAdoptionFixture {
store,
review,
grant,
approved,
},
))
}
#[test]
fn repo_work_mind_review_is_create_once_and_idempotent() -> Result<()> {
let (_temp, fixture) = repo_work_adoption_fixture()?;
put_repo_work_plan_adoption_review(&fixture.store, &fixture.review)?;
let mut counterfeit = fixture.review.clone();
counterfeit.action_command = "cargo test --all".to_string();
assert!(put_repo_work_plan_adoption_review(&fixture.store, &counterfeit).is_err());
assert_eq!(
runtime_repo_work_plan_adoption_review(&fixture.store, &fixture.review.review_id)?,
Some(fixture.review)
);
Ok(())
}
#[test]
fn repo_work_refuse_and_hold_cannot_mint_hands_authority() -> Result<()> {
for decision in [
crate::mind_gateway::RepoWorkPlanAdoptionDecision::Refuse,
crate::mind_gateway::RepoWorkPlanAdoptionDecision::Hold,
] {
let (_temp, mut fixture) = repo_work_adoption_fixture()?;
fixture.review.review_id = format!("mind-{decision:?}").to_lowercase();
fixture.review.decision = decision;
fixture.grant.adoption_review_id = fixture.review.review_id.clone();
put_repo_work_plan_adoption_review(&fixture.store, &fixture.review)?;
assert!(
commit_repo_work_hands_grant(&fixture.store, &fixture.grant, &fixture.approved)
.is_err()
);
assert!(
runtime_repo_work_hands_grant(&fixture.store, &fixture.grant.grant_id)?.is_none()
);
assert!(
runtime_hands_action_review(&fixture.store, &fixture.approved.review_id)?.is_none()
);
}
Ok(())
}
#[test]
fn repo_work_swapped_binding_fails_atomically() -> Result<()> {
let (_temp, mut fixture) = repo_work_adoption_fixture()?;
fixture.grant.plan_receipt_path = fixture.review.run_receipt_path.clone();
assert!(
commit_repo_work_hands_grant(&fixture.store, &fixture.grant, &fixture.approved)
.is_err()
);
assert!(runtime_repo_work_hands_grant(&fixture.store, &fixture.grant.grant_id)?.is_none());
assert!(
runtime_hands_action_review(&fixture.store, &fixture.approved.review_id)?.is_none()
);
Ok(())
}
#[test]
fn repo_work_counterfeit_review_digest_fails_atomically() -> Result<()> {
let (_temp, mut fixture) = repo_work_adoption_fixture()?;
fixture.grant.adoption_review_sha256 = "c".repeat(64);
assert!(
commit_repo_work_hands_grant(&fixture.store, &fixture.grant, &fixture.approved)
.is_err()
);
assert!(runtime_repo_work_hands_grant(&fixture.store, &fixture.grant.grant_id)?.is_none());
assert!(
runtime_hands_action_review(&fixture.store, &fixture.approved.review_id)?.is_none()
);
Ok(())
}
#[test]
fn repo_work_exact_adoption_chain_commits_grant_and_review_together() -> Result<()> {
let (_temp, fixture) = repo_work_adoption_fixture()?;
commit_repo_work_hands_grant(&fixture.store, &fixture.grant, &fixture.approved)?;
assert_eq!(
runtime_repo_work_hands_grant(&fixture.store, &fixture.grant.grant_id)?,
Some(fixture.grant)
);
assert_eq!(
runtime_hands_action_review(&fixture.store, &fixture.approved.review_id)?,
Some(fixture.approved)
);
Ok(())
}
#[test]
fn repo_work_grant_and_approved_review_are_create_once_as_a_pair() -> Result<()> {
let (_temp, fixture) = repo_work_adoption_fixture()?;
commit_repo_work_hands_grant(&fixture.store, &fixture.grant, &fixture.approved)?;
commit_repo_work_hands_grant(&fixture.store, &fixture.grant, &fixture.approved)?;
let mut counterfeit_grant = fixture.grant.clone();
counterfeit_grant.action_command = "cargo test --all".to_string();
assert!(
commit_repo_work_hands_grant(&fixture.store, &counterfeit_grant, &fixture.approved)
.is_err()
);
let mut counterfeit_review = fixture.approved.clone();
counterfeit_review.reasons = vec!["replacement opinion".to_string()];
assert!(
commit_repo_work_hands_grant(&fixture.store, &fixture.grant, &counterfeit_review)
.is_err()
);
assert_eq!(
runtime_repo_work_hands_grant(&fixture.store, &fixture.grant.grant_id)?,
Some(fixture.grant)
);
assert_eq!(
runtime_hands_action_review(&fixture.store, &fixture.approved.review_id)?,
Some(fixture.approved)
);
Ok(())
}
#[test]
fn runtime_spine_initializes_sessions_events_and_status() -> Result<()> {
let temp = tempdir()?;

View file

@ -63,10 +63,23 @@ validator; missing/changed node, edge, or link anchors stale their summaries.
There is still no production semantic index for graph claims, so Modeling index
availability is honestly unavailable rather than inferred from code search.
Next P0 cut: `epiphany-work run_adopt` currently synthesizes Mind ownership and
an approved Hands review from local JSON predicates. Route adoption through the
existing typed Mind gateway, immutably bound to the exact plan, and delete the
scheduler's ability to author Mind/Hands consequence.
The repo-work adoption authority rebuild is now implemented. `run_adopt`
consumes an immutable typed `RepoWorkPlanAdoptionReview` bound to exact
workspace, plan/run paths and SHA-256 bytes, action, command, commit message,
changed paths, queued Hands review, and Substrate grant. It atomically creates
an immutable `RepoWorkHandsGrant` plus approved Hands review; Refuse/Hold,
counterfeit digests, swapped paths, and same-ID replacement are rejected.
`run_tick` can only report `awaiting-mind-review`. `run_execute` rereads the
whole chain and revalidates persisted Substrate authority before PowerShell.
Soul closure now reloads typed runtime truth and cross-binds it to the execute
receipt; a swapped valid adoption chain fails. Six compiled smokes that taught
the deleted caller-writable `mindAdoptionDecision` model were removed.
Next architectural cut: finish the Aetheria lesson rather than polishing this
gate. Modeling needs a production semantic index over typed graph claims and a
durable frontier projection that automatically targets later Eyes/Hands
prompts. Memory-graph freshness is substrate; the living migration map is the
organ.
Epiphany is in an authority-provenance purification pass. The live question is not whether a command can produce a plausible document; it is whether the subsystem writing that document owns the fact it asserts.

View file

@ -0,0 +1,20 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.repo_work_hands_grant.schema.json",
"title": "Repo Work Hands Grant",
"type": "object", "additionalProperties": false,
"required": ["schemaVersion","grantId","adoptionReviewId","adoptionReviewSha256","workspaceIdentity","item","planId","planSha256","runReceiptSha256","planReceiptPath","runReceiptPath","handsIntentId","queuedHandsReviewId","approvedHandsReviewId","substrateGrantReceiptId","actionId","actionCommand","actionCommitMessage","allowedOperations","changedPaths","grantedAt","privateStateExposed"],
"properties": {
"schemaVersion":{"const":"epiphany.repo_work_hands_grant.v0"}, "grantId":{"type":"string","minLength":1},
"adoptionReviewId":{"type":"string","minLength":1}, "adoptionReviewSha256":{"type":"string","pattern":"^[0-9a-f]{64}$"},
"workspaceIdentity":{"type":"string","minLength":1}, "item":{"type":"string","minLength":1}, "planId":{"type":"string","minLength":1},
"planSha256":{"type":"string","pattern":"^[0-9a-f]{64}$"}, "runReceiptSha256":{"type":"string","pattern":"^[0-9a-f]{64}$"},
"planReceiptPath":{"type":"string","minLength":1}, "runReceiptPath":{"type":"string","minLength":1},
"handsIntentId":{"type":"string","minLength":1}, "queuedHandsReviewId":{"type":"string","minLength":1}, "approvedHandsReviewId":{"type":"string","minLength":1},
"substrateGrantReceiptId":{"type":"string","minLength":1}, "actionId":{"type":"string","minLength":1},
"actionCommand":{"type":"string","minLength":1}, "actionCommitMessage":{"type":"string","minLength":1},
"allowedOperations":{"type":"array","minItems":1,"uniqueItems":true,"items":{"enum":["patch","command","commit"]}},
"changedPaths":{"type":"array","minItems":1,"uniqueItems":true,"items":{"type":"string","minLength":1}},
"grantedAt":{"type":"string","format":"date-time"}, "privateStateExposed":{"const":false}
}
}

View file

@ -0,0 +1,21 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.repo_work_plan_adoption_review.schema.json",
"title": "Repo Work Plan Adoption Review",
"type": "object",
"additionalProperties": false,
"required": ["schemaVersion","reviewId","decision","workspaceIdentity","item","planSchemaVersion","planId","planSha256","runReceiptSha256","planReceiptPath","runReceiptPath","handsIntentId","queuedHandsReviewId","substrateGrantReceiptId","actionId","actionCommand","actionCommitMessage","changedPaths","reviewedAt","privateStateExposed"],
"properties": {
"schemaVersion": {"const":"epiphany.repo_work_plan_adoption_review.v0"},
"reviewId":{"type":"string","minLength":1}, "decision":{"enum":["adopt","refuse","hold"]},
"workspaceIdentity":{"type":"string","minLength":1}, "item":{"type":"string","minLength":1},
"planSchemaVersion":{"type":"string","minLength":1}, "planId":{"type":"string","minLength":1},
"planSha256":{"type":"string","pattern":"^[0-9a-f]{64}$"}, "runReceiptSha256":{"type":"string","pattern":"^[0-9a-f]{64}$"},
"planReceiptPath":{"type":"string","minLength":1}, "runReceiptPath":{"type":"string","minLength":1},
"handsIntentId":{"type":"string","minLength":1}, "queuedHandsReviewId":{"type":"string","minLength":1},
"substrateGrantReceiptId":{"type":"string","minLength":1}, "actionId":{"type":"string","minLength":1},
"actionCommand":{"type":"string","minLength":1}, "actionCommitMessage":{"type":"string","minLength":1},
"changedPaths":{"type":"array","minItems":1,"uniqueItems":true,"items":{"type":"string","minLength":1}},
"reviewedAt":{"type":"string","format":"date-time"}, "privateStateExposed":{"const":false}
}
}

View file

@ -1,6 +1,20 @@
{
"schemaVersion": "epiphany.cultnet_catalog.v0",
"schemas": [
{
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.repo_work_plan_adoption_review.schema.json",
"kind": "document_payload", "wireContracts": ["cultnet.schema.v0"],
"schemaVersion": "epiphany.repo_work_plan_adoption_review.v0",
"documentType": "epiphany.repo_work.plan_adoption_review",
"title": "Repo Work Plan Adoption Review", "path": "epiphany.repo_work_plan_adoption_review.schema.json"
},
{
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.repo_work_hands_grant.schema.json",
"kind": "document_payload", "wireContracts": ["cultnet.schema.v0"],
"schemaVersion": "epiphany.repo_work_hands_grant.v0",
"documentType": "epiphany.repo_work.hands_grant",
"title": "Repo Work Hands Grant", "path": "epiphany.repo_work_hands_grant.schema.json"
},
{
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.runtime.identity.schema.json",
"kind": "document_payload",

Binary file not shown.

View file

@ -661,4 +661,4 @@ current_status:
- 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, and now has repeatable closure and redacted-public-proof mouths. 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. 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`. Wrapper `tools/epiphany_local_run.ps1 -Mode repo-livefire-closure -Workspace E:\Projects\Huginn -RepoWorkItem huginn-inspection-readback-livefire -SkipBuild` exposes the same read-only proof through the local operator mouth; artifact `local-20260621-190156-803-2f85ba15` printed commit, path, gate, blocker, Soul verdict, Mind commit receipt, and `privateStateExposed=False` without push, merge, publication, or target mutation. New wrapper `tools/epiphany_local_run.ps1 -Mode repo-work-public-proof -Workspace E:\Projects\Huginn -RepoWorkItem huginn-inspection-readback-livefire -SkipBuild` exports the redacted proof bundle and repo-local Verse index without publishing; artifact `local-20260621-190645-345-4555b1dc` wrote `.epiphany\public\proof-bundles\repo-work-public-proof-huginn-inspection-readback-livefire.json`, proof id `repo-work-public-proof-huginn-inspection-readback-livefire`, SHA-256 `857545b199f3e37eb06c2e757e5cfc201958e46667bb1f8403fd333a22620f63`, nine artifact rows, zero publication rows, and `privateStateExposed=False`. swarm overview readback `local-20260621-190701-140-b9519ec6` sees the `REPO-WORK-PUBLIC-PROOF` row and action row from Huginn's repo-local Verse with the same SHA-256 while Bifrost publication remains missing. Launcher compact modes are now named compact operator-context modes instead of read-only modes, and `bifrost-public-proof` summaries print item, target public Verse, room, proof SHA-256, review receipts, and credit receipts so publication receipts are auditable without claiming upstream sync.
next_action: Cut repo-work Mind impersonation. `epiphany-work run_adopt` currently manufactures a Mind-owned adoption document and approved Hands review from local JSON predicates. Self/scheduler may route a plan, but only a resolved typed Mind gateway review immutably bound to the exact plan may authorize the admission primitive that creates Hands review. Delete the synthetic owner/interpreter path and prove that a structurally plausible plan without Mind receipt creates neither adoption nor Hands authority. Preserve the Aetheria lesson: memory-graph v1 now binds Modeling anatomy to state-store identity, accepted revision, and SHA-256 source bytes; launch and explicit refresh share validation; missing/changed anchors stale summaries. Modeling semantic graph indexing remains honestly unavailable until a real graph-claim indexer exists. The intentionally absent Other-world action still requires a typed intent and real owner. Keep closure's zero-substring invariant.
next_action: Build the Aetheria-shaped Modeling organ, not another freshness badge. Repo-work Mind impersonation is cut: adoption consumes an immutable typed Mind review, atomically publishes an immutable Hands grant/review pair, execution revalidates plan/run/Substrate authority before consequence, closure cross-binds runtime truth to the execute receipt, and hostile substitution/overwrite tests pass. The next owner is a production semantic index over typed Modeling graph claims plus a durable migration frontier automatically projected into Eyes/Hands prompts. Memory-graph v1 already binds anatomy to state-store identity, accepted revision, and source SHA-256; use that as substrate, then make Modeling name current migration bodies, invalidated beliefs, dependency order, and next bounded cuts across rehydration. The intentionally absent Other-world action still requires a typed intent and real owner. Keep closure's zero-substring invariant.