Add Idunn managed service desired state

This commit is contained in:
Meta Vens 2026-07-12 12:57:59 +01:00
commit 1da96bd74f
8 changed files with 267 additions and 1 deletions

View file

@ -6,9 +6,11 @@ use chrono::Utc;
use epiphany_core::EPIPHANY_CULTMESH_DAEMON_RESTART_POLICY_SCHEMA_VERSION;
use epiphany_core::EPIPHANY_CULTMESH_DAEMON_SCHEDULER_RECEIPT_SCHEMA_VERSION;
use epiphany_core::EPIPHANY_CULTMESH_DAEMON_SERVICE_LIFECYCLE_RECEIPT_SCHEMA_VERSION;
use epiphany_core::EPIPHANY_CULTMESH_MANAGED_SERVICE_POLICY_SCHEMA_VERSION;
use epiphany_core::EpiphanyCultMeshDaemonRestartPolicyEntry;
use epiphany_core::EpiphanyCultMeshDaemonSchedulerReceiptEntry;
use epiphany_core::EpiphanyCultMeshDaemonServiceLifecycleReceiptEntry;
use epiphany_core::EpiphanyCultMeshManagedServicePolicyEntry;
use epiphany_core::EpiphanyCultMeshDaemonStatusEntry;
use epiphany_core::EpiphanyLocalVerseContext;
use epiphany_core::EpiphanyServiceExecutionAuditReport;
@ -18,6 +20,7 @@ use epiphany_core::epiphany_cultmesh_daemon_poke_receipt_for_intent;
use epiphany_core::epiphany_service_execution_audit_report;
use epiphany_core::load_epiphany_cultmesh_daemon_restart_policy;
use epiphany_core::load_epiphany_cultmesh_daemon_service_lifecycle_receipts;
use epiphany_core::load_epiphany_cultmesh_managed_service_policy;
use epiphany_core::query_epiphany_local_verse_context;
use epiphany_core::seed_epiphany_local_verse_context;
use epiphany_core::write_epiphany_cultmesh_daemon_poke_intent;
@ -25,6 +28,7 @@ use epiphany_core::write_epiphany_cultmesh_daemon_poke_receipt;
use epiphany_core::write_epiphany_cultmesh_daemon_restart_policy;
use epiphany_core::write_epiphany_cultmesh_daemon_scheduler_receipt;
use epiphany_core::write_epiphany_cultmesh_daemon_service_lifecycle_receipt;
use epiphany_core::write_epiphany_cultmesh_managed_service_policy;
use epiphany_core::write_epiphany_cultmesh_daemon_status;
use serde_json::Value;
use serde_json::json;
@ -47,6 +51,8 @@ fn main() -> Result<()> {
"serve" | "loop" | "daemon" => serve(args),
"service-plan" | "install-service" => service_plan(args),
"service-launch" | "launch-service" | "start-service" => service_launch(args),
"managed-service-policy" | "service-desired-state" => managed_service_policy(args),
"managed-service-read" | "service-desired-state-read" => managed_service_read(args),
"service-runbook" | "runbook-service" => service_runbook(args),
"repo-work-service-audit"
| "repo-work-service-readiness"
@ -330,6 +336,121 @@ fn service_launch(args: Args) -> Result<()> {
Ok(())
}
fn managed_service_policy(args: Args) -> Result<()> {
seed_epiphany_local_verse_context(
&args.store,
args.runtime_id.clone(),
Utc::now().to_rfc3339(),
)?;
let command = service_command_path(&args)?;
let stdout_artifact = args.stdout_artifact.clone().unwrap_or_else(|| {
PathBuf::from(format!(".epiphany-run/services/{}.stdout.log", args.service_id))
});
let stderr_artifact = args.stderr_artifact.clone().unwrap_or_else(|| {
PathBuf::from(format!(".epiphany-run/services/{}.stderr.log", args.service_id))
});
let latest = load_epiphany_cultmesh_daemon_service_lifecycle_receipts(
&args.store,
args.runtime_id.clone(),
)?
.into_iter()
.filter(|receipt| receipt.service_id == args.service_id)
.max_by(|left, right| left.started_at_utc.cmp(&right.started_at_utc));
let policy = EpiphanyCultMeshManagedServicePolicyEntry {
schema_version: EPIPHANY_CULTMESH_MANAGED_SERVICE_POLICY_SCHEMA_VERSION.to_string(),
policy_id: args
.policy_id
.clone()
.unwrap_or_else(|| format!("managed-service-policy-{}", sanitize_id(&args.service_id))),
service_id: args.service_id.clone(),
owner_daemon_id: "epiphany-daemon-supervisor".to_string(),
command: command.display().to_string(),
args: service_serve_args(&args),
cwd: args.cwd.as_ref().map(|path| path.display().to_string()),
enabled: !args.disabled,
restart_mode: args.restart_mode.clone(),
cooldown_seconds: args.cooldown_seconds,
backoff_multiplier: args.backoff_multiplier,
stdout_artifact: stdout_artifact.display().to_string(),
stderr_artifact: stderr_artifact.display().to_string(),
last_lifecycle_receipt_id: latest
.as_ref()
.map(|receipt| receipt.receipt_id.clone())
.unwrap_or_default(),
updated_at_utc: Utc::now().to_rfc3339(),
private_state_exposed: false,
notes: vec![
"Idunn owns desired child-service state; the service binary owns its domain state."
.to_string(),
"Every start or restart delegates to the existing typed service lifecycle primitive."
.to_string(),
],
};
let written = write_epiphany_cultmesh_managed_service_policy(
&args.store,
args.runtime_id.clone(),
policy,
)?;
println!("{}", serde_json::to_string_pretty(&json!({
"schemaVersion": written.schema_version,
"status": "written",
"policyId": written.policy_id,
"serviceId": written.service_id,
"ownerDaemonId": written.owner_daemon_id,
"enabled": written.enabled,
"restartMode": written.restart_mode,
"command": written.command,
"args": written.args,
"stdoutArtifact": written.stdout_artifact,
"stderrArtifact": written.stderr_artifact,
"lastLifecycleReceiptId": written.last_lifecycle_receipt_id,
"privateStateExposed": written.private_state_exposed,
}))?);
Ok(())
}
fn managed_service_read(args: Args) -> Result<()> {
let policy = load_epiphany_cultmesh_managed_service_policy(
&args.store,
args.runtime_id.clone(),
&args.service_id,
)?
.with_context(|| format!("managed service policy missing for {}", args.service_id))?;
let latest = load_epiphany_cultmesh_daemon_service_lifecycle_receipts(
&args.store,
args.runtime_id.clone(),
)?
.into_iter()
.filter(|receipt| receipt.service_id == args.service_id)
.max_by(|left, right| left.started_at_utc.cmp(&right.started_at_utc));
println!("{}", serde_json::to_string_pretty(&json!({
"schemaVersion": "epiphany.cultmesh.managed_service_readback.v0",
"status": "desired-state-ready",
"serviceId": policy.service_id,
"ownerDaemonId": policy.owner_daemon_id,
"desired": {
"enabled": policy.enabled,
"restartMode": policy.restart_mode,
"command": policy.command,
"args": policy.args,
"cwd": policy.cwd,
"stdoutArtifact": policy.stdout_artifact,
"stderrArtifact": policy.stderr_artifact,
},
"latestLifecycle": latest.as_ref().map(|receipt| json!({
"receiptId": receipt.receipt_id,
"status": receipt.status,
"action": receipt.action,
"processId": receipt.process_id,
"startedAtUtc": receipt.started_at_utc,
"completedAtUtc": receipt.completed_at_utc,
})),
"processObservation": "unknown-until-managed-service-reconcile",
"privateStateExposed": false,
}))?);
Ok(())
}
fn service_runbook(args: Args) -> Result<()> {
seed_epiphany_local_verse_context(
&args.store,
@ -3202,6 +3323,7 @@ struct Args {
service_display_name: Option<String>,
service_description: Option<String>,
service_start_type: String,
restart_mode: String,
service_command: Option<PathBuf>,
service_args: Vec<String>,
runbook_path: Option<PathBuf>,
@ -3249,6 +3371,7 @@ impl Args {
let mut service_display_name = None;
let mut service_description = None;
let mut service_start_type = "demand".to_string();
let mut restart_mode = "on-failure".to_string();
let mut service_command = None;
let mut service_args = Vec::new();
let mut runbook_path = None;
@ -3353,6 +3476,9 @@ impl Args {
.next()
.context("missing --service-start-type value")?;
}
"--restart-mode" => {
restart_mode = values.next().context("missing --restart-mode value")?;
}
"--service-command" => {
service_command = Some(PathBuf::from(
values.next().context("missing --service-command value")?,
@ -3462,6 +3588,10 @@ impl Args {
| "service-launch"
| "launch-service"
| "start-service"
| "managed-service-policy"
| "service-desired-state"
| "managed-service-read"
| "service-desired-state-read"
| "service-runbook"
| "runbook-service"
| "repo-work-service-audit"
@ -3521,6 +3651,9 @@ impl Args {
if !matches!(service_start_type.as_str(), "auto" | "demand" | "disabled") {
anyhow::bail!("--service-start-type must be auto, demand, or disabled");
}
if !matches!(restart_mode.as_str(), "always" | "on-failure" | "never") {
anyhow::bail!("--restart-mode must be always, on-failure, or never");
}
Ok(Self {
command,
@ -3544,6 +3677,7 @@ impl Args {
service_display_name,
service_description,
service_start_type,
restart_mode,
service_command,
service_args,
runbook_path,

View file

@ -160,6 +160,10 @@ pub const EPIPHANY_CULTMESH_DAEMON_SERVICE_LIFECYCLE_RECEIPT_SCHEMA_VERSION: &st
"epiphany.cultmesh.daemon_service_lifecycle_receipt.v0";
pub const EPIPHANY_CULTMESH_DAEMON_SERVICE_LIFECYCLE_RECEIPT_LATEST_KEY: &str =
"epiphany-local/daemon-service-lifecycle-receipt/latest";
pub const EPIPHANY_CULTMESH_MANAGED_SERVICE_POLICY_TYPE: &str =
"epiphany.cultmesh.managed_service_policy";
pub const EPIPHANY_CULTMESH_MANAGED_SERVICE_POLICY_SCHEMA_VERSION: &str =
"epiphany.cultmesh.managed_service_policy.v0";
pub const EPIPHANY_CULTMESH_IDUNN_DEPLOYMENT_RECEIPT_SCHEMA_VERSION: &str =
"gamecult.idunn.deployment_receipt.v0";
pub const EPIPHANY_CULTMESH_IDUNN_DEPLOYMENT_RECEIPT_LATEST_KEY: &str =
@ -1475,6 +1479,48 @@ pub struct EpiphanyCultMeshDaemonServiceLifecycleReceiptEntry {
pub schema_catalog_sha256: String,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.cultmesh.managed_service_policy",
schema = "EpiphanyCultMeshManagedServicePolicyEntry"
)]
pub struct EpiphanyCultMeshManagedServicePolicyEntry {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub policy_id: String,
#[cultcache(key = 2)]
pub service_id: String,
#[cultcache(key = 3)]
pub owner_daemon_id: String,
#[cultcache(key = 4)]
pub command: String,
#[cultcache(key = 5)]
pub args: Vec<String>,
#[cultcache(key = 6)]
pub cwd: Option<String>,
#[cultcache(key = 7)]
pub enabled: bool,
#[cultcache(key = 8)]
pub restart_mode: String,
#[cultcache(key = 9)]
pub cooldown_seconds: i64,
#[cultcache(key = 10)]
pub backoff_multiplier: u32,
#[cultcache(key = 11)]
pub stdout_artifact: String,
#[cultcache(key = 12)]
pub stderr_artifact: String,
#[cultcache(key = 13)]
pub last_lifecycle_receipt_id: String,
#[cultcache(key = 14)]
pub updated_at_utc: String,
#[cultcache(key = 15)]
pub private_state_exposed: bool,
#[cultcache(key = 16)]
pub notes: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "gamecult.idunn.deployment_receipt",
@ -2584,6 +2630,7 @@ cultmesh_documents!(EpiphanyCultMeshDocuments {
EpiphanyCultMeshDaemonRestartPolicyEntry => EPIPHANY_CULTMESH_DAEMON_RESTART_POLICY_SCHEMA_VERSION,
EpiphanyCultMeshDaemonSchedulerReceiptEntry => EPIPHANY_CULTMESH_DAEMON_SCHEDULER_RECEIPT_SCHEMA_VERSION,
EpiphanyCultMeshDaemonServiceLifecycleReceiptEntry => EPIPHANY_CULTMESH_DAEMON_SERVICE_LIFECYCLE_RECEIPT_SCHEMA_VERSION,
EpiphanyCultMeshManagedServicePolicyEntry => EPIPHANY_CULTMESH_MANAGED_SERVICE_POLICY_SCHEMA_VERSION,
EpiphanyCultMeshIdunnDeploymentReceiptEntry => EPIPHANY_CULTMESH_IDUNN_DEPLOYMENT_RECEIPT_SCHEMA_VERSION,
EpiphanyCultMeshIdunnAftercareAuditReceiptEntry => EPIPHANY_CULTMESH_IDUNN_AFTERCARE_AUDIT_RECEIPT_SCHEMA_VERSION,
EpiphanyCultMeshSwarmBrakeEntry => EPIPHANY_CULTMESH_SWARM_BRAKE_SCHEMA_VERSION,
@ -3598,6 +3645,28 @@ pub fn load_epiphany_cultmesh_daemon_service_lifecycle_receipts(
.collect())
}
pub fn write_epiphany_cultmesh_managed_service_policy(
store_path: impl AsRef<Path>,
runtime_id: impl Into<String>,
policy: EpiphanyCultMeshManagedServicePolicyEntry,
) -> Result<EpiphanyCultMeshManagedServicePolicyEntry> {
validate_managed_service_policy(&policy)?;
let mut node = open_epiphany_cultmesh_node(store_path, runtime_id)?;
let key = epiphany_cultmesh_managed_service_policy_key(&policy.service_id);
let written = node.put(key.as_str(), &policy)?;
node.flush()?;
Ok(written)
}
pub fn load_epiphany_cultmesh_managed_service_policy(
store_path: impl AsRef<Path>,
runtime_id: impl Into<String>,
service_id: &str,
) -> Result<Option<EpiphanyCultMeshManagedServicePolicyEntry>> {
let node = open_epiphany_cultmesh_node(store_path, runtime_id)?;
node.get(&epiphany_cultmesh_managed_service_policy_key(service_id))
}
pub fn write_epiphany_cultmesh_idunn_deployment_receipt(
store_path: impl AsRef<Path>,
runtime_id: impl Into<String>,
@ -3951,6 +4020,33 @@ fn validate_daemon_restart_policy(policy: &EpiphanyCultMeshDaemonRestartPolicyEn
Ok(())
}
fn validate_managed_service_policy(policy: &EpiphanyCultMeshManagedServicePolicyEntry) -> Result<()> {
if policy.private_state_exposed {
return Err(anyhow!("managed service policies must not expose private state"));
}
for (label, value) in [
("policy id", policy.policy_id.as_str()),
("service id", policy.service_id.as_str()),
("owner daemon id", policy.owner_daemon_id.as_str()),
("command", policy.command.as_str()),
("restart mode", policy.restart_mode.as_str()),
("stdout artifact", policy.stdout_artifact.as_str()),
("stderr artifact", policy.stderr_artifact.as_str()),
("updated timestamp", policy.updated_at_utc.as_str()),
] {
if value.trim().is_empty() {
return Err(anyhow!("managed service policy missing {label}"));
}
}
if !matches!(policy.restart_mode.as_str(), "always" | "on-failure" | "never") {
return Err(anyhow!("managed service policy restart_mode must be always, on-failure, or never"));
}
if policy.cooldown_seconds < 0 || policy.backoff_multiplier == 0 {
return Err(anyhow!("managed service policy requires non-negative cooldown and positive backoff"));
}
Ok(())
}
fn validate_daemon_scheduler_receipt(
receipt: &EpiphanyCultMeshDaemonSchedulerReceiptEntry,
) -> Result<()> {
@ -6219,6 +6315,10 @@ fn epiphany_cultmesh_daemon_service_lifecycle_receipt_key(receipt_id: &str) -> S
format!("epiphany-local/daemon-service-lifecycle-receipt/{receipt_id}")
}
fn epiphany_cultmesh_managed_service_policy_key(service_id: &str) -> String {
format!("epiphany-local/managed-service-policy/{service_id}")
}
fn epiphany_cultmesh_idunn_deployment_receipt_key(receipt_id: &str) -> String {
format!("gamecult-local/idunn/deployment-receipt/{receipt_id}")
}

View file

@ -220,6 +220,8 @@ pub use cultmesh_integration::EPIPHANY_CULTMESH_DAEMON_SCHEDULER_RECEIPT_TYPE;
pub use cultmesh_integration::EPIPHANY_CULTMESH_DAEMON_SERVICE_LIFECYCLE_RECEIPT_LATEST_KEY;
pub use cultmesh_integration::EPIPHANY_CULTMESH_DAEMON_SERVICE_LIFECYCLE_RECEIPT_SCHEMA_VERSION;
pub use cultmesh_integration::EPIPHANY_CULTMESH_DAEMON_SERVICE_LIFECYCLE_RECEIPT_TYPE;
pub use cultmesh_integration::EPIPHANY_CULTMESH_MANAGED_SERVICE_POLICY_SCHEMA_VERSION;
pub use cultmesh_integration::EPIPHANY_CULTMESH_MANAGED_SERVICE_POLICY_TYPE;
pub use cultmesh_integration::EPIPHANY_CULTMESH_DAEMON_STATUS_SCHEMA_VERSION;
pub use cultmesh_integration::EPIPHANY_CULTMESH_DAEMON_STATUS_TYPE;
pub use cultmesh_integration::EPIPHANY_CULTMESH_DAEMON_TOOL_CAPABILITY_SCHEMA_VERSION;
@ -326,6 +328,7 @@ pub use cultmesh_integration::EpiphanyCultMeshDaemonPokeReceiptEntry;
pub use cultmesh_integration::EpiphanyCultMeshDaemonRestartPolicyEntry;
pub use cultmesh_integration::EpiphanyCultMeshDaemonSchedulerReceiptEntry;
pub use cultmesh_integration::EpiphanyCultMeshDaemonServiceLifecycleReceiptEntry;
pub use cultmesh_integration::EpiphanyCultMeshManagedServicePolicyEntry;
pub use cultmesh_integration::EpiphanyCultMeshDaemonStatusEntry;
pub use cultmesh_integration::EpiphanyCultMeshDaemonToolCapabilityEntry;
pub use cultmesh_integration::EpiphanyCultMeshDaemonToolInvocationIntentEntry;
@ -412,6 +415,7 @@ pub use cultmesh_integration::load_epiphany_cultmesh_daemon_liveness;
pub use cultmesh_integration::load_epiphany_cultmesh_daemon_restart_policy;
pub use cultmesh_integration::load_epiphany_cultmesh_daemon_restart_policy_directory;
pub use cultmesh_integration::load_epiphany_cultmesh_daemon_service_lifecycle_receipts;
pub use cultmesh_integration::load_epiphany_cultmesh_managed_service_policy;
pub use cultmesh_integration::load_epiphany_cultmesh_daemon_tool_directory;
pub use cultmesh_integration::load_epiphany_cultmesh_eve_surface_directory;
pub use cultmesh_integration::load_epiphany_cultmesh_idunn_aftercare_audit_receipt;
@ -477,6 +481,7 @@ pub use cultmesh_integration::write_epiphany_cultmesh_daemon_poke_receipt;
pub use cultmesh_integration::write_epiphany_cultmesh_daemon_restart_policy;
pub use cultmesh_integration::write_epiphany_cultmesh_daemon_scheduler_receipt;
pub use cultmesh_integration::write_epiphany_cultmesh_daemon_service_lifecycle_receipt;
pub use cultmesh_integration::write_epiphany_cultmesh_managed_service_policy;
pub use cultmesh_integration::write_epiphany_cultmesh_daemon_status;
pub use cultmesh_integration::write_epiphany_cultmesh_daemon_statuses;
pub use cultmesh_integration::write_epiphany_cultmesh_daemon_tool_capabilities;

View file

@ -368,6 +368,14 @@ Idunn-managed child service. The durable owner must therefore be a typed
managed-service desired-state document keyed by service id. It delegates all
starts to the existing service lifecycle primitive and must not invent an
`epiphany-daemon-heartbeat` topology identity.
The typed desired-state surface is now implemented as
`epiphany.cultmesh.managed_service_policy.v0`, persisted at a service-id key.
`epiphany-daemon-supervisor managed-service-policy` writes command, args, cwd,
enabled/restart/backoff intent, sealed log refs, and the latest lifecycle
witness; `managed-service-read` projects desired state plus
`latestLifecycle`. It deliberately reports process observation as unknown
until a real reconcile probe exists, so an old launch receipt cannot claim the
service is alive.
This map must change when ownership changes. Historical scars belong in git,
evidence, or an explicitly archived note—not in the machine's proprioception.

View file

@ -2208,6 +2208,15 @@ that family requires a standing topology daemon plus daemon liveness status.
Heartbeat is an Idunn-managed child service. Add a typed managed-service
desired-state policy keyed by service id, and have reconciliation delegate to
the existing `service-launch` lifecycle owner.
The persistence/readback half is landed. New typed document
`epiphany.cultmesh.managed_service_policy.v0` is registered in CultMesh and
keyed by service id. Supervisor commands `managed-service-policy` and
`managed-service-read` round-tripped live service `idunn-epiphany-heartbeat` in
the existing heartbeat fixture, preserving the prior lifecycle receipt and
showing desired command/args/cwd/restart mode/log refs without routine payloads.
Readback calls the historical evidence `latestLifecycle`, not `actual`, and
reports `processObservation=unknown-until-managed-service-reconcile`. Next add
that reconcile probe and delegate restarts to the existing lifecycle primitive.
## Immediate Re-entry Instruction

View file

@ -108,6 +108,12 @@ their liveness status. The heartbeat loop is an Idunn-managed child service.
The missing owner is a typed managed-service desired-state policy keyed by
service id, plus compact operator readback; do not forge an eighth standing
daemon to reuse the daemon policy table.
That desired-state document is now landed as
`epiphany.cultmesh.managed_service_policy.v0`. Idunn can write it from the same
service command/args/cwd and sealed log refs used by lifecycle launch, and can
read it beside the latest lifecycle receipt. Readback explicitly reports
process observation as unknown until reconciliation probes reality; a past
`launched` receipt is not allowed to impersonate a living process.
### Owner
@ -140,6 +146,10 @@ mode, cooldown/backoff, expected heartbeat store, stdout/stderr artifact refs,
and latest lifecycle receipt. It emits desired-state readback and delegates
every actual start/restart to the existing service lifecycle launch primitive.
It does not own heartbeat scheduling state, daemon topology, or routine output.
The persistence/publication half is complete. Next add reconciliation that
probes the policy's last PID or an equivalent platform process witness, applies
enabled/restart-mode/cooldown intent, and delegates any restart to the existing
service lifecycle launch primitive.
### Forbidden writers

Binary file not shown.

View file

@ -659,4 +659,4 @@ current_status:
- Service lifecycle receipt history is now event-only: `load_epiphany_cultmesh_daemon_service_lifecycle_receipts` filters out the `epiphany-local/daemon-service-lifecycle-receipt/latest` mirror, while `load_latest_epiphany_cultmesh_daemon_service_lifecycle_receipt` remains the latest-state reader. Focused unit test `service_lifecycle_receipt_history_excludes_latest_mirror` proves two per-receipt writes return exactly two history records and latest points at the second. Fresh smoke `verse-query-lifecycle-history-filter-smoke` and live overview/triage `local-20260618-145632-497-efac6f0d` / `local-20260618-145632-321-867947af` prove compact swarm sights still expose service lifecycle attention plus the present elevated runbook artifact without daemon pokes or service mutation. Do not use a broad unscoped cargo test filter for this check on Windows; it fans out across bin harnesses and hit a broken pipe during this pass.
- `epiphany-verse-query seed-compact` is the compact seed receipt for wrappers and read-only rites. The older `seed` command still prints the full local Verse context for explicit debugging, but compact wrapper modes now avoid full `seed`/final `query` readback after the combined overview proof exposed Windows debug stack overflow from full-context serialization pressure.
last_completed_repo_swarm_cut: Huginn live-fire has crossed the first real repo Body from Persona pressure to branch-local Hands commit and Soul/Mind closure, and now has repeatable closure and redacted-public-proof mouths. On `E:\Projects\Huginn` branch `epiphany/huginn/first-awakening`, `tools/epiphany_local_run.ps1 -Mode repo-persona-intake` accepted item `huginn-inspection-readback-livefire`, Weksa/Persona intake remained sealed, and `epiphany-work derive-plan` lowered it to model-authored safe family `repo.status_section` for `README.md` only. `tools/epiphany_local_run.ps1 -Mode repo-swarm-run` advanced four bounded pulses: run-from-plan, adopt-from-plan, execute-from-plan, and close-from-execute. Huginn commit `e9b07c38fceabbdc89ea35c2156326c80c0393a2` updated only `README.md`; closure receipt `.epiphany\work\work-close-huginn-inspection-readback-livefire.json` records status `closed`, Soul verdict `passed`, Mind state commit `repo-work-close-huginn-inspection-readback-livefire-mind-commit`, safe family `repo.status_section`, path scope `README.md`, Bifrost publication gate, and `privateStateExposed=false`. Verification also ran Huginn's TypeScript compiler through concrete Node path: `C:\Program Files\nodejs\node.exe E:\Projects\Huginn\node_modules\typescript\bin\tsc -p tsconfig.json` passed. Native verifier `epiphany-repo-livefire-closure-smoke --workspace E:\Projects\Huginn --item huginn-inspection-readback-livefire --expected-path README.md --expected-family repo.status_section --expected-gate awaiting-publication --expected-blocker bifrost-publication-missing` checks closure receipt, Mind map entry, overview gate/blocker, authority seals, and `git show` changed paths, returning `status=ok`, `soulVerdict=passed`, `publicationGate=Bifrost`, and `privateStateExposed=false`. Wrapper `tools/epiphany_local_run.ps1 -Mode repo-livefire-closure -Workspace E:\Projects\Huginn -RepoWorkItem huginn-inspection-readback-livefire -SkipBuild` exposes the same read-only proof through the local operator mouth; artifact `local-20260621-190156-803-2f85ba15` printed commit, path, gate, blocker, Soul verdict, Mind commit receipt, and `privateStateExposed=False` without push, merge, publication, or target mutation. New wrapper `tools/epiphany_local_run.ps1 -Mode repo-work-public-proof -Workspace E:\Projects\Huginn -RepoWorkItem huginn-inspection-readback-livefire -SkipBuild` exports the redacted proof bundle and repo-local Verse index without publishing; artifact `local-20260621-190645-345-4555b1dc` wrote `.epiphany\public\proof-bundles\repo-work-public-proof-huginn-inspection-readback-livefire.json`, proof id `repo-work-public-proof-huginn-inspection-readback-livefire`, SHA-256 `857545b199f3e37eb06c2e757e5cfc201958e46667bb1f8403fd333a22620f63`, nine artifact rows, zero publication rows, and `privateStateExposed=False`. Gjallar readback `local-20260621-190701-140-b9519ec6` sees the `REPO-WORK-PUBLIC-PROOF` row and action row from Huginn's repo-local Verse with the same SHA-256 while Bifrost publication remains missing. Launcher compact modes are now named compact operator-context modes instead of read-only modes, and `bifrost-public-proof` summaries print item, target public Verse, room, proof SHA-256, review receipts, and credit receipts so publication receipts are auditable without claiming upstream sync.
next_action: Add the missing typed Idunn managed-service desired-state policy keyed by service id; do not forge a topology daemon or reuse standing-daemon restart policy. Persist heartbeat serve command/args/cwd, enabled/restart/backoff intent, heartbeat store and sealed log refs, reconcile by delegating to the existing service-launch lifecycle primitive, and expose compact desired/actual/last-pulse readback before proving unattended restart.
next_action: Add Idunn managed-service reconciliation over the landed service-id desired-state policy. Probe the latest lifecycle PID or equivalent platform witness, keep historical lifecycle separate from current observation, honor enabled/restart-mode/cooldown intent, delegate every restart to the existing service-launch primitive, and prove heartbeat unattended restart from persisted state without duplicate pending turns.