Add repo work closure review receipts

This commit is contained in:
Meta Vens 2026-06-20 13:59:06 +01:00
commit 6c0c93fbff
5 changed files with 183 additions and 33 deletions

View file

@ -219,6 +219,8 @@ struct CloseArgs {
verification_command: Option<String>,
verification_summary: Option<String>,
modeling_summary: Option<String>,
closure_model_ref: Option<String>,
model_authored: bool,
state_revision: u64,
}
@ -726,6 +728,8 @@ fn parse_close_args(args: impl Iterator<Item = String>) -> Result<CloseArgs> {
let mut verification_command = None;
let mut verification_summary = None;
let mut modeling_summary = None;
let mut closure_model_ref = None;
let mut model_authored = false;
let mut state_revision = 0_u64;
let mut args = args.peekable();
@ -747,6 +751,10 @@ fn parse_close_args(args: impl Iterator<Item = String>) -> Result<CloseArgs> {
"--modeling-summary" => {
modeling_summary = Some(take_string(&mut args, "--modeling-summary")?);
}
"--closure-model-ref" | "--model-ref" => {
closure_model_ref = Some(take_string(&mut args, "--closure-model-ref")?);
}
"--model-authored" => model_authored = true,
"--state-revision" => state_revision = take_u64(&mut args, "--state-revision")?,
other => return Err(anyhow!("unexpected close argument {other:?}")),
}
@ -760,6 +768,8 @@ fn parse_close_args(args: impl Iterator<Item = String>) -> Result<CloseArgs> {
verification_command,
verification_summary,
modeling_summary,
closure_model_ref,
model_authored,
state_revision,
})
}
@ -2843,11 +2853,86 @@ fn run_close(args: CloseArgs) -> Result<Value> {
fs::write(&stderr_artifact, &verification.stderr)
.with_context(|| format!("failed to write {}", stderr_artifact.display()))?;
let now = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
let verification_passed = verification.status.success();
let declared_changed_paths =
sorted_normalized_paths(string_array_from_json(&execute_receipt, &["changedPaths"]));
let actual_changed_paths = sorted_normalized_paths(
git_output(
&workspace,
&[
"diff-tree",
"--no-commit-id",
"--name-only",
"-r",
&commit_sha,
],
)?
.lines()
.map(ToString::to_string)
.collect(),
);
let path_scope_matched = declared_changed_paths == actual_changed_paths;
let commit_stat = git_output(&workspace, &["show", "--stat", "--oneline", &commit_sha])?;
let closure_model_authored = args.model_authored || args.closure_model_ref.is_some();
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!({
"schemaVersion": "epiphany.repo_work_closure_review.v0",
"createdAt": now,
"workspace": workspace,
"receiptId": closure_review_id,
"item": item,
"owner": "Soul+Modeling",
"stateGate": "Mind",
"executeReceiptPath": execute_receipt_path,
"runtimeStore": runtime_store,
"handsReceipts": {
"patchReceiptId": patch_receipt_id,
"commandReceiptId": command_receipt_id,
"commitReceiptId": commit_receipt_id,
"commitSha": commit_sha
},
"verification": {
"command": verification_command,
"exitCode": verification.status.code(),
"stdoutArtifact": normalize_path_for_receipt(&stdout_artifact),
"stderrArtifact": normalize_path_for_receipt(&stderr_artifact),
"passed": verification.status.success()
},
"sourceGrounding": {
"declaredChangedPaths": declared_changed_paths,
"actualChangedPaths": actual_changed_paths,
"pathScopeMatched": path_scope_matched,
"commitStat": compact_multiline(&commit_stat),
"commitReceiptMatchedExecuteReceipt": true
},
"modelingReview": {
"modelAuthored": closure_model_authored,
"modelRef": args.closure_model_ref,
"deterministicFallback": !closure_model_authored,
"operatorAuthoredShellDetails": false,
"summary": args.modeling_summary.clone()
},
"authoritySeal": {
"branchLocalOnly": true,
"publicationAuthorized": false,
"mergeAuthorized": false,
"serviceLifecycleAuthority": false,
"crossRepoMutation": false,
"privateStateExposed": false
},
"privateStateExposed": false,
"nextSafeMove": "Soul may pass closure only when verification succeeds and actual git changed paths match the Hands-declared path scope."
});
write_json(&closure_review_path, &closure_review)?;
let verification_passed = verification.status.success() && path_scope_matched;
let soul_verdict_id = format!("repo-work-close-{item_slug}-soul-verdict");
let soul_summary = args.verification_summary.unwrap_or_else(|| {
if verification_passed {
format!("Soul verified branch-local commit {commit_sha} for repo work item {item}.")
} else if !path_scope_matched {
format!(
"Soul verification failed for branch-local commit {commit_sha}: actual changed paths did not match declared Hands path scope."
)
} else {
format!("Soul verification failed for branch-local commit {commit_sha}.")
}
@ -2858,8 +2943,9 @@ fn run_close(args: CloseArgs) -> Result<Value> {
commit_receipt_id.clone(),
normalize_path_for_receipt(&stdout_artifact),
normalize_path_for_receipt(&stderr_artifact),
normalize_path_for_receipt(&closure_review_path),
];
evidence_ids.extend(string_array_from_json(&execute_receipt, &["changedPaths"]));
evidence_ids.extend(declared_changed_paths.clone());
let soul_verdict = SoulVerdictReceipt {
schema_version: SOUL_VERDICT_RECEIPT_SCHEMA_VERSION.to_string(),
receipt_id: soul_verdict_id.clone(),
@ -2874,6 +2960,11 @@ fn run_close(args: CloseArgs) -> Result<Value> {
evidence_ids: evidence_ids.clone(),
risks: if verification_passed {
Vec::new()
} else if !path_scope_matched {
vec![
"Closure refused because actual git changed paths differ from Hands-declared scope."
.to_string(),
]
} else {
vec!["Closure verification command failed; publication remains blocked.".to_string()]
},
@ -2897,8 +2988,11 @@ fn run_close(args: CloseArgs) -> Result<Value> {
"verdict": soul_verdict.verdict,
"summary": soul_verdict.summary,
"stdoutArtifact": normalize_path_for_receipt(&stdout_artifact),
"stderrArtifact": normalize_path_for_receipt(&stderr_artifact)
"stderrArtifact": normalize_path_for_receipt(&stderr_artifact),
"closureReviewReceiptId": closure_review_id,
"closureReviewPath": normalize_path_for_receipt(&closure_review_path)
},
"closureReview": closure_review,
"authority": {
"branchLocalOnly": true,
"publicationGateSatisfied": false,
@ -2916,6 +3010,7 @@ fn run_close(args: CloseArgs) -> Result<Value> {
"receiptPath": closure_receipt_path,
"item": failed_receipt["item"],
"soul": failed_receipt["soul"],
"closureReview": failed_receipt["closureReview"],
"authority": failed_receipt["authority"],
"privateStateExposed": false,
"nextSafeMove": failed_receipt["nextSafeMove"],
@ -2925,7 +3020,7 @@ fn run_close(args: CloseArgs) -> Result<Value> {
let modeling_summary = args.modeling_summary.unwrap_or_else(|| {
format!(
"Modeling records repo work item {item} changed [{}] at commit {commit_sha}; scheduler should stop implementation until publication review.",
string_array_from_json(&execute_receipt, &["changedPaths"]).join(", ")
declared_changed_paths.join(", ")
)
});
let gateway_id = format!("repo-work-close-{item_slug}-mind-review");
@ -2943,7 +3038,7 @@ fn run_close(args: CloseArgs) -> Result<Value> {
refused_effects: Vec::new(),
reasons: vec![
"Soul passed the branch-local Hands consequence.".to_string(),
"Modeling summary is source-grounded in execute receipt and commit proof.".to_string(),
"Modeling summary is source-grounded in execute receipt, closure review, and commit proof.".to_string(),
"Mind admits closure metadata only; Bifrost still gates publication and merge.".to_string(),
],
contract: "Mind review for repo-work closure; admits the verified Modeling summary and publication gate without granting merge or service authority.".to_string(),
@ -2981,13 +3076,17 @@ fn run_close(args: CloseArgs) -> Result<Value> {
"verdict": soul_verdict.verdict,
"summary": soul_verdict.summary,
"stdoutArtifact": normalize_path_for_receipt(&stdout_artifact),
"stderrArtifact": normalize_path_for_receipt(&stderr_artifact)
"stderrArtifact": normalize_path_for_receipt(&stderr_artifact),
"closureReviewReceiptId": closure_review_id,
"closureReviewPath": normalize_path_for_receipt(&closure_review_path)
},
"closureReview": closure_review,
"modeling": {
"summary": modeling_summary,
"changedPaths": execute_receipt["changedPaths"],
"commitSha": execute_receipt["handsReceipts"]["commitSha"],
"source": "epiphany.repo_work_execute_receipt.v0"
"source": "epiphany.repo_work_closure_review.v0",
"modelAuthored": closure_model_authored
},
"mind": {
"gatewayReviewId": mind_review.gateway_id,
@ -3018,6 +3117,7 @@ fn run_close(args: CloseArgs) -> Result<Value> {
"item": closure_receipt["item"],
"handsReceipts": closure_receipt["handsReceipts"],
"soul": closure_receipt["soul"],
"closureReview": closure_receipt["closureReview"],
"modeling": closure_receipt["modeling"],
"mind": closure_receipt["mind"],
"authority": closure_receipt["authority"],
@ -3467,6 +3567,8 @@ fn run_overview(args: OverviewArgs) -> Result<Value> {
let adopt_receipt_path = work_receipt_path(&workspace, "adopt", &item);
let execute_receipt_path = work_receipt_path(&workspace, "execute", &item);
let close_receipt_path = work_receipt_path(&workspace, "close", &item);
let close_review_receipt_path =
artifact_dir.join(format!("work-close-{item_slug}-review.json"));
let publish_receipt_path = work_receipt_path(&workspace, "publish", &item);
let sync_receipt_path = work_receipt_path(&workspace, "sync", &item);
let overview_receipt_path = artifact_dir.join(format!("work-overview-{item_slug}.json"));
@ -3538,6 +3640,7 @@ fn run_overview(args: OverviewArgs) -> Result<Value> {
("run", &run_receipt_path),
("adopt", &adopt_receipt_path),
("execute", &execute_receipt_path),
("close-review", &close_review_receipt_path),
("close", &close_receipt_path),
("publish", &publish_receipt_path),
("sync", &sync_receipt_path),
@ -3561,6 +3664,7 @@ fn run_overview(args: OverviewArgs) -> Result<Value> {
("run", &run_receipt_path),
("adopt", &adopt_receipt_path),
("execute", &execute_receipt_path),
("close-review", &close_review_receipt_path),
("close", &close_receipt_path),
("publish", &publish_receipt_path),
("sync", &sync_receipt_path),
@ -3597,6 +3701,7 @@ fn run_overview(args: OverviewArgs) -> Result<Value> {
"runReceiptPath": existing_path_value(&run_receipt_path),
"adoptReceiptPath": existing_path_value(&adopt_receipt_path),
"executeReceiptPath": existing_path_value(&execute_receipt_path),
"closeReviewReceiptPath": existing_path_value(&close_review_receipt_path),
"closeReceiptPath": existing_path_value(&close_receipt_path),
"publishReceiptPath": existing_path_value(&publish_receipt_path),
"syncReceiptPath": existing_path_value(&sync_receipt_path),
@ -4489,6 +4594,8 @@ fn run_tick(args: TickArgs) -> Result<Value> {
modeling_summary: Some(format!(
"Modeling records scheduler-closed repo work item {item}; publication remains gated by Bifrost."
)),
closure_model_ref: None,
model_authored: false,
state_revision: 0,
})?;
status = "advanced".to_string();
@ -5351,6 +5458,14 @@ fn compact_line(text: &str) -> String {
}
}
fn compact_multiline(text: &str) -> Vec<String> {
text.lines()
.map(compact_line)
.filter(|line| !line.is_empty())
.take(40)
.collect()
}
fn compact_join(values: &[String]) -> String {
if values.is_empty() {
"none".to_string()
@ -5363,6 +5478,16 @@ fn compact_join(values: &[String]) -> String {
}
}
fn sorted_normalized_paths(paths: Vec<String>) -> Vec<String> {
let mut normalized = normalize_paths(paths)
.into_iter()
.filter(|path| path != ".")
.collect::<Vec<_>>();
normalized.sort();
normalized.dedup();
normalized
}
fn powershell_single_quoted(value: &str) -> String {
format!("'{}'", value.replace('\'', "''"))
}
@ -5726,7 +5851,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>]\n\
close --workspace <repo> [--item <id>] [--execute-receipt <path>] [--verification-command <command>] [--closure-model-ref <ref>] [--model-authored]\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\

View file

@ -639,10 +639,17 @@ It reads `work-execute-<item>.json`, requires
matches the recorded git SHA, then runs a verification command in the repo Body
(`git show --stat --oneline <commit>` by default, or
`--verification-command <command>` when supplied). The command stdout/stderr are
sealed under `.epiphany/work/`, Soul writes
`epiphany.soul.verdict_receipt`, Modeling records the execution/commit summary,
and Mind writes gateway review plus state-commit receipts into runtime-spine.
The final `.epiphany/work/work-close-<item>.json` receipt is
sealed under `.epiphany/work/`. Closure also writes a structured
`epiphany.repo_work_closure_review.v0` packet at
`.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`,
and authority seals. Soul passes only when the verification command succeeds
and actual git changed paths match the Hands-declared path scope. 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
`.epiphany/work/work-close-<item>.json` receipt is
`epiphany.repo_work_closure_receipt.v0` with
`durableStateAdmitted=true`, `publicationGateSatisfied=true`, and
`privateStateExposed=false`. It still does not grant publication, merge,
@ -662,6 +669,20 @@ A fifth smoke proved the closure chain on a disposable fresh repo Body:
the next tick returned `noop:none`, and every receipt summary reported
`privateStateExposed=false`.
A richer closure-review smoke proved the typed Soul/Modeling bridge:
`.epiphany-smoke\closure-review-20260620-135009` ran init -> online -> accept
-> `derive-plan --action-family task-card --model-authored` -> run -> adopt ->
execute -> `close --closure-model-ref smoke-closure-review-v0 --model-authored`
-> overview -> export-proof. The close output carried
`epiphany.repo_work_closure_review.v0`, `pathScopeMatched=true`,
`modelAuthored=true`, Soul `passed`, and `privateStateExposed=false`; overview
included a present `close-review` proof artifact row; redacted public proof
included that row with SHA-256 and no local path fields. A tampered execute
receipt declaring `README.md` while the commit actually changed
`notes/epiphany-work/closure-review-request-task-card.toml` produced
`verification-failed`, Soul `failed`, `pathScopeMatched=false`, and
`privateStateExposed=false`.
`epiphany-work overview` is the first compact repo work sight/proof surface:
```powershell
@ -982,8 +1003,8 @@ 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:
- richer model-authored closure where Soul and Modeling inspect source and
consequences beyond the deterministic mechanical path
- deeper model-authored closure review beyond the first typed
`epiphany.repo_work_closure_review.v0` packet
- later 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
@ -1009,9 +1030,9 @@ temptation wearing clean robes.
| Local Verse online | `epiphany-swarm online` seeds repo-local CultMesh, standing-faculty SoA, topology, liveness, Eve, and tool sight. | Keep private Verse sealed while exposing operator-safe repo status. |
| Persona/Bifrost intake | `epiphany-work accept` records pressure and candidate action refs without Hands authority. `epiphany-work persona-intake` now invokes the Persona bubble speech-audit path, records public discussion and candidate-action refs, then delegates to `accept`; wrapper mode `repo-persona-intake` exposes the operator mouth. | Deepen the intake-to-Imagination interpreter so richer model-authored action items can be proposed without granting Hands, publication, or durable-state authority at the mouth edge. |
| Imagination planning | `derive-plan` now writes a typed `epiphany.repo_work_imagination_action_items_receipt.v0` before the executable plan receipt. The action-item receipt can carry model provenance, allowed safe family, requested paths, verification asks, stop conditions, escalation reasons, and private-state seals; command text remains deterministic safe-family lowering for `append-worklog`, `planning-note`, `checklist-note`, `section-note` / `repo.markdown_managed_section`, and `task-card` / `repo.task_card`. `plan` remains manual quarantine scaffolding. | Deepen model-authored closure and any later non-planning action classes without turning model text into arbitrary shell authority. |
| Self scheduling | `tick` and `serve` prove one-step branch-local advancement, brake refusal, active-turn refusal, cooldown, and stale-turn recovery; `tick` now routes executed branch-local work through the existing Soul/Modeling/Mind `close` gate; `queue-run` selects tick-actionable rows from the typed repo-work queue and delegates to `tick`; `epiphany-swarm run` is the bounded operator mouth over that queue/tick physiology; `repo-work-service-plan` and `repo-work-service-runbook` write Idunn lifecycle receipts/artifacts for the same queue-run command without launching it. | Add richer safe-family depth next; keep any future queue-run service launch/install behind Idunn and explicit operator authority. |
| Self scheduling | `tick` and `serve` prove one-step branch-local advancement, brake refusal, active-turn refusal, cooldown, and stale-turn recovery; `tick` now routes executed branch-local work through the existing Soul/Modeling/Mind `close` gate; `queue-run` selects tick-actionable rows from the typed repo-work queue and delegates to `tick`; `epiphany-swarm run` is the bounded operator mouth over that queue/tick physiology; `repo-work-service-plan` and `repo-work-service-runbook` write Idunn lifecycle receipts/artifacts for the same queue-run command without launching it. | Keep any future queue-run service launch/install behind Idunn and explicit operator authority. |
| Branch-local Hands work | `adopt` and `execute` create approved Hands gates, run planned commands, stage declared paths, commit on `epiphany/*`, and write receipts. | Keep mutation branch-contained and receipt-backed; broaden only through typed plan families, not ad hoc shell freedom. |
| Soul/Modeling/Mind closure | `close` verifies the Hands commit and writes deterministic Soul, Modeling, and Mind receipts. | Add richer model-authored closure where useful, while preserving deterministic local closure for simple mechanical work. |
| Soul/Modeling/Mind closure | `close` verifies the Hands commit, writes `epiphany.repo_work_closure_review.v0`, refuses path-scope mismatches, and writes Soul, Modeling, and Mind receipts. | Deepen model-authored closure review beyond the first structured closure-review packet while preserving deterministic local closure for simple mechanical work. |
| Repo work sight | `overview` emits compact proof rows and mirrors typed `epiphany.cultmesh.repo_work_overview.v0` event documents plus a latest key; Gjallar enumerates the history as queue rows and non-mutating action rows; Persona's Eve surface and Eve connection readbacks expose peer-readable gate/blocker/next-action rows; `queue-run` consumes the same queue for branch-local scheduler pulses. | Deepen the Persona-to-plan loop without moving action authority out of Hands/Self/Bifrost. |
| Publication | `publish` routes Bifrost/GitHub receipts from closure or explicit Soul/Mind refs. | Keep publication Bifrost-owned; do not let scheduler publish. |
| Upstream main sync | `sync` proves the published commit is contained by upstream main after explicit merge/sync authority. | Treat upstream-main sync as a required final proof for published work. |
@ -1282,9 +1303,12 @@ Required organs before MVP:
without operator shell details. Remaining work is richer closure depth and any
later model-authored action classes that move beyond planning cargo while
preserving the same authority seals.
- Closure depth: deterministic Soul/Modeling/Mind closure exists for Hands
commits; richer model-authored Soul/Modeling review remains to replace the
current local verification rite where appropriate.
- 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, 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.
- 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