From af9f0bd6a3a901f5dca22d346e28c0b86bf4686c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 20 Jun 2026 00:04:40 +0100 Subject: [PATCH] Add repo work scheduler physiology --- epiphany-core/src/bin/epiphany-work.rs | 279 +++++++++++++++++++++- notes/epiphany-repo-swarm-mvp-contract.md | 51 +++- notes/fresh-workspace-handoff.md | 2 +- state/ledgers.msgpack | Bin 304174 -> 304683 bytes state/map.yaml | 4 +- 5 files changed, 313 insertions(+), 23 deletions(-) diff --git a/epiphany-core/src/bin/epiphany-work.rs b/epiphany-core/src/bin/epiphany-work.rs index 00864ae1..587d914f 100644 --- a/epiphany-core/src/bin/epiphany-work.rs +++ b/epiphany-core/src/bin/epiphany-work.rs @@ -1,6 +1,7 @@ use anyhow::Context; use anyhow::Result; use anyhow::anyhow; +use chrono::DateTime; use chrono::Utc; use epiphany_core::HANDS_ACTION_INTENT_SCHEMA_VERSION; use epiphany_core::HANDS_COMMAND_RECEIPT_TYPE; @@ -175,6 +176,8 @@ struct TickArgs { local_verse_store: Option, artifact_dir: Option, runtime_store: Option, + cooldown_seconds: u64, + active_timeout_seconds: u64, dry_run: bool, } @@ -644,6 +647,8 @@ fn parse_tick_args(args: impl Iterator) -> Result { let mut local_verse_store = None; let mut artifact_dir = None; let mut runtime_store = None; + let mut cooldown_seconds = 0_u64; + let mut active_timeout_seconds = 900_u64; let mut dry_run = false; let mut args = args.peekable(); @@ -657,6 +662,12 @@ fn parse_tick_args(args: impl Iterator) -> Result { } "--artifact-dir" => artifact_dir = Some(take_path(&mut args, "--artifact-dir")?), "--runtime-store" => runtime_store = Some(take_path(&mut args, "--runtime-store")?), + "--cooldown-seconds" => { + cooldown_seconds = take_u64(&mut args, "--cooldown-seconds")?; + } + "--active-timeout-seconds" => { + active_timeout_seconds = take_u64(&mut args, "--active-timeout-seconds")?; + } "--dry-run" | "--no-execute" => dry_run = true, other => return Err(anyhow!("unexpected tick argument {other:?}")), } @@ -669,6 +680,8 @@ fn parse_tick_args(args: impl Iterator) -> Result { local_verse_store, artifact_dir, runtime_store, + cooldown_seconds, + active_timeout_seconds, dry_run, }) } @@ -2000,7 +2013,7 @@ fn run_tick(args: TickArgs) -> Result { }, "nextSafeMove": "Release or narrow the local Verse swarm brake before repo-work scheduler mutation." }); - let receipt_path = artifact_dir.join(format!("work-tick-{item_slug}.json")); + let receipt_path = tick_receipt_path(&artifact_dir, &item_slug); write_json(&receipt_path, &receipt)?; return Ok(json!({ "schemaVersion": "epiphany.repo_work_scheduler_tick.v0", @@ -2019,6 +2032,199 @@ fn run_tick(args: TickArgs) -> Result { } } + let active_receipt_path = tick_active_receipt_path(&artifact_dir, &item_slug); + let last_completed_receipt_path = tick_last_completed_receipt_path(&artifact_dir, &item_slug); + let mut recovered_active_turn = Value::Null; + + if active_receipt_path.exists() { + let active_receipt = read_json(&active_receipt_path)?; + let active_started_at = parse_utc_rfc3339(&active_receipt, "startedAt") + .or_else(|| parse_utc_rfc3339(&active_receipt, "createdAt")); + let active_age_seconds = active_started_at.map(seconds_since); + let active_is_stale = active_age_seconds + .map(|age| age >= args.active_timeout_seconds as i64) + .unwrap_or(false); + + if args.active_timeout_seconds == 0 || !active_is_stale { + let after_receipts = repo_work_receipt_state( + &accept_receipt_path, + &plan_receipt_path, + &run_receipt_path, + &adopt_receipt_path, + &execute_receipt_path, + &publish_receipt_path, + &sync_receipt_path, + ); + let now = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true); + let receipt = json!({ + "schemaVersion": "epiphany.repo_work_scheduler_tick_receipt.v0", + "createdAt": now, + "workspace": workspace, + "item": item, + "localVerseStore": local_verse_store, + "scheduler": { + "owner": "Self", + "pulseKind": "repo-work-local", + "oneStepOnly": true, + "dryRun": args.dry_run, + "cooldownSeconds": args.cooldown_seconds, + "activeTimeoutSeconds": args.active_timeout_seconds + }, + "status": "refused-active-turn", + "action": "none", + "reason": "repo-work scheduler pulse already active for this work item", + "physiology": { + "activeReceiptPath": active_receipt_path, + "lastCompletedReceiptPath": last_completed_receipt_path, + "activeAgeSeconds": active_age_seconds, + "activeTimeoutSeconds": args.active_timeout_seconds, + "privateStateExposed": false + }, + "beforeReceipts": before_receipts, + "afterReceipts": after_receipts, + "advancedResult": Value::Null, + "authority": { + "branchLocalOnly": true, + "publicationAuthorized": false, + "mergeAuthorized": false, + "serviceLifecycleAuthorized": false, + "crossRepoMutationAuthorized": false, + "privateStateExposed": false, + "mutationBlockedBy": "epiphany.repo_work_scheduler_active_turn" + }, + "nextSafeMove": "Wait for the active scheduler pulse to finish, or let the active-turn timeout recover a stale marker." + }); + let receipt_path = tick_receipt_path(&artifact_dir, &item_slug); + write_json(&receipt_path, &receipt)?; + return Ok(json!({ + "schemaVersion": "epiphany.repo_work_scheduler_tick.v0", + "status": receipt["status"], + "action": receipt["action"], + "workspace": receipt["workspace"], + "item": receipt["item"], + "receiptPath": receipt_path, + "reason": receipt["reason"], + "authority": receipt["authority"], + "privateStateExposed": false, + "nextSafeMove": receipt["nextSafeMove"], + })); + } + + fs::remove_file(&active_receipt_path).with_context(|| { + format!( + "failed to clear stale active tick receipt {}", + active_receipt_path.display() + ) + })?; + recovered_active_turn = json!({ + "activeReceiptPath": active_receipt_path, + "activeAgeSeconds": active_age_seconds, + "activeTimeoutSeconds": args.active_timeout_seconds, + "recovered": true, + "privateStateExposed": false + }); + } + + if args.cooldown_seconds > 0 && last_completed_receipt_path.exists() { + let last_completed_receipt = read_json(&last_completed_receipt_path)?; + if let Some(last_created_at) = parse_utc_rfc3339(&last_completed_receipt, "createdAt") { + let elapsed_seconds = seconds_since(last_created_at); + if elapsed_seconds < args.cooldown_seconds as i64 { + let after_receipts = repo_work_receipt_state( + &accept_receipt_path, + &plan_receipt_path, + &run_receipt_path, + &adopt_receipt_path, + &execute_receipt_path, + &publish_receipt_path, + &sync_receipt_path, + ); + let now = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true); + let receipt = json!({ + "schemaVersion": "epiphany.repo_work_scheduler_tick_receipt.v0", + "createdAt": now, + "workspace": workspace, + "item": item, + "localVerseStore": local_verse_store, + "scheduler": { + "owner": "Self", + "pulseKind": "repo-work-local", + "oneStepOnly": true, + "dryRun": args.dry_run, + "cooldownSeconds": args.cooldown_seconds, + "activeTimeoutSeconds": args.active_timeout_seconds + }, + "status": "refused-by-cooldown", + "action": "none", + "reason": format!( + "repo-work scheduler cooldown has not elapsed: {elapsed_seconds}s < {}s", + args.cooldown_seconds + ), + "physiology": { + "activeReceiptPath": active_receipt_path, + "lastCompletedReceiptPath": last_completed_receipt_path, + "elapsedSeconds": elapsed_seconds, + "cooldownSeconds": args.cooldown_seconds, + "privateStateExposed": false + }, + "beforeReceipts": before_receipts, + "afterReceipts": after_receipts, + "advancedResult": Value::Null, + "authority": { + "branchLocalOnly": true, + "publicationAuthorized": false, + "mergeAuthorized": false, + "serviceLifecycleAuthorized": false, + "crossRepoMutationAuthorized": false, + "privateStateExposed": false, + "mutationBlockedBy": "epiphany.repo_work_scheduler_cooldown" + }, + "nextSafeMove": "Wait for scheduler cooldown to elapse before pulsing this work item again." + }); + let receipt_path = tick_receipt_path(&artifact_dir, &item_slug); + write_json(&receipt_path, &receipt)?; + return Ok(json!({ + "schemaVersion": "epiphany.repo_work_scheduler_tick.v0", + "status": receipt["status"], + "action": receipt["action"], + "workspace": receipt["workspace"], + "item": receipt["item"], + "receiptPath": receipt_path, + "reason": receipt["reason"], + "authority": receipt["authority"], + "privateStateExposed": false, + "nextSafeMove": receipt["nextSafeMove"], + })); + } + } + } + + let active_started_at = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true); + let active_marker = json!({ + "schemaVersion": "epiphany.repo_work_scheduler_tick_active.v0", + "createdAt": active_started_at, + "startedAt": active_started_at, + "workspace": workspace, + "item": item, + "scheduler": { + "owner": "Self", + "pulseKind": "repo-work-local", + "oneStepOnly": true, + "dryRun": args.dry_run, + "cooldownSeconds": args.cooldown_seconds, + "activeTimeoutSeconds": args.active_timeout_seconds + }, + "authority": { + "branchLocalOnly": true, + "publicationAuthorized": false, + "mergeAuthorized": false, + "serviceLifecycleAuthorized": false, + "crossRepoMutationAuthorized": false, + "privateStateExposed": false + } + }); + write_json(&active_receipt_path, &active_marker)?; + let mut action = "none".to_string(); let status; let reason; @@ -2033,7 +2239,8 @@ fn run_tick(args: TickArgs) -> Result { } else if publish_receipt_path.exists() { action = "none".to_string(); status = "noop".to_string(); - reason = "publication receipt exists; scheduler stops before merge/sync authority".to_string(); + reason = + "publication receipt exists; scheduler stops before merge/sync authority".to_string(); next_safe_move = "Wait for maintainer/Bifrost merge receipt, then run epiphany-work sync.".to_string(); } else if execute_receipt_path.exists() { @@ -2082,7 +2289,8 @@ fn run_tick(args: TickArgs) -> Result { status = "blocked".to_string(); reason = "run receipt exists but no matching plan receipt exists".to_string(); next_safe_move = - "Write an Imagination/Self action plan before adopting Hands authority.".to_string(); + "Write an Imagination/Self action plan before adopting Hands authority." + .to_string(); } else if args.dry_run { action = "adopt-from-plan".to_string(); status = "would-advance".to_string(); @@ -2106,7 +2314,8 @@ fn run_tick(args: TickArgs) -> Result { status = "advanced".to_string(); reason = "adopted queued Hands run packet from typed action plan".to_string(); next_safe_move = - "Run another scheduler pulse to execute the approved branch-local plan.".to_string(); + "Run another scheduler pulse to execute the approved branch-local plan." + .to_string(); } } else if plan_receipt_path.exists() { let plan_receipt = read_json(&plan_receipt_path)?; @@ -2116,8 +2325,8 @@ fn run_tick(args: TickArgs) -> Result { if requested_paths.is_empty() { status = "blocked".to_string(); reason = "plan receipt has no changedPaths for the Substrate Gate".to_string(); - next_safe_move = "Repair the plan receipt or write a new plan with changed paths." - .to_string(); + next_safe_move = + "Repair the plan receipt or write a new plan with changed paths.".to_string(); } else if args.dry_run { action = "run-from-plan".to_string(); status = "would-advance".to_string(); @@ -2137,8 +2346,8 @@ fn run_tick(args: TickArgs) -> Result { requested_paths, })?; status = "advanced".to_string(); - reason = "opened queued Substrate Gate and Hands run packet from plan paths" - .to_string(); + reason = + "opened queued Substrate Gate and Hands run packet from plan paths".to_string(); next_safe_move = "Run another scheduler pulse to adopt the plan into Hands authority.".to_string(); } @@ -2146,7 +2355,8 @@ fn run_tick(args: TickArgs) -> Result { status = "blocked".to_string(); reason = "accepted work item has no matching action plan receipt".to_string(); next_safe_move = - "Create an Imagination/Self plan receipt before scheduler can advance work.".to_string(); + "Create an Imagination/Self plan receipt before scheduler can advance work." + .to_string(); } let after_receipts = repo_work_receipt_state( @@ -2169,11 +2379,21 @@ fn run_tick(args: TickArgs) -> Result { "owner": "Self", "pulseKind": "repo-work-local", "oneStepOnly": true, - "dryRun": args.dry_run + "dryRun": args.dry_run, + "cooldownSeconds": args.cooldown_seconds, + "activeTimeoutSeconds": args.active_timeout_seconds }, "status": status, "action": action, "reason": reason, + "physiology": { + "activeReceiptPath": active_receipt_path, + "lastCompletedReceiptPath": last_completed_receipt_path, + "recoveredActiveTurn": recovered_active_turn, + "cooldownSeconds": args.cooldown_seconds, + "activeTimeoutSeconds": args.active_timeout_seconds, + "privateStateExposed": false + }, "beforeReceipts": before_receipts, "afterReceipts": after_receipts, "advancedResult": advanced_result, @@ -2187,8 +2407,17 @@ fn run_tick(args: TickArgs) -> Result { }, "nextSafeMove": next_safe_move }); - let receipt_path = artifact_dir.join(format!("work-tick-{item_slug}.json")); + let receipt_path = tick_receipt_path(&artifact_dir, &item_slug); write_json(&receipt_path, &receipt)?; + write_json(&last_completed_receipt_path, &receipt)?; + if active_receipt_path.exists() { + fs::remove_file(&active_receipt_path).with_context(|| { + format!( + "failed to clear active tick receipt {}", + active_receipt_path.display() + ) + })?; + } Ok(json!({ "schemaVersion": "epiphany.repo_work_scheduler_tick.v0", "status": receipt["status"], @@ -2588,6 +2817,28 @@ fn write_json(path: &Path, value: &Value) -> Result<()> { .with_context(|| format!("failed to write {}", path.display())) } +fn tick_receipt_path(artifact_dir: &Path, item_slug: &str) -> PathBuf { + artifact_dir.join(format!("work-tick-{item_slug}.json")) +} + +fn tick_active_receipt_path(artifact_dir: &Path, item_slug: &str) -> PathBuf { + artifact_dir.join(format!("work-tick-active-{item_slug}.json")) +} + +fn tick_last_completed_receipt_path(artifact_dir: &Path, item_slug: &str) -> PathBuf { + artifact_dir.join(format!("work-tick-last-{item_slug}.json")) +} + +fn parse_utc_rfc3339(value: &Value, field: &str) -> Option> { + DateTime::parse_from_rfc3339(value.get(field)?.as_str()?) + .ok() + .map(|timestamp| timestamp.with_timezone(&Utc)) +} + +fn seconds_since(timestamp: DateTime) -> i64 { + Utc::now().signed_duration_since(timestamp).num_seconds() +} + fn ensure_git_repo(workspace: &Path) -> Result<()> { let output = Command::new("git") .arg("-C") @@ -2612,6 +2863,12 @@ fn take_string(args: &mut impl Iterator, name: &str) -> Result, name: &str) -> Result { + take_string(args, name)? + .parse::() + .with_context(|| format!("invalid integer for {name}")) +} + fn sanitize(value: &str) -> String { let sanitized = value .chars() diff --git a/notes/epiphany-repo-swarm-mvp-contract.md b/notes/epiphany-repo-swarm-mvp-contract.md index 37f418b3..21a64777 100644 --- a/notes/epiphany-repo-swarm-mvp-contract.md +++ b/notes/epiphany-repo-swarm-mvp-contract.md @@ -456,6 +456,24 @@ swarm brake is engaged, `tick` writes the same scheduler receipt family with new run/adopt/execute receipt. A brake is a machine stop, not a suggestion written in nice ink. +The pulse now also owns its first scheduler physiology markers: + +- `.epiphany/work/work-tick-active-.json` is written before the pulse + attempts a branch-local action and cleared only after the final tick receipt + is written. If a later pulse sees a live marker, it writes + `status=refused-active-turn`, action `none`, and creates no new + run/adopt/execute receipt. +- `.epiphany/work/work-tick-last-.json` stores the last completed tick + receipt. When `--cooldown-seconds ` is supplied, a later pulse refuses as + `status=refused-by-cooldown` until that completion-anchored interval elapses. + Cooldown begins after completion, not at launch. +- `--active-timeout-seconds ` lets a stale active marker recover. A stale + marker is removed, the next safe branch-local step may proceed, and the final + tick receipt records `physiology.recoveredActiveTurn.recovered=true`. + +These markers are scheduler receipts, not hidden daemon memory. They carry the +same authority seal and `privateStateExposed=false`. + The pulse stops once branch-local execution has been recorded. It does not publish, merge, synthesize Soul/Mind receipts, install services, or impersonate Idunn. Those gates remain owned by their organs. @@ -483,6 +501,17 @@ Verse before the first pulse. `epiphany-work tick` returned the brake allowed the next pulse to advance `run-from-plan`, and the refusal receipt reported `privateStateExposed=false`. +A third smoke proved scheduler physiology on a disposable fresh repo Body. A +synthetic active marker made `epiphany-work tick --cooldown-seconds 60` return +`refused-active-turn:none`. After clearing the marker, the next pulse advanced +`advanced:run-from-plan`, wrote `work-run-.json`, cleared the active +marker, and wrote `work-tick-last-.json`. An immediate third pulse with +the same cooldown returned `refused-by-cooldown:none` and did not create an +adoption receipt. A final stale-marker proof with +`--active-timeout-seconds 1` recovered the marker, advanced +`advanced:adopt-from-plan`, and recorded `recoveredActiveTurn=true`. All +summaries reported `privateStateExposed=false`. + ## Migration Implication The next migration plan must treat autonomous branch-local work as a required @@ -499,9 +528,11 @@ This plan starts from the current state: ten native front doors exist, but the work loop is not yet a full physiology. `init`, `online`, `accept`, `plan`, `run`, `adopt`, `execute`, `tick`, `publish`, and `sync` prove the typed path from repo birth to branch-local scheduler pulse, Bifrost/GitHub publication -receipts, and upstream-main sync proof. They do not yet prove daemonized -scheduling, cooldown physiology, Soul/Modeling/Mind closure after execution, or -a repo Persona that can turn +receipts, and upstream-main sync proof. `tick` now also proves brake refusal, +active-turn refusal, completion-anchored cooldown refusal, and stale active-turn +recovery through typed scheduler receipts. The chain does not yet prove +daemonized cadence, Soul/Modeling/Mind closure after execution, or a repo +Persona that can turn conversation into autonomous action without the operator feeding plan details by hand. @@ -554,8 +585,10 @@ The chain is typed and sealed enough to be useful: and writes `upstreamMainSynced=true` only after git proves the published commit is contained by upstream main. - `epiphany-work tick` is the first Self-owned scheduler pulse: it advances one - safe branch-local step across plan-backed `run`, `adopt`, or `execute`, then - stops before Soul/Mind/Bifrost gates. + safe branch-local step across plan-backed `run`, `adopt`, or `execute`, + refuses under local Verse brake, refuses while an active turn is live, refuses + during explicit completion-anchored cooldown, recovers stale active markers, + then stops before Soul/Mind/Bifrost gates. The scar is equally important: this is still an operator-triggered pulse, not a daemonized physiology. The operator can prove each organ, but the swarm does @@ -569,10 +602,10 @@ physiology while preserving the same authority receipts. Required organs before MVP: -- Scheduler physiology: the first `epiphany-work tick` pulse exists; remaining - work is daemonized cadence, cooldown after completion, active-turn/brake - integration, stale-turn recovery, and handoff from branch-local execution to - Soul/Modeling/Mind closure. +- Scheduler physiology: the first `epiphany-work tick` pulse now has brake, + active-turn, cooldown, and stale-turn recovery receipts. Remaining work is + daemonized cadence/policy around that pulse and handoff from branch-local + execution to Soul/Modeling/Mind closure. - Persona-to-plan automation: repo Persona input becomes candidate action pressure, Mind/Interpreter extracts work-shaped intent, and Imagination writes the plan packet without the operator hand-authoring shell details. diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 20b56f1a..6fe88c84 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -102,7 +102,7 @@ remembers doctrine; the branch remembers the blade. - Edge JSON is allowed for schema description, hostile ingress before immediate typed parsing, sealed forensic artifacts, or named quarantine experiments. When both subsystems are ours, runtime data must remain typed CultCache documents and move over CultNet typed contracts. Generic `serde_json::Value` in worker launch/result/selfPatch/runtime flow is contamination until classified and replaced. - Hands now has a first executable action receipt spine. `epiphany-core::hands_gateway` owns `HandsActionIntent`, `HandsActionReview`, `HandsPatchReceipt`, `HandsCommandReceipt`, and `HandsCommitReceipt`; runtime-spine can persist/reread them; `runtime_spine_persists_mind_gateway_review_receipts` includes the Hands chain beside Mind/Eyes/Substrate/Soul/Continuity proofs and proves the post-timestamp Hands receipt-chain detector; `epiphany-hands-action-smoke` emits intent/review/patch/command/commit receipt ids from `.epiphany-smoke\runtime-spine\hands-action-smoke.msgpack`; and the repo-action launch proof profile now requires action intent, action review, and patch receipt together. `epiphany-mvp-coordinator` now turns `continueImplementation` into a persisted Substrate Gate grant plus Hands intent/review gate and includes the receipt ids in the operator step. `epiphany-hands-action` is the native consequence recorder for that gate: `record-patch`, `record-command`, `record-commit`, and turn-level `record-pass` load the stored intent/review or `finalAction.handsActionGate` from `coordinator-summary.json`, require an approved matching review, enforce allowed operations, and keep patch/commit changed paths inside the requested scope. `epiphany-mvp-coordinator-smoke` includes the deterministic Hands pass proof. The coordinator now treats a complete Hands receipt chain after an accepted Soul result as new implementation evidence, launches Soul again, and routes a passing accepted Soul result to Modeling/modeling before another Hands turn. Work-loop telemetry is now typed internal CultMesh state: Verification role launches write `epiphany.cultmesh.work_loop_telemetry.v0` beside runtime-spine, render that packet for Soul with resolved receipt bodies, commit diff preview, source proof notes, verification assertions, and stdout/stderr previews, and Modeling launches read/enrich the latest packet with Soul acceptance receipt ids before Modeling updates the map. Local Verse prompt context now exposes only a sealed `latest_work_loop_summary` digest with telemetry id, Hands receipt ids, path/source/assertion counts, and Soul receipt ids; receipt payload previews, artifact previews, and commit diff previews remain internal telemetry and are leak-guarded by tests plus `epiphany-verse-query -- smoke`. - `notes/epiphany-swarm-readiness-plan.md` is the current live-fire direction gate. The local runner proves startup/status/coordinator wiring; it does not authorize unattended swarm operation. Carry VoidBot's lessons forward: one CTB-style initiative scheduler, typed pause/brake, active-turn freeze, cooldown after completion, stale active-turn recovery receipts, parent-side speech eligibility, memory lifecycle phases, and typed operation proposals instead of prompt-owned governance. `epiphany-verse-query swarm-brake --brake-status engaged|released` can now write the local Verse brake document, and `tools/epiphany_local_run.ps1` refuses live `plan`/`run`/`mvp` modes when that brake is engaged while keeping `status` readable. `epiphany-mvp-coordinator` also checks `--local-verse-store` before app-server startup, and `epiphany-heartbeat-store` guards tick/pump/heat/queue-mention/routine when passed `--local-verse-store`. Daemon tool invocation, daemon lifecycle poke, and `epiphany-daemon-supervisor` restart/tick/serve/service-plan/service-launch/service-runbook/windows-service-install/windows-service-status/windows-service-reconcile/windows-service-execution-readiness/start/stop attempts now fail closed under a matching engaged brake; supervisor restart now has durable per-daemon policy with cooldown/backoff, explicit scheduled tick pulses, latest scheduler receipts, typed service lifecycle plan/launch/runbook/install/status/reconcile/readiness/start/stop receipts, and local operator wrapper modes that write runbook/install/status/reconcile/readiness/control-plan/execution artifacts plus operator run receipts. The installed-service read path, read-only service-policy reconciliation path, execution-readiness path, non-mutating start/stop control-plan path, and non-elevated execution refusal path have been proved against real Windows `EventLog` service-manager state or safe missing-service targets through local Verse receipts, including explicit wrapper install/start/stop execution modes. The next cut is actual elevated install/start/stop execution proof under operator authority. -- `notes/epiphany-repo-swarm-mvp-contract.md` is the repo-swarm MVP authority map and migration plan. It states the live ten-front-door chain from `epiphany-repo init` through `epiphany-work tick`, `publish`, and `sync`, documents the operator-safe proof receipts already proved with `privateStateExposed=false`, and names the remaining MVP organs: daemonized scheduler cadence/cooldown/active-turn integration, Persona-to-plan automation, Soul/Modeling/Mind closure, repo work overview, and proof bundles. Autonomous branch-local work inside the owned repo Body is in scope and desired; Bifrost gates publication/merge/escalation, Idunn owns daemon lifecycle, and `epiphany-work tick` may only advance one safe receipt-backed branch-local step per pulse before stopping at Soul/Mind/Bifrost gates. The tick pulse now reads the repo-local CultMesh swarm brake and writes `refused-by-swarm-brake` without creating run/adopt/execute receipts when the brake is engaged. +- `notes/epiphany-repo-swarm-mvp-contract.md` is the repo-swarm MVP authority map and migration plan. It states the live ten-front-door chain from `epiphany-repo init` through `epiphany-work tick`, `publish`, and `sync`, documents the operator-safe proof receipts already proved with `privateStateExposed=false`, and names the remaining MVP organs: daemonized scheduler cadence, Persona-to-plan automation, Soul/Modeling/Mind closure, repo work overview, and proof bundles. Autonomous branch-local work inside the owned repo Body is in scope and desired; Bifrost gates publication/merge/escalation, Idunn owns daemon lifecycle, and `epiphany-work tick` may only advance one safe receipt-backed branch-local step per pulse before stopping at Soul/Mind/Bifrost gates. The tick pulse now reads the repo-local CultMesh swarm brake and writes `refused-by-swarm-brake` without creating run/adopt/execute receipts when the brake is engaged. It also writes active-turn and last-completed scheduler receipts, refuses live duplicate pulses as `refused-active-turn`, refuses explicit completion-anchored cooldown as `refused-by-cooldown`, clears stale active markers after `--active-timeout-seconds`, and records `physiology.recoveredActiveTurn.recovered=true` when recovery proceeds. Disposable fresh-repo smoke proved active refusal, `advanced:run-from-plan`, cooldown refusal without adoption, stale recovery into `advanced:adopt-from-plan`, active marker cleanup, last-completed anchor creation, and `privateStateExposed=false`. - `service-execution-runbook` makes that next cut repeatable: it generates the exact elevated wrapper command sequence without mutating the service manager, continues after per-step failure to preserve more receipts, runs `service-execution-audit` in `finally`, exits nonzero if any step failed, and seals a typed runbook lifecycle receipt. Run the generated artifact only with explicit operator authority. - `service-execution-audit` is the non-mutating acceptance proof after the elevated rite: it reports `complete` only when typed lifecycle receipts prove the runbook witness plus expected elevated chain, and otherwise reports the missing/failed sealed checks. - `service-tick` is the non-elevated scheduler pulse proof: it writes the latest daemon scheduler receipt through durable policy ownership and local wrapper receipts. diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 3bdbcf468608af22182aa493c9b15b73cf14026c..dd046cfcb14a50c3aade80255a8c3c84bfd61ae5 100644 GIT binary patch delta 510 zcmZ9Iu}T9$5Qe=Zpp6zO)Xo9hT#%^EfrU*TK&-6X>`uJn?snGMy@ab$Soi=E#Lg5} zf(RDAfmjH>l6$#CgT=D2`_KP>^S^wypTF9#+rj2xbkK{Adlw&Tt*4E*{opzXnm5zN z?X(g2Q{Bz?X`}TpyT5#FZLKflce9<{$@du=3dh7LSQ(=#GGuUxOhMBaVqw9R1{DbD zk`ackU=2bm$b@yFq}*ia{I?{^6pb~QxU>mnbX9ht&uC*%i8W-!AVXlE1vaM?!7z>d z+=OvhSXAMPf22tx44HDMxWbBN@ejG+6uARJjf9q{A}(}TNkfwfPd0QOzZQYaP adopt-from-plan -> execute-from-plan` then no-ops before Soul/Mind/Bifrost gates, local CultMesh swarm-brake refusal that writes `refused-by-swarm-brake` without creating a run/adopt/execute receipt, and upstream-main ancestry proof after explicit merge receipt. Remaining MVP organs are daemonized scheduler cadence/cooldown/active-turn integration, Persona-to-plan automation, Soul/Modeling/Mind closure after Hands commits, compact repo work overview, and operator-safe proof bundles; the scheduler may advance only one safe receipt-backed branch-local step per pulse and must not publish, merge, mutate services, cross repo boundaries, expose private state, or impersonate Idunn. + - Use `notes/epiphany-repo-swarm-mvp-contract.md` as the authority correction and migration plan for repo-swarm autonomy. Autonomous branch-local work inside an Epiphany-owned repo Body is in scope and desired; the safety boundary is typed authority, branch containment, interruptibility, receipts, private-state sealing, and Bifrost-gated publication/merge/escalation, not asking a human before every local edit. Ten native repo-swarm front doors are live and documented there: `epiphany-repo init`, `epiphany-swarm online`, `epiphany-work accept`, `epiphany-work plan`, `epiphany-work run`, `epiphany-work adopt --from-plan`, `epiphany-work execute --from-plan`, `epiphany-work tick`, `epiphany-work publish`, and `epiphany-work sync`. Fresh-repo smokes proved the chain with `privateStateExposed=false`, including plan-backed branch-local commit creation, a Self-owned tick pulse that advances `run-from-plan -> adopt-from-plan -> execute-from-plan` then no-ops before Soul/Mind/Bifrost gates, local CultMesh swarm-brake refusal that writes `refused-by-swarm-brake` without creating a run/adopt/execute receipt, active-turn refusal through `work-tick-active-.json`, explicit completion-anchored cooldown refusal through `work-tick-last-.json`, stale active-turn recovery that records `physiology.recoveredActiveTurn.recovered=true`, and upstream-main ancestry proof after explicit merge receipt. Remaining MVP organs are daemonized scheduler cadence, Persona-to-plan automation, Soul/Modeling/Mind closure after Hands commits, compact repo work overview, and operator-safe proof bundles; the scheduler may advance only one safe receipt-backed branch-local step per pulse and must not publish, merge, mutate services, cross repo boundaries, expose private state, or impersonate Idunn. - Agent memory lifecycle now has a first typed v0 operation surface in `epiphany-core::agent_memory`: `AgentMemoryLifecycleOperation` can revise, retire, crystallize, prune, and merge bounded memory records after role/agent review, and `epiphany-agent-memory-store review-lifecycle|apply-lifecycle` exposes the path at the operator edge. This closes the first explicit lifecycle-operation slice from the swarm-readiness gate; it does not yet make sleep silently mutate identity or project truth. - Persona public speech now has a parent-side eligibility receipt at the Discord/Aquarium mouth edge. `epiphany.persona_speech_audit.v0` records action kind, content fingerprint, opening/topic keys, recent-window counts, repeated opening/topic counts, channel saturation, and allow/block reasons. `epiphany-persona-discord post` blocks repeated public openings before token/webhook work; draft and bubble still emit audit receipts for operator inspection. The compatibility JSON artifact remains for display, but the durable witness is now `epiphany.cultmesh.persona_speech_audit.v0` in CultMesh with latest-key projection through local Verse context. Local MVP Persona bubbles now write that durable witness to `.epiphany-run/cultmesh/local-verse.ccmp`, not a separate mouth-side store. This is posting-edge policy, not a Persona prompt wish. - Grow the VoidBot repo-Persona pattern into Epiphany's product direction: each Epiphany owns one or more repos, has one or more Personas, and exposes project-native conversation through Aquarium, Discord, voice/WebRTC, stream surfaces, CLI, and CultNet clients so humans can talk to projects instead of pre-authoring complete implementation briefs. Persona is special because Imagination acts as Projector, turning typed state into narrative context before the Persona thinks, and Mind acts as Interpreter, turning natural Persona output into reviewed state/side-effect intents after the Persona thinks. @@ -633,4 +633,4 @@ current_status: - `tools/epiphany_local_run.ps1 -Mode swarm-online-runbook` now emits an explicit Idunn preflight summary before the operator elevation boundary. Its JSON and one-line summary report `preflightStatus`, child artifact present/hash-verified/mismatch/missing counts, aggregate service execution failed/missing check counts, child preflight rows with expected/actual SHA-256s, and private-state exposure. Live proof `local-20260619-025627-410-9724e2f6` generated the non-mutating parent artifact with `preflightStatus=ready-for-elevated-operator`, two child runbooks present, two hashes verified, zero mismatches, zero missing artifacts, eleven failed sealed service checks, zero missing checks, explicit aftercare audits, and `privateStateExposed=false`. The remaining scar is still explicit elevated operator execution followed by complete Idunn audits, not missing parent/child artifact readiness. - 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. - next_action: Continue repo-swarm MVP work from the documented ten-front-door chain. Next deepen scheduler physiology from operator-triggered brake-aware `epiphany-work tick` into daemonized cadence/cooldown/active-turn-aware pulses and Soul/Modeling/Mind closure, then build Persona-to-plan automation so plan packets are model/Imagination-authored rather than operator-authored. Preserve Idunn service lifecycle surfaces and existing CultMesh/Odin/Eve tool directories; service install/start execution still requires explicit elevated operator authority. + next_action: Continue repo-swarm MVP work from the documented ten-front-door chain. Next wrap the brake/cooldown/active-turn-aware `epiphany-work tick` in daemonized cadence policy, then connect branch-local execution to Soul/Modeling/Mind closure and build Persona-to-plan automation so plan packets are model/Imagination-authored rather than operator-authored. Preserve Idunn service lifecycle surfaces and existing CultMesh/Odin/Eve tool directories; service install/start execution still requires explicit elevated operator authority.