From 8074e6d1ddbc73464f520c9bed707d2c46f32fa1 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 20 Jun 2026 22:45:59 +0100 Subject: [PATCH] Add repo deployment execution runbook --- ...any-repo-deployment-config-family-smoke.rs | 49 +++++ epiphany-core/src/bin/epiphany-work.rs | 178 +++++++++++++++++- notes/epiphany-repo-swarm-mvp-contract.md | 15 ++ notes/fresh-workspace-handoff.md | 1 + state/ledgers.msgpack | Bin 354555 -> 355175 bytes state/map.yaml | 6 +- 6 files changed, 245 insertions(+), 4 deletions(-) diff --git a/epiphany-core/src/bin/epiphany-repo-deployment-config-family-smoke.rs b/epiphany-core/src/bin/epiphany-repo-deployment-config-family-smoke.rs index c1844178..d553e950 100644 --- a/epiphany-core/src/bin/epiphany-repo-deployment-config-family-smoke.rs +++ b/epiphany-core/src/bin/epiphany-repo-deployment-config-family-smoke.rs @@ -211,6 +211,16 @@ fn run_smoke(args: Args) -> Result { &["deployment-config-audit", "--workspace", path_str(&repo)?], &root, )?; + let runbook = cargo_json( + &manifest, + "epiphany-work", + &[ + "deployment-execution-runbook", + "--workspace", + path_str(&repo)?, + ], + &root, + )?; require_eq( &plan, @@ -299,6 +309,40 @@ fn run_smoke(args: Args) -> Result { require_bool(&audit, &["mergeAuthorized"], false)?; require_bool(&audit, &["daemonOwnsExecution"], true)?; require_bool(&audit, &["privateStateExposed"], false)?; + require_eq( + &runbook, + &["schemaVersion"], + "epiphany.repo_deployment_execution_runbook.v0", + )?; + require_eq(&runbook, &["status"], "ready-for-operator-git-push")?; + require_bool(&runbook, &["runbookWritten"], true)?; + require_bool(&runbook, &["requiresExplicitOperatorAuthority"], true)?; + require_bool(&runbook, &["mutatesRemoteWhenRun"], true)?; + require_bool(&runbook, &["executionAuthorized"], false)?; + require_bool(&runbook, &["deploymentAuthority"], false)?; + require_bool(&runbook, &["sshAuthority"], false)?; + require_bool(&runbook, &["gitPushAuthority"], false)?; + require_bool(&runbook, &["serviceLifecycleAuthority"], false)?; + require_bool(&runbook, &["handsAuthority"], false)?; + require_bool(&runbook, &["publicationAuthorized"], false)?; + require_bool(&runbook, &["mergeAuthorized"], false)?; + require_bool(&runbook, &["daemonOwnsExecution"], true)?; + require_bool(&runbook, &["privateStateExposed"], false)?; + require_eq(&runbook, &["watchedRef"], "refs/heads/main")?; + let runbook_path = value_at_path(&runbook, &["runbookPath"]) + .and_then(Value::as_str) + .ok_or_else(|| anyhow!("missing runbook path"))?; + let runbook_text = fs::read_to_string(runbook_path) + .with_context(|| format!("failed to read runbook {runbook_path}"))?; + require_text( + &runbook_text, + "schema_version = \"epiphany.repo_deployment_execution_runbook.v0\"", + )?; + require_text(&runbook_text, "git push $Remote HEAD:refs/heads/main")?; + require_text( + &runbook_text, + "gamecult.idunn.deployment_aftercare_audit.v0", + )?; let summary = json!({ "schemaVersion": "epiphany.repo_deployment_config_family_smoke.v0", @@ -317,6 +361,11 @@ fn run_smoke(args: Args) -> Result { "deploymentConfigAuditStatus": audit["status"], "deploymentConfigAuditReceiptPath": audit["receiptPath"], "readyForIdunnReview": audit["readyForIdunnReview"], + "deploymentExecutionRunbookStatus": runbook["status"], + "deploymentExecutionRunbookPath": runbook["runbookPath"], + "deploymentExecutionRunbookSha256": runbook["runbookSha256"], + "requiresExplicitOperatorAuthority": runbook["requiresExplicitOperatorAuthority"], + "mutatesRemoteWhenRun": runbook["mutatesRemoteWhenRun"], "deploymentTrigger": "git-push-observed-by-idunn", "deploymentOwner": "Idunn", "daemonOwnsExecution": true, diff --git a/epiphany-core/src/bin/epiphany-work.rs b/epiphany-core/src/bin/epiphany-work.rs index 896ffe34..1cc13a03 100644 --- a/epiphany-core/src/bin/epiphany-work.rs +++ b/epiphany-core/src/bin/epiphany-work.rs @@ -123,6 +123,9 @@ fn main() -> Result<()> { "deployment-config-audit" | "audit-deployment-config" | "idunn-deployment-audit" => { run_deployment_config_audit(parse_deployment_config_audit_args(args)?) } + "deployment-execution-runbook" | "idunn-deployment-runbook" | "deployment-push-runbook" => { + run_deployment_execution_runbook(parse_deployment_execution_runbook_args(args)?) + } "export-proof" | "public-proof" => run_export_proof(parse_export_proof_args(args)?), "tick" | "pulse" | "schedule" => run_tick(parse_tick_args(args)?), "queue-run" | "run-queue" | "queue-tick" | "scheduler-run" => { @@ -330,6 +333,14 @@ struct DeploymentConfigAuditArgs { write_receipt: bool, } +#[derive(Clone, Debug)] +struct DeploymentExecutionRunbookArgs { + workspace: PathBuf, + artifact_dir: Option, + remote: String, + write_receipt: bool, +} + #[derive(Clone, Debug)] struct ExportProofArgs { workspace: PathBuf, @@ -1125,6 +1136,36 @@ fn parse_deployment_config_audit_args( }) } +fn parse_deployment_execution_runbook_args( + args: impl Iterator, +) -> Result { + let mut workspace = None; + let mut artifact_dir = None; + let mut remote = "origin".to_string(); + let mut write_receipt = true; + + let mut args = args.peekable(); + while let Some(arg) = args.next() { + match arg.as_str() { + "--workspace" => workspace = Some(take_path(&mut args, "--workspace")?), + "--artifact-dir" => artifact_dir = Some(take_path(&mut args, "--artifact-dir")?), + "--remote" => remote = take_string(&mut args, "--remote")?, + "--no-write" | "--read-only" => write_receipt = false, + other => { + return Err(anyhow!( + "unexpected deployment-execution-runbook argument {other:?}" + )); + } + } + } + Ok(DeploymentExecutionRunbookArgs { + workspace: workspace.context("missing --workspace")?, + artifact_dir, + remote, + write_receipt, + }) +} + fn parse_export_proof_args(args: impl Iterator) -> Result { let mut workspace = None; let mut item = None; @@ -10405,6 +10446,128 @@ fn run_deployment_config_audit(args: DeploymentConfigAuditArgs) -> Result Ok(receipt) } +fn run_deployment_execution_runbook(args: DeploymentExecutionRunbookArgs) -> Result { + let workspace = args + .workspace + .canonicalize() + .with_context(|| format!("failed to resolve {}", args.workspace.display()))?; + ensure_git_repo(&workspace)?; + let artifact_dir = args + .artifact_dir + .unwrap_or_else(|| workspace.join(".epiphany").join("work")); + fs::create_dir_all(&artifact_dir) + .with_context(|| format!("failed to create {}", artifact_dir.display()))?; + + let config_path = workspace.join(".epiphany").join("deployment.toml"); + let config_text = if config_path.exists() { + fs::read_to_string(&config_path) + .with_context(|| format!("failed to read {}", config_path.display()))? + } else { + String::new() + }; + let watched_ref = deployment_config_string_value(&config_text, "watched_ref") + .unwrap_or_else(|| "refs/heads/main".to_string()); + let deployment_script_ref = + deployment_config_string_value(&config_text, "deployment_script_ref") + .unwrap_or_else(|| "deploy/idunn-deploy.ps1".to_string()); + let current_branch = git_output(&workspace, &["rev-parse", "--abbrev-ref", "HEAD"])?; + let current_commit = git_output(&workspace, &["rev-parse", "HEAD"])?; + let generated_at = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true); + let receipt_path = artifact_dir.join("deployment-execution-runbook.json"); + let runbook_dir = artifact_dir.join("idunn-deployment"); + let runbook_path = runbook_dir.join("idunn-git-push-runbook.ps1"); + let audit = run_deployment_config_audit(DeploymentConfigAuditArgs { + workspace: workspace.clone(), + artifact_dir: Some(artifact_dir.clone()), + write_receipt: args.write_receipt, + })?; + let ready_for_idunn = audit.get("readyForIdunnReview").and_then(Value::as_bool) == Some(true); + let push_command = format!("git push {} HEAD:{watched_ref}", args.remote); + let operator_execution_command = format!( + "powershell -ExecutionPolicy Bypass -File {} -Remote {}", + powershell_single_quoted(&runbook_path.display().to_string()), + powershell_single_quoted(&args.remote) + ); + let mut runbook_written = false; + let mut runbook_sha256 = Value::Null; + + if ready_for_idunn && args.write_receipt { + fs::create_dir_all(&runbook_dir) + .with_context(|| format!("failed to create {}", runbook_dir.display()))?; + let runbook = [ + "# schema_version = \"epiphany.repo_deployment_execution_runbook.v0\"".to_string(), + "# owner = \"Idunn\"".to_string(), + "# authority = \"explicit-operator-git-push\"".to_string(), + "# generated_by = \"epiphany-work deployment-execution-runbook\"".to_string(), + "param([string]$Remote = 'origin')".to_string(), + "$ErrorActionPreference = 'Stop'".to_string(), + format!("Set-Location -LiteralPath {}", powershell_single_quoted(&workspace.display().to_string())), + "Write-Host 'Epiphany Idunn deployment handoff: operator-owned git push.'".to_string(), + "Write-Host 'This runbook mutates the remote ref; run only with explicit operator authority.'".to_string(), + "git status --short".to_string(), + format!("git push $Remote HEAD:{watched_ref}"), + "Write-Host 'Aftercare: wait for Idunn deployment receipt and aftercare audit on CultMesh.'".to_string(), + "Write-Host 'Required receipts: gamecult.idunn.deployment_receipt.v0 and gamecult.idunn.deployment_aftercare_audit.v0.'".to_string(), + ] + .join("\n"); + fs::write(&runbook_path, runbook) + .with_context(|| format!("failed to write {}", runbook_path.display()))?; + runbook_written = true; + runbook_sha256 = json!(file_sha256(&runbook_path)?); + } + + let status = if ready_for_idunn { + "ready-for-operator-git-push" + } else { + "blocked-config-not-ready" + }; + let receipt = json!({ + "schemaVersion": "epiphany.repo_deployment_execution_runbook.v0", + "runbookId": "repo-idunn-git-push-runbook", + "generatedAt": generated_at, + "status": status, + "workspace": workspace, + "configPath": config_path, + "configAudit": audit, + "receiptPath": receipt_path, + "runbookPath": if runbook_written { json!(runbook_path) } else { Value::Null }, + "runbookSha256": runbook_sha256, + "runbookWritten": runbook_written, + "operatorExecutionCommand": if runbook_written { + json!(operator_execution_command) + } else { + Value::Null + }, + "gitPushCommand": push_command, + "remote": args.remote, + "watchedRef": watched_ref, + "currentBranch": current_branch, + "currentCommit": current_commit, + "deploymentScriptRef": deployment_script_ref, + "requiredIdunnReceipts": [ + "gamecult.idunn.deployment_receipt.v0", + "gamecult.idunn.deployment_aftercare_audit.v0" + ], + "requiresExplicitOperatorAuthority": true, + "mutatesRemoteWhenRun": true, + "executionAuthorized": false, + "deploymentAuthority": false, + "sshAuthority": false, + "gitPushAuthority": false, + "serviceLifecycleAuthority": false, + "handsAuthority": false, + "publicationAuthorized": false, + "mergeAuthorized": false, + "crossBodyMutationAuthorized": false, + "daemonOwnsExecution": true, + "privateStateExposed": false + }); + if args.write_receipt { + write_json(&receipt_path, &receipt)?; + } + Ok(receipt) +} + fn run_export_proof(args: ExportProofArgs) -> Result { let workspace = args .workspace @@ -12449,6 +12612,18 @@ fn compact_text(text: &str, limit: usize) -> String { truncated } +fn deployment_config_string_value(text: &str, key: &str) -> Option { + let prefix = format!("{key} = "); + for line in text.lines() { + let trimmed = line.trim(); + if let Some(value) = trimmed.strip_prefix(&prefix) { + let value = value.strip_prefix('"')?.strip_suffix('"')?; + return Some(value.to_string()); + } + } + None +} + fn read_json(path: &Path) -> Result { let raw = fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?; @@ -12549,7 +12724,7 @@ fn sanitize(value: &str) -> String { fn print_usage() { eprintln!( - "usage: epiphany-work ...\n\ + "usage: epiphany-work ...\n\ persona-intake --workspace --item --message [--topic ] [--store ] [--runtime-id ]\n\ accept --workspace --from --item [--summary ] [--topic ] [--store ] [--runtime-id ] [--online-receipt ] [--public-discussion-ref ] [--candidate-action-ref ]\n\ derive-plan --workspace [--item ] [--accept-receipt ] [--action-family append-worklog|planning-note|checklist-note|section-note|repo-status-section|task-card|repo-manifest|repo-tool-capabilities|repo-tool-request|repo-eve-surface|repo-collaboration-policy|repo-collaboration-topic|repo-consensus-brief|repo-objective-draft|repo-adoption-request|repo-scheduling-request|repo-work-order|repo-verification-request|repo-publication-request|repo-sync-request|repo-maintainer-review-request|repo-pr-request|repo-credit-request|repo-artifact-acceptance-request|repo-metrics-request|repo-doctrine-update-request|repo-secret-policy-request|repo-deployment-config|repo-deployment-request] [--target-path ] [--model-ref ] [--model-authored] [--action-summary ] [--verification-ask ] [--stop-condition ] [--escalation-reason ] [--assumption ] [--constraint ] [--non-goal ] [--open-question ] [--decision-point ] [--evidence-need ]\n\ @@ -12562,6 +12737,7 @@ fn print_usage() { sync --workspace [--item ] [--publish-receipt ] [--upstream-ref origin/main] --merge-receipt \n\ overview --workspace [--item ] [--accept-receipt ] [--no-write]\n\ deployment-config-audit --workspace [--artifact-dir ] [--no-write]\n\ + deployment-execution-runbook --workspace [--artifact-dir ] [--remote origin] [--no-write]\n\ export-proof --workspace [--item ] [--accept-receipt ] [--output ] [--local-verse-store ] [--runtime-id repo-swarm-local]\n\ tick --workspace [--item ] [--local-verse-store ] [--runtime-store ] [--dry-run]\n\ queue-run --workspace [--local-verse-store ] [--runtime-id repo-swarm-local] [--max-items 1] [--dry-run]" diff --git a/notes/epiphany-repo-swarm-mvp-contract.md b/notes/epiphany-repo-swarm-mvp-contract.md index b644bd63..5e50222d 100644 --- a/notes/epiphany-repo-swarm-mvp-contract.md +++ b/notes/epiphany-repo-swarm-mvp-contract.md @@ -789,6 +789,21 @@ Idunn git-push contract as `ready-for-idunn-review` while keeping `.epiphany-smoke\repo-deployment-config-family-20260620-213042` proved the audit receipt beside the family close path. +The matching execution handoff is now a runbook, not a hidden deployment act. +`epiphany-work deployment-execution-runbook --workspace ` reruns the +config audit, then writes +`.epiphany/work/idunn-deployment/idunn-git-push-runbook.ps1` only when the +config is ready for Idunn review. The receipt +`epiphany.repo_deployment_execution_runbook.v0` reports +`ready-for-operator-git-push`, the exact `git push origin +HEAD:refs/heads/main` handoff, the runbook SHA-256, and the required Idunn +deployment/aftercare receipt contracts. It also marks +`requiresExplicitOperatorAuthority=true` and `mutatesRemoteWhenRun=true` while +keeping repo-agent execution, deployment, SSH, git-push, service lifecycle, +Hands, publication, merge, and cross-body authority false. Smoke +`.epiphany-smoke\repo-deployment-config-family-20260620-214109` proved the +runbook beside config close and audit. + ### Landed Work Run Gate The sixth front door exists as native Rust: diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 112f30cb..e00c159c 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -127,6 +127,7 @@ remembers doctrine; the branch remembers the blade. - 2026-06-20: Gjallar now has a changed-path lens over Mind-admitted repo-work maps. `repoWorkMapPathLensRows` / `REPO-WORK-MAP-PATH` group accepted map consequences by changed repo path, with item count, safe-family list, latest item/commit/Mind commit, Bifrost gate, and `privateStateExposed=false`. Smoke `.epiphany-smoke\repo-close-mind-adoption-guard-20260620-205740` reran the counterfeit-adoption guard and proved `gjallarRepoWorkMapPathLensCount=1` alongside the family lens. The next map work is richer semantic/source-grounded lenses beyond safe-family and path aggregation. - 2026-06-20: Gjallar now has a branch lens over Mind-admitted repo-work maps. `repoWorkMapBranchLensRows` / `REPO-WORK-MAP-BRANCH` group accepted map consequences by branch with item count, changed-path count, safe-family list, latest item/commit/Mind commit, Bifrost gate, and `privateStateExposed=false`. Smoke `.epiphany-smoke\repo-close-mind-adoption-guard-20260620-210747` reran the counterfeit-adoption guard and proved `gjallarRepoWorkMapBranchLensCount=1` alongside the family/path lenses. This is branch-workbench sight only: Gjallar still does not schedule, publish, merge, deploy, keep daemons alive, or admit Mind state. - 2026-06-20: `epiphany-work deployment-config-audit` is live as the non-mutating Idunn handoff readback for repo deployment config. It reads `.epiphany/deployment.toml`, writes `epiphany.repo_deployment_config_audit.v0`, reports `ready-for-idunn-review` only when the disabled git-push Idunn trigger, CultMesh intent/receipt/aftercare contracts, required receipts, and authority seals are present, and always keeps execution/deployment/SSH/git-push/service/Hands/publication/merge/cross-body authority false. Smoke `.epiphany-smoke\repo-deployment-config-family-20260620-213042` proved the audit beside the `repo.deployment_config` close path with `daemonOwnsExecution=true`, `readyForIdunnReview=true`, and `privateStateExposed=false`. +- 2026-06-20: `epiphany-work deployment-execution-runbook` is live as the explicit operator-owned git-push handoff for Idunn deployment. It requires the deployment config audit to pass, writes `.epiphany/work/idunn-deployment/idunn-git-push-runbook.ps1`, emits `epiphany.repo_deployment_execution_runbook.v0`, reports `ready-for-operator-git-push`, includes the exact `git push origin HEAD:refs/heads/main` route, runbook SHA-256, and required Idunn deployment/aftercare receipt contracts, and marks `requiresExplicitOperatorAuthority=true` plus `mutatesRemoteWhenRun=true` while keeping execution/deployment/SSH/git-push/service/Hands/publication/merge/cross-body authority false for the repo agent. Smoke `.epiphany-smoke\repo-deployment-config-family-20260620-214109` proved the runbook with `deploymentExecutionRunbookStatus=ready-for-operator-git-push`, SHA-256 `57560eda912b6e5c15f97acf53500534bb3bcf739098f329add3d88e5a9cd1d3`, and `privateStateExposed=false`. - 2026-06-20: Deeper model-authored closure review is now live. `epiphany-work close` accepts `--closure-model-verdict passed|failed|needs-work|blocked`, `--closure-model-finding`, and `--require-closure-model-verdict`; the close review embeds `epiphany.repo_work_model_closure_review.v0` under `modelingReview.closureReview`, records reviewed inputs and gate enforcement, and Soul refuses closure when a supplied or required model verdict does not pass. Focused smoke `.epiphany-smoke\closure-model-gate-20260620-135103` created a disposable repo, ran two `repo.status_section` items through public front doors, closed one with required model verdict `passed`, and blocked one with `needs-work` while `familyAssertions.status=passed`, `pathScopeMatched=true`, and `privateStateExposed=false`. Current remaining repo-swarm MVP organs are deeper interpreter/adoption semantics, more useful repo-owned configuration/action families, and Idunn-owned queue-run service lifecycle under explicit operator authority. - 2026-06-20: Mind adoption/interpreter depth now has a concrete interpretation packet. `epiphany-work adopt` writes `epiphany.repo_work_mind_adoption_decision.v0` before the main adoption receipt, embeds `epiphany.repo_work_mind_interpretation.v0` with input summary, safe-family recognition, requested-path classification, allowed branch-local Hands transition, and forbidden durable-state/publication/service/private transitions, then records Self presentation, Mind evidence review, safe-family requirement, branch-local scope, Bifrost publication gate, Soul closure requirement, and no Hands/durable-state/publication/merge/service/cross-body/private authority inside the Mind decision; the main adoption receipt cites that decision before granting branch-local Hands authority. Focused smoke `.epiphany-smoke\repo-planning-facets-20260620-175503` proves model-authored planning facets survive into Imagination, plan derivation, Mind interpretation, Mind decision, and adoption, while close still passes Soul/family/path checks with `privateStateExposed=false`. Current remaining repo-swarm MVP organs are deeper interpreter semantics beyond the first Mind interpretation packet, more useful repo-owned configuration/action families, and Idunn-owned queue-run service lifecycle under explicit operator authority. - 2026-06-20: `repo-manifest` is now the next non-planning safe family. `epiphany-work derive-plan --action-family repo-manifest` lowers accepted pressure into a structured `epiphany.toml` Body manifest with safe family `repo.body_manifest`, body domain, private/local/public Verse ids, Eve surface id, receipt-required capability hints, arbitrary shell sealed false, and private-state seals. Soul closure assertions now inspect the committed manifest for schema, safe family, summary, body domain, Verse ids, Eve surface, receipt requirement, no arbitrary shell, and private-state seals. Focused smoke `.epiphany-smoke\repo-manifest-family-20260620-141552` ran a disposable repo through init -> online -> accept -> repo-manifest derive-plan -> tick run/adopt/execute/close; Hands committed only `epiphany.toml`, Soul passed with `pathScopeMatched=true`, `familyAssertions.status=passed`, and `privateStateExposed=false`. Current remaining repo-swarm MVP organs are deeper interpreter/adoption semantics, additional useful repo-owned config/action families, and Idunn-owned queue-run service lifecycle under explicit operator authority. diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index a4dbf78c99e165fab8abe96553ae905d2d07a4d1..c3877bc8c2db9b37f84aa944f039ede28d841799 100644 GIT binary patch delta 447 zcmYL_ziU)M5XXIYw;&uAA_oytOh|jjy&t!C_YMq1QkXU&1Og)ac4zOc-tE5Z+tpa5 zu+YNx*fzD5U=jQW?32P?5bOe}ZG7=ZEWS)J-}%gZ=jTQ5$JgG+wWvFyvvhLwm_OzzI|vv z*itnt)Jp_bgDVm6q*^Gh9GD6%bH)zLD~$qnh1VM9$_*-Cmryb-jFhtwZIJcw{)lwL z6_SUYQ4;5&U6PIac!JzJW%M)abz#hl5SOHPdW-#s>cihY>_6Eaq;w2)LdP)*mM3X3 zM#>W^^9kdUW;BjzHY$p&;Bu1XaXyt