Add model verdict gate for repo closure
This commit is contained in:
parent
3fb2f04ded
commit
80cf753d58
6 changed files with 570 additions and 29 deletions
409
epiphany-core/src/bin/epiphany-repo-closure-model-smoke.rs
Normal file
409
epiphany-core/src/bin/epiphany-repo-closure-model-smoke.rs
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use chrono::Utc;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = Args::parse()?;
|
||||
let result = run_smoke(args)?;
|
||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Args {
|
||||
root: PathBuf,
|
||||
smoke_root: PathBuf,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
fn parse() -> Result<Self> {
|
||||
let mut root = env::current_dir().context("failed to resolve current directory")?;
|
||||
let mut smoke_root = root.join(".epiphany-smoke");
|
||||
let mut args = env::args().skip(1).peekable();
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--root" => root = take_path(&mut args, "--root")?,
|
||||
"--smoke-root" => smoke_root = take_path(&mut args, "--smoke-root")?,
|
||||
other => return Err(anyhow!("unexpected argument {other:?}")),
|
||||
}
|
||||
}
|
||||
Ok(Self { root, smoke_root })
|
||||
}
|
||||
}
|
||||
|
||||
fn run_smoke(args: Args) -> Result<Value> {
|
||||
let root = args
|
||||
.root
|
||||
.canonicalize()
|
||||
.with_context(|| format!("failed to resolve {}", args.root.display()))?;
|
||||
let manifest = root.join("epiphany-core").join("Cargo.toml");
|
||||
if !manifest.exists() {
|
||||
return Err(anyhow!(
|
||||
"could not find epiphany-core manifest at {}",
|
||||
manifest.display()
|
||||
));
|
||||
}
|
||||
fs::create_dir_all(&args.smoke_root)
|
||||
.with_context(|| format!("failed to create {}", args.smoke_root.display()))?;
|
||||
let stamp = Utc::now().format("%Y%m%d-%H%M%S").to_string();
|
||||
let smoke_dir = args.smoke_root.join(format!("closure-model-gate-{stamp}"));
|
||||
if smoke_dir.exists() {
|
||||
fs::remove_dir_all(&smoke_dir)
|
||||
.with_context(|| format!("failed to clear {}", smoke_dir.display()))?;
|
||||
}
|
||||
fs::create_dir_all(&smoke_dir)
|
||||
.with_context(|| format!("failed to create {}", smoke_dir.display()))?;
|
||||
|
||||
let repo = smoke_dir.join("repo-body");
|
||||
fs::create_dir_all(&repo).with_context(|| format!("failed to create {}", repo.display()))?;
|
||||
git(["init"], &repo)?;
|
||||
git(
|
||||
["config", "user.email", "epiphany-smoke@example.invalid"],
|
||||
&repo,
|
||||
)?;
|
||||
git(["config", "user.name", "Epiphany Smoke"], &repo)?;
|
||||
fs::write(
|
||||
repo.join("README.md"),
|
||||
"# Closure Model Gate Smoke\n\nThis repository proves model-authored closure verdicts.\n",
|
||||
)
|
||||
.with_context(|| format!("failed to seed {}", repo.join("README.md").display()))?;
|
||||
git(["add", "README.md"], &repo)?;
|
||||
git(["commit", "-m", "Seed closure model smoke repo"], &repo)?;
|
||||
git(["switch", "-c", "epiphany/closure-model-gate"], &repo)?;
|
||||
|
||||
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",
|
||||
"closure-model-gate-smoke",
|
||||
"--topic",
|
||||
"closure-model-gate",
|
||||
],
|
||||
&root,
|
||||
)?;
|
||||
cargo_json(
|
||||
&manifest,
|
||||
"epiphany-swarm",
|
||||
&[
|
||||
"online",
|
||||
"--workspace",
|
||||
path_str(&repo)?,
|
||||
"--epiphany-root",
|
||||
path_str(&root)?,
|
||||
"--runtime-id",
|
||||
"closure-model-gate-smoke",
|
||||
"--local-verse-store",
|
||||
path_str(&local_verse)?,
|
||||
],
|
||||
&root,
|
||||
)?;
|
||||
|
||||
let good = drive_item(
|
||||
&manifest,
|
||||
&root,
|
||||
&repo,
|
||||
"closure-model-pass",
|
||||
"Prove a model-authored closure verdict can explicitly pass after deterministic family checks.",
|
||||
"passed",
|
||||
"Modeling reviewed the verified status section, path scope, family assertions, and authority seal.",
|
||||
)?;
|
||||
let bad = drive_item(
|
||||
&manifest,
|
||||
&root,
|
||||
&repo,
|
||||
"closure-model-block",
|
||||
"Prove a model-authored closure verdict can block publication even when deterministic family checks pass.",
|
||||
"needs-work",
|
||||
"Modeling found the status text too thin and requires another branch-local pass before publication.",
|
||||
)?;
|
||||
|
||||
require_eq(&good.close, &["status"], "closed")?;
|
||||
require_eq(
|
||||
&good.close,
|
||||
&["closureReview", "modelingReview", "closureReview", "status"],
|
||||
"passed",
|
||||
)?;
|
||||
require_bool(
|
||||
&good.close,
|
||||
&[
|
||||
"closureReview",
|
||||
"modelingReview",
|
||||
"closureReview",
|
||||
"gateEnforced",
|
||||
],
|
||||
true,
|
||||
)?;
|
||||
require_eq(&bad.close, &["status"], "verification-failed")?;
|
||||
require_eq(&bad.close, &["soul", "verdict"], "failed")?;
|
||||
require_eq(
|
||||
&bad.close,
|
||||
&["closureReview", "modelingReview", "closureReview", "status"],
|
||||
"failed",
|
||||
)?;
|
||||
require_bool(
|
||||
&bad.close,
|
||||
&["closureReview", "modelingReview", "closureReview", "passed"],
|
||||
false,
|
||||
)?;
|
||||
require_eq(
|
||||
&bad.close,
|
||||
&["closureReview", "familyAssertions", "status"],
|
||||
"passed",
|
||||
)?;
|
||||
require_bool(
|
||||
&bad.close,
|
||||
&["closureReview", "sourceGrounding", "pathScopeMatched"],
|
||||
true,
|
||||
)?;
|
||||
require_bool(&good.close, &["privateStateExposed"], false)?;
|
||||
require_bool(&bad.close, &["privateStateExposed"], false)?;
|
||||
|
||||
let summary = json!({
|
||||
"schemaVersion": "epiphany.repo_work_closure_model_gate_smoke.v0",
|
||||
"status": "ok",
|
||||
"smokeDir": smoke_dir,
|
||||
"repo": repo,
|
||||
"branch": git_output(["branch", "--show-current"], &repo)?,
|
||||
"passItem": good.item,
|
||||
"passCloseStatus": good.close["status"],
|
||||
"passModelStatus": good.close["closureReview"]["modelingReview"]["closureReview"]["status"],
|
||||
"passModelGateEnforced": good.close["closureReview"]["modelingReview"]["closureReview"]["gateEnforced"],
|
||||
"blockItem": bad.item,
|
||||
"blockCloseStatus": bad.close["status"],
|
||||
"blockSoulVerdict": bad.close["soul"]["verdict"],
|
||||
"blockModelStatus": bad.close["closureReview"]["modelingReview"]["closureReview"]["status"],
|
||||
"blockFamilyAssertionsStatus": bad.close["closureReview"]["familyAssertions"]["status"],
|
||||
"blockPathScopeMatched": bad.close["closureReview"]["sourceGrounding"]["pathScopeMatched"],
|
||||
"privateStateExposed": false,
|
||||
});
|
||||
write_json(&smoke_dir.join("summary.json"), &summary)?;
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
struct ItemProof {
|
||||
item: String,
|
||||
close: Value,
|
||||
}
|
||||
|
||||
fn drive_item(
|
||||
manifest: &Path,
|
||||
root: &Path,
|
||||
repo: &Path,
|
||||
item: &str,
|
||||
summary: &str,
|
||||
model_verdict: &str,
|
||||
model_finding: &str,
|
||||
) -> Result<ItemProof> {
|
||||
cargo_json(
|
||||
manifest,
|
||||
"epiphany-work",
|
||||
&[
|
||||
"accept",
|
||||
"--workspace",
|
||||
path_str(repo)?,
|
||||
"--from",
|
||||
"persona",
|
||||
"--item",
|
||||
item,
|
||||
"--summary",
|
||||
summary,
|
||||
],
|
||||
root,
|
||||
)?;
|
||||
cargo_json(
|
||||
manifest,
|
||||
"epiphany-work",
|
||||
&[
|
||||
"derive-plan",
|
||||
"--workspace",
|
||||
path_str(repo)?,
|
||||
"--item",
|
||||
item,
|
||||
"--action-family",
|
||||
"repo-status-section",
|
||||
"--model-ref",
|
||||
"closure-model-gate-smoke-imagination-v1",
|
||||
"--model-authored",
|
||||
],
|
||||
root,
|
||||
)?;
|
||||
for _ in 0..3 {
|
||||
cargo_json(
|
||||
manifest,
|
||||
"epiphany-work",
|
||||
&[
|
||||
"tick",
|
||||
"--workspace",
|
||||
path_str(repo)?,
|
||||
"--epiphany-root",
|
||||
path_str(root)?,
|
||||
"--item",
|
||||
item,
|
||||
"--cooldown-seconds",
|
||||
"0",
|
||||
],
|
||||
root,
|
||||
)?;
|
||||
}
|
||||
let close = cargo_json(
|
||||
manifest,
|
||||
"epiphany-work",
|
||||
&[
|
||||
"close",
|
||||
"--workspace",
|
||||
path_str(repo)?,
|
||||
"--item",
|
||||
item,
|
||||
"--closure-model-ref",
|
||||
"closure-model-gate-smoke-soul-v1",
|
||||
"--model-authored",
|
||||
"--require-closure-model-verdict",
|
||||
"--closure-model-verdict",
|
||||
model_verdict,
|
||||
"--closure-model-finding",
|
||||
model_finding,
|
||||
],
|
||||
root,
|
||||
)?;
|
||||
Ok(ItemProof {
|
||||
item: item.to_string(),
|
||||
close,
|
||||
})
|
||||
}
|
||||
|
||||
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 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 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()))
|
||||
}
|
||||
|
|
@ -220,6 +220,9 @@ struct CloseArgs {
|
|||
verification_summary: Option<String>,
|
||||
modeling_summary: Option<String>,
|
||||
closure_model_ref: Option<String>,
|
||||
closure_model_verdict: Option<String>,
|
||||
closure_model_finding: Option<String>,
|
||||
require_closure_model_verdict: bool,
|
||||
model_authored: bool,
|
||||
state_revision: u64,
|
||||
}
|
||||
|
|
@ -729,6 +732,9 @@ fn parse_close_args(args: impl Iterator<Item = String>) -> Result<CloseArgs> {
|
|||
let mut verification_summary = None;
|
||||
let mut modeling_summary = None;
|
||||
let mut closure_model_ref = None;
|
||||
let mut closure_model_verdict = None;
|
||||
let mut closure_model_finding = None;
|
||||
let mut require_closure_model_verdict = false;
|
||||
let mut model_authored = false;
|
||||
let mut state_revision = 0_u64;
|
||||
|
||||
|
|
@ -754,6 +760,15 @@ fn parse_close_args(args: impl Iterator<Item = String>) -> Result<CloseArgs> {
|
|||
"--closure-model-ref" | "--model-ref" => {
|
||||
closure_model_ref = Some(take_string(&mut args, "--closure-model-ref")?);
|
||||
}
|
||||
"--closure-model-verdict" | "--model-verdict" => {
|
||||
closure_model_verdict = Some(take_string(&mut args, "--closure-model-verdict")?);
|
||||
}
|
||||
"--closure-model-finding" | "--model-finding" => {
|
||||
closure_model_finding = Some(take_string(&mut args, "--closure-model-finding")?);
|
||||
}
|
||||
"--require-closure-model-verdict" | "--require-model-verdict" => {
|
||||
require_closure_model_verdict = true;
|
||||
}
|
||||
"--model-authored" => model_authored = true,
|
||||
"--state-revision" => state_revision = take_u64(&mut args, "--state-revision")?,
|
||||
other => return Err(anyhow!("unexpected close argument {other:?}")),
|
||||
|
|
@ -769,6 +784,9 @@ fn parse_close_args(args: impl Iterator<Item = String>) -> Result<CloseArgs> {
|
|||
verification_summary,
|
||||
modeling_summary,
|
||||
closure_model_ref,
|
||||
closure_model_verdict,
|
||||
closure_model_finding,
|
||||
require_closure_model_verdict,
|
||||
model_authored,
|
||||
state_revision,
|
||||
})
|
||||
|
|
@ -2459,6 +2477,78 @@ fn push_assertion(assertions: &mut Vec<Value>, id: &str, passed: bool, summary:
|
|||
}));
|
||||
}
|
||||
|
||||
fn closure_model_review(
|
||||
model_authored: bool,
|
||||
model_ref: Option<&str>,
|
||||
verdict: Option<&str>,
|
||||
finding: Option<&str>,
|
||||
required: bool,
|
||||
) -> Result<(Value, bool)> {
|
||||
let normalized_verdict = verdict
|
||||
.map(|value| value.trim().to_ascii_lowercase())
|
||||
.filter(|value| !value.is_empty());
|
||||
if let Some(verdict) = normalized_verdict.as_deref() {
|
||||
match verdict {
|
||||
"passed" | "failed" | "needs-work" | "blocked" => {}
|
||||
other => {
|
||||
return Err(anyhow!(
|
||||
"unsupported closure model verdict {other:?}; expected passed, failed, needs-work, or blocked"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let gate_enforced = required || normalized_verdict.is_some();
|
||||
let passed = match normalized_verdict.as_deref() {
|
||||
Some("passed") => true,
|
||||
Some("failed" | "needs-work" | "blocked") => false,
|
||||
Some(_) => unreachable!(),
|
||||
None => !required,
|
||||
};
|
||||
let status = match normalized_verdict.as_deref() {
|
||||
Some("passed") => "passed",
|
||||
Some("failed" | "needs-work" | "blocked") => "failed",
|
||||
Some(_) => unreachable!(),
|
||||
None if required => "missing-required-verdict",
|
||||
None if model_authored || model_ref.is_some() => "provenance-only",
|
||||
None => "deterministic-fallback",
|
||||
};
|
||||
let reason = match status {
|
||||
"passed" => "model-authored closure verdict passed",
|
||||
"failed" => "model-authored closure verdict refused closure",
|
||||
"missing-required-verdict" => {
|
||||
"closure required a model-authored verdict but none was supplied"
|
||||
}
|
||||
"provenance-only" => "model provenance was recorded without making it a hard closure gate",
|
||||
_ => "deterministic closure checks are the active gate",
|
||||
};
|
||||
|
||||
Ok((
|
||||
json!({
|
||||
"schemaVersion": "epiphany.repo_work_model_closure_review.v0",
|
||||
"status": status,
|
||||
"passed": passed,
|
||||
"required": required,
|
||||
"gateEnforced": gate_enforced,
|
||||
"modelAuthored": model_authored || model_ref.is_some(),
|
||||
"modelRef": model_ref,
|
||||
"verdict": normalized_verdict,
|
||||
"finding": finding.map(compact_line),
|
||||
"reason": reason,
|
||||
"reviewedInputs": [
|
||||
"Hands commit receipt matched execute receipt",
|
||||
"Verification command result",
|
||||
"Declared versus actual git changed paths",
|
||||
"Safe-family committed-content assertions",
|
||||
"Authority seal and private-state exposure flag"
|
||||
],
|
||||
"operatorAuthoredShellDetails": false,
|
||||
"privateStateExposed": false
|
||||
}),
|
||||
passed,
|
||||
))
|
||||
}
|
||||
|
||||
fn read_committed_file(
|
||||
workspace: &Path,
|
||||
commit_sha: &str,
|
||||
|
|
@ -3191,6 +3281,13 @@ fn run_close(args: CloseArgs) -> Result<Value> {
|
|||
let (family_assertions, family_assertions_passed) =
|
||||
closure_family_assertions(&workspace, &commit_sha, &execute_receipt, &item)?;
|
||||
let closure_model_authored = args.model_authored || args.closure_model_ref.is_some();
|
||||
let (model_closure_review, model_closure_passed) = closure_model_review(
|
||||
closure_model_authored,
|
||||
args.closure_model_ref.as_deref(),
|
||||
args.closure_model_verdict.as_deref(),
|
||||
args.closure_model_finding.as_deref(),
|
||||
args.require_closure_model_verdict,
|
||||
)?;
|
||||
let closure_review_id = format!("repo-work-close-{item_slug}-closure-review");
|
||||
let closure_review_path = artifact_dir.join(format!("work-close-{item_slug}-review.json"));
|
||||
let closure_review = json!({
|
||||
|
|
@ -3222,15 +3319,17 @@ fn run_close(args: CloseArgs) -> Result<Value> {
|
|||
"pathScopeMatched": path_scope_matched,
|
||||
"commitStat": compact_multiline(&commit_stat),
|
||||
"commitReceiptMatchedExecuteReceipt": true,
|
||||
"familyAssertionsPassed": family_assertions_passed
|
||||
"familyAssertionsPassed": family_assertions_passed,
|
||||
"modelClosurePassed": model_closure_passed
|
||||
},
|
||||
"familyAssertions": family_assertions,
|
||||
"modelingReview": {
|
||||
"modelAuthored": closure_model_authored,
|
||||
"modelRef": args.closure_model_ref,
|
||||
"modelRef": args.closure_model_ref.clone(),
|
||||
"deterministicFallback": !closure_model_authored,
|
||||
"operatorAuthoredShellDetails": false,
|
||||
"summary": args.modeling_summary.clone()
|
||||
"summary": args.modeling_summary.clone(),
|
||||
"closureReview": model_closure_review
|
||||
},
|
||||
"authoritySeal": {
|
||||
"branchLocalOnly": true,
|
||||
|
|
@ -3241,11 +3340,13 @@ fn run_close(args: CloseArgs) -> Result<Value> {
|
|||
"privateStateExposed": false
|
||||
},
|
||||
"privateStateExposed": false,
|
||||
"nextSafeMove": "Soul may pass closure only when verification succeeds, actual git changed paths match the Hands-declared path scope, and safe-family assertions pass for known Imagination families."
|
||||
"nextSafeMove": "Soul may pass closure only when verification succeeds, actual git changed paths match the Hands-declared path scope, safe-family assertions pass for known Imagination families, and any supplied or required model-authored closure verdict passes."
|
||||
});
|
||||
write_json(&closure_review_path, &closure_review)?;
|
||||
let verification_passed =
|
||||
verification.status.success() && path_scope_matched && family_assertions_passed;
|
||||
let verification_passed = verification.status.success()
|
||||
&& path_scope_matched
|
||||
&& family_assertions_passed
|
||||
&& model_closure_passed;
|
||||
let soul_verdict_id = format!("repo-work-close-{item_slug}-soul-verdict");
|
||||
let soul_summary = args.verification_summary.unwrap_or_else(|| {
|
||||
if verification_passed {
|
||||
|
|
@ -3258,6 +3359,10 @@ fn run_close(args: CloseArgs) -> Result<Value> {
|
|||
format!(
|
||||
"Soul verification failed for branch-local commit {commit_sha}: safe-family assertions did not pass."
|
||||
)
|
||||
} else if !model_closure_passed {
|
||||
format!(
|
||||
"Soul verification failed for branch-local commit {commit_sha}: model-authored closure review did not pass."
|
||||
)
|
||||
} else {
|
||||
format!("Soul verification failed for branch-local commit {commit_sha}.")
|
||||
}
|
||||
|
|
@ -3295,6 +3400,11 @@ fn run_close(args: CloseArgs) -> Result<Value> {
|
|||
"Closure refused because the committed content did not satisfy known safe-family assertions."
|
||||
.to_string(),
|
||||
]
|
||||
} else if !model_closure_passed {
|
||||
vec![
|
||||
"Closure refused because the model-authored closure review was missing or non-passing."
|
||||
.to_string(),
|
||||
]
|
||||
} else {
|
||||
vec!["Closure verification command failed; publication remains blocked.".to_string()]
|
||||
},
|
||||
|
|
@ -4925,6 +5035,9 @@ fn run_tick(args: TickArgs) -> Result<Value> {
|
|||
"Modeling records scheduler-closed repo work item {item}; publication remains gated by Bifrost."
|
||||
)),
|
||||
closure_model_ref: None,
|
||||
closure_model_verdict: None,
|
||||
closure_model_finding: None,
|
||||
require_closure_model_verdict: false,
|
||||
model_authored: false,
|
||||
state_revision: 0,
|
||||
})?;
|
||||
|
|
@ -6181,7 +6294,7 @@ fn print_usage() {
|
|||
run --workspace <repo> [--item <id>] [--accept-receipt <path>] [--runtime-store <path>] [--requested-path <path>]\n\
|
||||
adopt --workspace <repo> [--item <id>] [--run-receipt <path>] [--from-plan <path>] [--plan-summary <text>] [--adoption-evidence-ref <ref>]\n\
|
||||
execute --workspace <repo> [--item <id>] [--from-plan <path>] [--command <command>] [--changed-path <path>] [--commit-message <text>]\n\
|
||||
close --workspace <repo> [--item <id>] [--execute-receipt <path>] [--verification-command <command>] [--closure-model-ref <ref>] [--model-authored]\n\
|
||||
close --workspace <repo> [--item <id>] [--execute-receipt <path>] [--verification-command <command>] [--closure-model-ref <ref>] [--model-authored] [--closure-model-verdict passed|failed|needs-work|blocked] [--closure-model-finding <text>] [--require-closure-model-verdict]\n\
|
||||
publish --workspace <repo> [--item <id>] --change-summary <text> --justification <text> --verification-receipt <ref> --review-receipt <ref> --ledger-entry-id <id> --pull-request-url <url> --pull-request-title <text>\n\
|
||||
sync --workspace <repo> [--item <id>] [--publish-receipt <path>] [--upstream-ref origin/main] --merge-receipt <ref>\n\
|
||||
overview --workspace <repo> [--item <id>] [--accept-receipt <path>] [--no-write]\n\
|
||||
|
|
|
|||
|
|
@ -659,10 +659,15 @@ sealed under `.epiphany/work/`. Closure also writes a structured
|
|||
`.epiphany/work/work-close-<item>-review.json`, recording the verification
|
||||
command/exit code, declared Hands paths, actual git commit paths, commit stat,
|
||||
optional model-review provenance from `--closure-model-ref` / `--model-authored`,
|
||||
safe-family assertions for known Imagination families, and authority seals. Soul
|
||||
passes only when the verification command succeeds, actual git changed paths
|
||||
match the Hands-declared path scope, and the committed target content satisfies
|
||||
the deterministic safe-family assertions when the plan family is known. Soul
|
||||
optional hard model closure verdicts from `--closure-model-verdict`, safe-family
|
||||
assertions for known Imagination families, and authority seals. Soul passes only
|
||||
when the verification command succeeds, actual git changed paths match the
|
||||
Hands-declared path scope, the committed target content satisfies the
|
||||
deterministic safe-family assertions when the plan family is known, and any
|
||||
supplied or required model-authored closure verdict passes.
|
||||
`--require-closure-model-verdict` turns missing model verdicts into a closure
|
||||
refusal; without that flag, older provenance-only model refs remain
|
||||
compatibility evidence instead of a hard gate. Soul
|
||||
then writes `epiphany.soul.verdict_receipt`, Modeling records the
|
||||
execution/commit/closure-review summary, and Mind writes gateway review plus
|
||||
state-commit receipts into runtime-spine. The final
|
||||
|
|
@ -713,6 +718,18 @@ Soul `failed`, `familyAssertions.status=failed`, and failed assertions
|
|||
`status-section-summary-present`, and `status-section-private-seal`, with
|
||||
`privateStateExposed=false`.
|
||||
|
||||
A model-closure gate smoke proved the new source-grounded review tooth:
|
||||
`.epiphany-smoke\closure-model-gate-20260620-135103` created a disposable repo,
|
||||
ran `epiphany-repo init`, `epiphany-swarm online`, two `repo-status-section`
|
||||
items through accept -> derive-plan -> tick run/adopt/execute, then closed one
|
||||
item with `--require-closure-model-verdict --closure-model-verdict passed` and
|
||||
one with `--closure-model-verdict needs-work`. The pass item returned
|
||||
`closed` with `modelingReview.closureReview.status=passed` and
|
||||
`gateEnforced=true`. The blocked item returned `verification-failed`, Soul
|
||||
`failed`, `modelingReview.closureReview.status=failed`, while
|
||||
`familyAssertions.status=passed` and `pathScopeMatched=true`; private state
|
||||
remained sealed.
|
||||
|
||||
`epiphany-work overview` is the first compact repo work sight/proof surface:
|
||||
|
||||
```powershell
|
||||
|
|
@ -967,15 +984,16 @@ sync proof.
|
|||
`tick` now also proves brake refusal, active-turn refusal,
|
||||
completion-anchored cooldown refusal, and stale active-turn recovery through
|
||||
typed scheduler receipts; `serve` now proves bounded cadence around that same
|
||||
tick artery; `close` now proves first deterministic Soul/Modeling/Mind closure
|
||||
over Hands commit receipts; `derive-plan` now proves a deterministic
|
||||
tick artery; `close` now proves deterministic Soul/Modeling/Mind closure over
|
||||
Hands commit receipts plus an optional/required model-authored closure verdict
|
||||
gate; `derive-plan` now proves a deterministic
|
||||
Persona/Bifrost pressure-to-plan bridge with no operator-authored shell
|
||||
details; `overview` now proves compact local proof-bundle sight over the
|
||||
receipt chain; `queue-run` now proves that Self can select tick-actionable rows
|
||||
from the typed repo-work overview queue without reading private thought or
|
||||
taking publication/service authority. The chain does not yet prove fully
|
||||
model-authored Imagination planning, deeper model-authored Soul/Modeling
|
||||
closure, optional Idunn-hosted lifecycle for queue pulses, or richer published
|
||||
model-authored Imagination planning beyond allowlisted safe-family lowering,
|
||||
optional Idunn-hosted lifecycle for queue pulses, or richer published
|
||||
proof bundles.
|
||||
|
||||
The MVP target is narrower than the full Perfect Machine and wider than a demo:
|
||||
|
|
@ -1033,14 +1051,12 @@ No: the machine is not yet Epiphany Online in the full MVP sense. The remaining
|
|||
gap is not "more permission prompts." The remaining gap is to make the
|
||||
existing typed artery run as a living repo organism:
|
||||
|
||||
- deeper model-authored closure review beyond the first typed
|
||||
`epiphany.repo_work_closure_review.v0` packet
|
||||
- further non-planning action classes where Imagination can propose useful
|
||||
branch-local work beyond planning cargo without arbitrary shell authority
|
||||
- an Idunn-owned service lifecycle path for the queue-run pulse when the
|
||||
operator explicitly grants service mutation or elevated host authority
|
||||
- a repeatable fresh-repo acceptance smoke that proves the whole path without
|
||||
supervisor target-repo edits
|
||||
- richer model-authored planning depth beyond deterministic safe-family command
|
||||
lowering
|
||||
|
||||
The MVP is functional when a human can talk to the repo Persona, the swarm can
|
||||
turn that pressure into typed work, keep advancing lawful branch-local steps
|
||||
|
|
@ -1332,17 +1348,19 @@ Required organs before MVP:
|
|||
`section-note`, and `task-card` now prove richer model-authored planning cargo
|
||||
without operator shell details. `repo-status-section` now proves the first
|
||||
repo-visible non-planning safe family by updating a marker-bounded README
|
||||
status section with the same authority seals. Remaining work is richer closure
|
||||
depth and any later model-authored action classes that move beyond
|
||||
documentation/status cargo while preserving the same authority seals.
|
||||
status section with the same authority seals. Remaining work is later
|
||||
model-authored action classes that move beyond documentation/status cargo
|
||||
while preserving the same authority seals.
|
||||
- Closure depth: `close` now writes a structured
|
||||
`epiphany.repo_work_closure_review.v0` packet for Hands commits, records
|
||||
model-review provenance when supplied, refuses actual-vs-declared path-scope
|
||||
mismatch before Soul passes, runs committed-content assertions for known safe
|
||||
families, refuses assertion failures even when the path scope matches, and
|
||||
exposes the closure-review artifact through overview/export proof rows.
|
||||
Deeper model-authored Soul/Modeling review remains a later upgrade on top of
|
||||
this typed packet.
|
||||
families, refuses assertion failures even when the path scope matches, can
|
||||
require a model-authored closure verdict before Soul passes, and exposes the
|
||||
closure-review artifact through overview/export proof rows. The verdict packet
|
||||
records reviewed inputs, finding text, enforced/skipped gate state, and
|
||||
private-state seals; focused smoke proves a model `needs-work` verdict blocks
|
||||
closure even when path scope and safe-family assertions pass.
|
||||
- Repo work projection/run surface: local `epiphany-work overview` proof bundles, typed
|
||||
`epiphany.cultmesh.repo_work_overview.v0` event history plus latest-key
|
||||
projection, and Gjallar multi-item `repo-work-overview` action/readback rows
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Reference in a new issue