diff --git a/epiphany-core/src/bin/epiphany-daemon-supervisor.rs b/epiphany-core/src/bin/epiphany-daemon-supervisor.rs index 9bbf7730..5541dc89 100644 --- a/epiphany-core/src/bin/epiphany-daemon-supervisor.rs +++ b/epiphany-core/src/bin/epiphany-daemon-supervisor.rs @@ -230,7 +230,22 @@ fn service_launch(args: Args) -> Result<()> { let service_args = service_serve_args(&args); let mut command = Command::new(&command_path); command.args(&service_args); - command.stdout(Stdio::null()).stderr(Stdio::null()); + if let Some(stdout_path) = &args.stdout_artifact { + if let Some(parent) = stdout_path.parent() { + fs::create_dir_all(parent)?; + } + command.stdout(Stdio::from(fs::File::create(stdout_path)?)); + } else { + command.stdout(Stdio::null()); + } + if let Some(stderr_path) = &args.stderr_artifact { + if let Some(parent) = stderr_path.parent() { + fs::create_dir_all(parent)?; + } + command.stderr(Stdio::from(fs::File::create(stderr_path)?)); + } else { + command.stderr(Stdio::null()); + } if let Some(cwd) = &args.cwd { command.current_dir(cwd); } @@ -267,7 +282,9 @@ fn service_launch(args: Args) -> Result<()> { exit_code, started_at, completed_at, - None, + args.stdout_artifact + .as_ref() + .map(|path| path.display().to_string()), ); let written = write_epiphany_cultmesh_daemon_service_lifecycle_receipt( &args.store, @@ -284,6 +301,8 @@ fn service_launch(args: Args) -> Result<()> { "receiptId": written.receipt_id, "processId": written.process_id, "exitCode": written.exit_code, + "stdoutArtifact": args.stdout_artifact, + "stderrArtifact": args.stderr_artifact, "privateStateExposed": written.private_state_exposed, }))? ); @@ -3170,6 +3189,8 @@ struct Args { intent_id: Option, receipt_id: Option, artifact_ref: Option, + stdout_artifact: Option, + stderr_artifact: Option, } impl Args { @@ -3210,6 +3231,8 @@ impl Args { let mut intent_id = None; let mut receipt_id = None; let mut artifact_ref = None; + let mut stdout_artifact = None; + let mut stderr_artifact = None; while let Some(arg) = values.next() { match arg.as_str() { @@ -3339,6 +3362,16 @@ impl Args { "--artifact-ref" => { artifact_ref = Some(values.next().context("missing --artifact-ref value")?); } + "--stdout-artifact" => { + stdout_artifact = Some(PathBuf::from( + values.next().context("missing --stdout-artifact value")?, + )); + } + "--stderr-artifact" => { + stderr_artifact = Some(PathBuf::from( + values.next().context("missing --stderr-artifact value")?, + )); + } other => anyhow::bail!("unknown argument {other:?}"), } } @@ -3464,6 +3497,8 @@ impl Args { intent_id, receipt_id, artifact_ref, + stdout_artifact, + stderr_artifact, }) } } diff --git a/epiphany-core/src/bin/epiphany-work.rs b/epiphany-core/src/bin/epiphany-work.rs index f92c39d3..100129f6 100644 --- a/epiphany-core/src/bin/epiphany-work.rs +++ b/epiphany-core/src/bin/epiphany-work.rs @@ -104,7 +104,6 @@ use std::fs; use std::path::Path; use std::path::PathBuf; use std::process::Command; -use std::process::Stdio; fn main() -> Result<()> { let mut args = env::args().skip(1); @@ -14208,11 +14207,36 @@ fn resolve_repo_work_model_runtime(epiphany_root: &Path) -> Result { .ok_or_else(|| anyhow!("epiphany-openai-runtime binary is not built")) } +fn resolve_daemon_supervisor(epiphany_root: &Path) -> Result { + let exe_name = if cfg!(windows) { + "epiphany-daemon-supervisor.exe" + } else { + "epiphany-daemon-supervisor" + }; + let mut candidates = Vec::new(); + if let Some(target) = env::var_os("CARGO_TARGET_DIR") { + candidates.push(PathBuf::from(target).join("debug").join(exe_name)); + } + candidates.push( + epiphany_root + .join("epiphany-core") + .join("target") + .join("debug") + .join(exe_name), + ); + candidates.push(epiphany_root.join("target").join("debug").join(exe_name)); + candidates + .into_iter() + .find(|candidate| candidate.is_file()) + .ok_or_else(|| anyhow!("epiphany-daemon-supervisor binary is not built")) +} + fn launch_repo_work_modeling_worker( epiphany_root: &Path, workspace: &Path, artifact_dir: &Path, runtime_store: &Path, + local_verse_store: &Path, request: &RepoWorkModelingRequest, item_slug: &str, ) -> Result { @@ -14272,28 +14296,73 @@ fn launch_repo_work_modeling_worker( }, )?; let runtime_bin = resolve_repo_work_model_runtime(epiphany_root)?; + let supervisor_bin = resolve_daemon_supervisor(epiphany_root)?; let stdout_path = artifact_dir.join(format!("{job_id}.stdout.log")); let stderr_path = artifact_dir.join(format!("{job_id}.stderr.log")); - let stdout = fs::File::create(&stdout_path)?; - let stderr = fs::File::create(&stderr_path)?; - let child = Command::new(&runtime_bin) - .arg("run-worker") + let service_id = format!("idunn-{job_id}"); + let output = Command::new(&supervisor_bin) + .arg("service-launch") .arg("--store") + .arg(local_verse_store) + .arg("--runtime-id") + .arg("repo-work-local") + .arg("--daemon-id") + .arg("epiphany-daemon-modeling") + .arg("--service-id") + .arg(&service_id) + .arg("--service-command") + .arg(&runtime_bin) + .arg("--service-arg") + .arg("run-worker") + .arg("--service-arg") + .arg("--store") + .arg("--service-arg") .arg(runtime_store) + .arg("--service-arg") .arg("--job-id") + .arg("--service-arg") .arg(&job_id) + .arg("--service-arg") + .arg("--cwd") + .arg("--service-arg") + .arg(workspace) + .arg("--service-arg") + .arg("--max-runtime-seconds") + .arg("--service-arg") + .arg("300") .arg("--cwd") .arg(workspace) - .arg("--max-runtime-seconds") - .arg("300") - .stdout(Stdio::from(stdout)) - .stderr(Stdio::from(stderr)) - .spawn() - .with_context(|| format!("failed to launch {}", runtime_bin.display()))?; + .arg("--stdout-artifact") + .arg(&stdout_path) + .arg("--stderr-artifact") + .arg(&stderr_path) + .arg("--reason") + .arg(format!( + "Idunn launches typed repo-work Modeling job {job_id}." + )) + .output() + .with_context(|| { + format!( + "failed to invoke Idunn supervisor {}", + supervisor_bin.display() + ) + })?; + if !output.status.success() { + return Err(anyhow!( + "Idunn supervisor refused repo-work Modeling launch: {}", + String::from_utf8_lossy(&output.stderr) + )); + } + let lifecycle: Value = serde_json::from_slice(&output.stdout) + .context("Idunn supervisor did not emit its lifecycle receipt JSON")?; Ok(json!({ "jobId": job_id, - "processId": child.id(), + "processId": lifecycle["processId"], "runtimeBin": runtime_bin, + "supervisorBin": supervisor_bin, + "lifecycleOwner": "Idunn", + "lifecycleReceiptId": lifecycle["receiptId"], + "serviceId": lifecycle["serviceId"], "stdoutArtifact": stdout_path, "stderrArtifact": stderr_path, "requestId": request.request_id, @@ -14749,11 +14818,15 @@ fn run_tick(args: TickArgs) -> Result { } None => { action = "launch-modeling".to_string(); + let lifecycle_store = local_verse_store + .clone() + .unwrap_or_else(|| workspace.join(".epiphany").join("local-verse.ccmp")); advanced_result = launch_repo_work_modeling_worker( &epiphany_root, &workspace, &artifact_dir, &runtime_store, + &lifecycle_store, &request, &item_slug, )?; @@ -16604,6 +16677,9 @@ mod authority_tests { .next() .expect("production source before authority tests"); assert!(!production.contains("put_repo_work_modeling_finding(")); + assert!(!production.contains(".spawn()")); + assert!(production.contains("epiphany-daemon-supervisor")); + assert!(production.contains("\"lifecycleOwner\": \"Idunn\"")); assert!(!source.contains(&["RepoWork", "MapStore"].concat())); assert!(!source.contains(&["repo-work-map", ".msgpack"].concat())); } diff --git a/notes/epiphany-current-algorithmic-map.md b/notes/epiphany-current-algorithmic-map.md index 583fb7ca..677ac9d6 100644 --- a/notes/epiphany-current-algorithmic-map.md +++ b/notes/epiphany-current-algorithmic-map.md @@ -143,6 +143,15 @@ runtime document. The obsolete direct-CLI closure smoke was deleted. A live negative probe returned exit 1 for forged passing CLI cargo while leaving the typed request awaiting its real Modeling worker. +Detached worker physiology is now Idunn-owned. `epiphany-work` opens the typed +runtime job and invokes `epiphany-daemon-supervisor service-launch`; it contains +no child-process `spawn`. The supervisor launches the existing model runtime, +owns Windows hidden-process behavior, redirects stdout/stderr to explicit +artifacts, and publishes an +`epiphany.cultmesh.daemon_service_lifecycle_receipt.v0` containing service and +process identity. Self receives that receipt ID as routing evidence only. Live +item `idunn-owned-modeling` crossed this boundary and reached the Bifrost gate. + The accepted interpretation is persisted as `epiphany.modeling.repo_work_finding.v0`; it references the passing Soul verdict, diff --git a/notes/epiphany-fork-implementation-plan.md b/notes/epiphany-fork-implementation-plan.md index 0a1ab461..34ed0ea4 100644 --- a/notes/epiphany-fork-implementation-plan.md +++ b/notes/epiphany-fork-implementation-plan.md @@ -204,9 +204,9 @@ Delete or demote: ## Immediate Next Action -Move repo-work Modeling process stewardship under Idunn and add a reviewed -request generation for non-passing immutable findings. The scheduler now routes -passing requests through the existing model runtime and reaches Bifrost without -Self-authored Modeling evidence; preserve that authority path. Do not overwrite -a blocked finding, launch duplicate jobs, or grow a scheduler-owned process -manager. +Design and implement a typed Mind-reviewed request-generation pointer for +non-passing immutable repo-work Modeling findings. Idunn now owns detached +worker launch through its existing lifecycle receipt mouth. Preserve the old +request/finding as immutable evidence, make one typed owner select the current +generation, and keep filesystem closure artifacts and Self from becoming +mutable routing authorities. diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 99e477c4..ca39e2dd 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -2085,6 +2085,18 @@ Live negative item `cli-modeling-authority-refusal` returned exit 1 when `close --model-authored` supplied a forged passing finding; the error names the required Modeling runtime route. The old CLI path is structurally unable to publish the canonical finding. +Idunn now owns the detached worker launch itself. The repo-work scheduler no +longer calls `Command::spawn`; it invokes the existing daemon supervisor's +generic `service-launch` mouth. That mouth now accepts explicit stdout/stderr +artifact paths, owns hidden child creation, and writes its standard typed +CultMesh lifecycle receipt. Authenticated live item `idunn-owned-modeling` +returned lifecycle receipt +`daemon-service-lifecycle-receipt-idunn-repo-work-modeling-idunn-owned-modeling-launch`, +process id `45012`, a passing runtime-authored finding, closed Mind/map +admission, and overview gate `awaiting-publication` with +`bifrost-publication-missing`. Next design the typed Mind-reviewed generation +pointer for a non-passing immutable finding; do not mutate the old request or +let a filesystem close artifact become the new routing authority. ## Immediate Re-entry Instruction diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index fc1db03a..0d057a21 100644 Binary files a/state/ledgers.msgpack and b/state/ledgers.msgpack differ diff --git a/state/map.yaml b/state/map.yaml index e189c06d..de11eaa1 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -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: Move detached repo-work Modeling worker stewardship under Idunn and add a reviewed request generation for non-passing immutable findings. Preserve the now-live typed scheduler path from Soul request through the existing model runtime, canonical Modeling finding, immutable Mind/map admission, and Bifrost gate; Self must remain unable to author or overwrite Modeling evidence. + next_action: Design and implement a typed Mind-reviewed request-generation pointer for non-passing immutable repo-work Modeling findings. Idunn now owns detached worker launch through the existing daemon service lifecycle receipt mouth. Preserve each prior request/finding as immutable evidence; filesystem closure artifacts and Self must not become mutable routing authorities.