diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index 2c88ee1f..3d6bf562 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -177,6 +177,8 @@ mod tests { use prometeu_hal::cartridge::{AppMode, AssetsPayloadSource}; use prometeu_hal::syscalls::caps; use prometeu_system::CrashReport; + use prometeu_system::process::ProcessState; + use prometeu_system::task::TaskState; fn halting_program() -> Vec { let code = assemble("HALT").expect("assemble"); @@ -289,7 +291,10 @@ mod tests { firmware.load_cartridge(trapping_game_cartridge()); firmware.tick(&signals, &mut hardware); - assert!(matches!(firmware.state, FirmwareState::GameRunning(_))); + let task_id = match &firmware.state { + FirmwareState::GameRunning(step) => step.task_id, + other => panic!("expected GameRunning state, got {:?}", other), + }; firmware.tick(&signals, &mut hardware); @@ -302,6 +307,13 @@ mod tests { }, other => panic!("expected AppCrashes state, got {:?}", other), } + + let task = firmware.os.task_manager.get(task_id).expect("task should exist"); + let process = + firmware.os.process_manager.get(task.process_id).expect("process should exist"); + + assert_eq!(task.state, TaskState::Crashed); + assert_eq!(process.state, ProcessState::Crashed); } #[test] diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs index 63fdfda1..43d7ae63 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs @@ -19,19 +19,20 @@ impl GameRunningStep { } pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option { - let Some(task) = ctx.os.task_manager.get(self.task_id) else { + let Some(task_state) = ctx.os.task_manager.get(self.task_id).map(|task| task.state) else { // TODO: is it panic? let report = CrashReport::VmPanic { message: "task is gone, panic!".to_string(), pc: None }; return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); }; - if task.state != TaskState::Foreground { + if task_state != TaskState::Foreground { // TODO: is it panic? let report = CrashReport::VmPanic { message: "should be running as foreground, panic!".to_string(), pc: None, }; + let _ = ctx.os.crash_task(self.task_id, Some(&report)); return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); } @@ -41,7 +42,12 @@ impl GameRunningStep { ctx.hw.gfx_mut().present(); } - result.map(|report| FirmwareState::AppCrashes(AppCrashesStep { report })) + if let Some(report) = result { + let _ = ctx.os.crash_task(self.task_id, Some(&report)); + return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); + } + + None } pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {} diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_system_running.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_system_running.rs index d7dd7ab8..bcbccdf8 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_system_running.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_system_running.rs @@ -20,6 +20,7 @@ impl SystemRunningStep { pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option { let outcome = ctx.hub.update_system_profile(ctx.os, ctx.vm, ctx.signals, ctx.hw); if let Some(report) = outcome.crash { + let _ = ctx.os.crash_task(self.task_id, Some(&report)); return Some(FirmwareState::AppCrashes(AppCrashesStep { report })); } diff --git a/crates/console/prometeu-system/src/lib.rs b/crates/console/prometeu-system/src/lib.rs index 78a9ef7f..1773a865 100644 --- a/crates/console/prometeu-system/src/lib.rs +++ b/crates/console/prometeu-system/src/lib.rs @@ -4,7 +4,7 @@ mod programs; mod services; pub use crash_report::CrashReport; -pub use os::SystemOS; +pub use os::{LifecycleError, LifecycleOperation, SystemOS}; pub use programs::PrometeuHub; pub use services::fs; pub use services::process; diff --git a/crates/console/prometeu-system/src/os/lifecycle.rs b/crates/console/prometeu-system/src/os/lifecycle.rs new file mode 100644 index 00000000..bd771d47 --- /dev/null +++ b/crates/console/prometeu-system/src/os/lifecycle.rs @@ -0,0 +1,18 @@ +use crate::process::ProcessId; +use crate::task::{TaskId, TaskState}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LifecycleOperation { + SetForeground, + Suspend, + Resume, + Close, + Crash, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LifecycleError { + TaskNotFound(TaskId), + ProcessNotFound(ProcessId), + InvalidTransition { task_id: TaskId, from: TaskState, operation: LifecycleOperation }, +} diff --git a/crates/console/prometeu-system/src/os/mod.rs b/crates/console/prometeu-system/src/os/mod.rs index 61995fd9..5a2e6d76 100644 --- a/crates/console/prometeu-system/src/os/mod.rs +++ b/crates/console/prometeu-system/src/os/mod.rs @@ -1,3 +1,5 @@ +mod lifecycle; mod system_os; +pub use lifecycle::{LifecycleError, LifecycleOperation}; pub use system_os::SystemOS; diff --git a/crates/console/prometeu-system/src/os/system_os.rs b/crates/console/prometeu-system/src/os/system_os.rs index 8f9fecee..ae536400 100644 --- a/crates/console/prometeu-system/src/os/system_os.rs +++ b/crates/console/prometeu-system/src/os/system_os.rs @@ -1,11 +1,12 @@ use crate::CrashReport; use crate::VirtualMachineRuntime; use crate::fs::{FsBackend, FsState, VirtualFS}; +use crate::os::{LifecycleError, LifecycleOperation}; use crate::process::ProcessManager; use crate::services::memcard::MemcardService; use crate::services::window_manager::WindowManager; -use crate::task::TaskId; use crate::task::TaskManager; +use crate::task::{TaskId, TaskState}; use prometeu_hal::cartridge::Cartridge; use prometeu_hal::log::{LogLevel, LogService, LogSource}; use prometeu_hal::telemetry::CertificationConfig; @@ -102,7 +103,8 @@ impl SystemOS { let title = title.into(); let process_id = self.process_manager.spawn_vm_game(app_id, title.clone()); let task_id = self.task_manager.create_game_task(process_id, app_id, title); - self.task_manager.set_foreground(task_id); + self.set_foreground_task(task_id) + .expect("newly created game task should have an associated process"); task_id } @@ -111,7 +113,8 @@ impl SystemOS { let title = title.into(); let process_id = self.process_manager.spawn_vm_shell(app_id, title.clone()); let task_id = self.task_manager.create_shell_task(process_id, app_id, title); - self.task_manager.set_foreground(task_id); + self.set_foreground_task(task_id) + .expect("newly created shell task should have an associated process"); task_id } @@ -120,8 +123,202 @@ impl SystemOS { let title = title.into(); let process_id = self.process_manager.spawn_native_shell(app_id, title.clone()); let task_id = self.task_manager.create_shell_task(process_id, app_id, title); - self.task_manager.set_foreground(task_id); + self.set_foreground_task(task_id) + .expect("newly created native shell task should have an associated process"); task_id } + + pub fn set_foreground_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> { + let process_id = self.process_id_for_task(task_id)?; + + self.task_manager.set_foreground(task_id); + self.process_manager.mark_running(process_id); + + Ok(()) + } + + pub fn suspend_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> { + let process_id = self.process_id_for_task(task_id)?; + + self.task_manager.mark_suspended(task_id); + self.process_manager.mark_suspended(process_id); + + Ok(()) + } + + pub fn resume_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> { + let task = self.task_manager.get(task_id).ok_or(LifecycleError::TaskNotFound(task_id))?; + + if task.state != TaskState::Suspended { + return Err(LifecycleError::InvalidTransition { + task_id, + from: task.state, + operation: LifecycleOperation::Resume, + }); + } + + let process_id = self.process_id_for_task(task_id)?; + + self.task_manager.set_foreground(task_id); + self.process_manager.mark_running(process_id); + + Ok(()) + } + + pub fn close_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> { + let process_id = self.process_id_for_task(task_id)?; + + self.task_manager.close_task(task_id); + self.process_manager.mark_stopped(process_id); + + Ok(()) + } + + pub fn crash_task( + &mut self, + task_id: TaskId, + _report: Option<&CrashReport>, + ) -> Result<(), LifecycleError> { + let process_id = self.process_id_for_task(task_id)?; + + self.task_manager.mark_crashed(task_id); + self.process_manager.mark_crashed(process_id); + + Ok(()) + } + + fn process_id_for_task( + &self, + task_id: TaskId, + ) -> Result { + let task = self.task_manager.get(task_id).ok_or(LifecycleError::TaskNotFound(task_id))?; + + if !self.process_manager.contains(task.process_id) { + return Err(LifecycleError::ProcessNotFound(task.process_id)); + } + + Ok(task.process_id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::process::ProcessState; + + fn process_state_for_task(os: &SystemOS, task_id: TaskId) -> ProcessState { + let task = os.task_manager.get(task_id).expect("task should exist"); + os.process_manager.get(task.process_id).expect("process should exist").state + } + + #[test] + fn set_foreground_task_marks_task_and_process_running() { + let mut os = SystemOS::new(None); + let task_id = os.create_vm_game_task(42, "Sector Crawl"); + + os.suspend_task(task_id).expect("suspend should succeed"); + os.set_foreground_task(task_id).expect("foreground should succeed"); + + assert_eq!( + os.task_manager.get(task_id).expect("task should exist").state, + TaskState::Foreground + ); + assert_eq!(process_state_for_task(&os, task_id), ProcessState::Running); + } + + #[test] + fn suspend_task_marks_task_and_process_suspended() { + let mut os = SystemOS::new(None); + let task_id = os.create_vm_game_task(42, "Sector Crawl"); + + os.suspend_task(task_id).expect("suspend should succeed"); + + assert_eq!( + os.task_manager.get(task_id).expect("task should exist").state, + TaskState::Suspended + ); + assert_eq!(process_state_for_task(&os, task_id), ProcessState::Suspended); + } + + #[test] + fn resume_task_marks_suspended_task_foreground_and_process_running() { + let mut os = SystemOS::new(None); + let task_id = os.create_vm_game_task(42, "Sector Crawl"); + + os.suspend_task(task_id).expect("suspend should succeed"); + os.resume_task(task_id).expect("resume should succeed"); + + assert_eq!( + os.task_manager.get(task_id).expect("task should exist").state, + TaskState::Foreground + ); + assert_eq!(process_state_for_task(&os, task_id), ProcessState::Running); + } + + #[test] + fn close_task_marks_task_closed_and_process_stopped_without_removing_entities() { + let mut os = SystemOS::new(None); + let task_id = os.create_vm_game_task(42, "Sector Crawl"); + let process_id = os.task_manager.get(task_id).expect("task should exist").process_id; + + os.close_task(task_id).expect("close should succeed"); + + assert_eq!( + os.task_manager.get(task_id).expect("task should remain").state, + TaskState::Closed + ); + assert_eq!( + os.process_manager.get(process_id).expect("process should remain").state, + ProcessState::Stopped + ); + } + + #[test] + fn crash_task_marks_task_and_process_crashed() { + let mut os = SystemOS::new(None); + let task_id = os.create_vm_game_task(42, "Sector Crawl"); + + os.crash_task(task_id, None).expect("crash should succeed"); + + assert_eq!( + os.task_manager.get(task_id).expect("task should exist").state, + TaskState::Crashed + ); + assert_eq!(process_state_for_task(&os, task_id), ProcessState::Crashed); + } + + #[test] + fn lifecycle_operation_missing_task_returns_typed_error() { + let mut os = SystemOS::new(None); + + assert_eq!(os.suspend_task(TaskId(999)), Err(LifecycleError::TaskNotFound(TaskId(999)))); + } + + #[test] + fn lifecycle_operation_missing_process_returns_typed_error() { + let mut os = SystemOS::new(None); + let task_id = os.create_vm_game_task(42, "Sector Crawl"); + let process_id = os.task_manager.get(task_id).expect("task should exist").process_id; + + os.process_manager.mark_stopped(process_id); + os.process_manager.remove_stopped(); + + assert_eq!(os.suspend_task(task_id), Err(LifecycleError::ProcessNotFound(process_id))); + } + + #[test] + fn resume_task_from_non_suspended_state_returns_invalid_transition() { + let mut os = SystemOS::new(None); + let task_id = os.create_vm_game_task(42, "Sector Crawl"); + + assert_eq!( + os.resume_task(task_id), + Err(LifecycleError::InvalidTransition { + task_id, + from: TaskState::Foreground, + operation: LifecycleOperation::Resume, + }) + ); + } } diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 112b04fe..f2e2d13d 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,4 +1,4 @@ -{"type":"meta","next_id":{"DSC":34,"AGD":34,"DEC":25,"PLN":55,"LSN":41,"CLSN":1}} +{"type":"meta","next_id":{"DSC":34,"AGD":34,"DEC":26,"PLN":58,"LSN":41,"CLSN":1}} {"type":"discussion","id":"DSC-0023","status":"done","ticket":"perf-full-migration-to-atomic-telemetry","title":"Agenda - [PERF] Full Migration to Atomic Telemetry","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["perf","runtime","telemetry"],"agendas":[{"id":"AGD-0021","file":"AGD-0021-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0008","file":"DEC-0008-full-migration-to-atomic-telemetry.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0007","file":"PLN-0007-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0028","file":"discussion/lessons/DSC-0023-perf-full-migration-to-atomic-telemetry/LSN-0028-converging-to-single-atomic-telemetry-source.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]} {"type":"discussion","id":"DSC-0020","status":"done","ticket":"jenkins-gitea-integration","title":"Jenkins Gitea Integration and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins","gitea"],"agendas":[{"id":"AGD-0018","file":"AGD-0018-jenkins-gitea-integration-and-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0003","file":"DEC-0003-jenkins-gitea-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0003","file":"PLN-0003-jenkins-gitea-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0021","file":"discussion/lessons/DSC-0020-jenkins-gitea-integration/LSN-0021-jenkins-gitea-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]} {"type":"discussion","id":"DSC-0021","status":"done","ticket":"asset-entry-codec-enum-with-metadata","title":"Asset Entry Codec Enum Contract","created_at":"2026-04-09","updated_at":"2026-04-09","tags":["asset","runtime","codec","metadata"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0024","file":"discussion/lessons/DSC-0021-asset-entry-codec-enum-contract/LSN-0024-string-on-the-wire-enum-in-runtime.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]} @@ -30,5 +30,5 @@ {"type":"discussion","id":"DSC-0019","status":"done","ticket":"jenkinsfile-correction","title":"Jenkinsfile Correction and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins"],"agendas":[{"id":"AGD-0017","file":"AGD-0017-jenkinsfile-correction.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0002","file":"DEC-0002-jenkinsfile-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0002","file":"PLN-0002-jenkinsfile-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0020","file":"discussion/lessons/DSC-0019-jenkins-ci-standardization/LSN-0020-jenkins-standard-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]} {"type":"discussion","id":"DSC-0030","status":"done","ticket":"internal-viewport-270p","title":"Agenda - Internal Viewport 270p (480x270)","created_at":"2026-04-27","updated_at":"2026-04-28","tags":["gfx","runtime","viewport","resolution","frame-composer","host"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0039","file":"discussion/lessons/DSC-0030-internal-viewport-270p/LSN-0039-resolution-baseline-changes-are-runtime-wide-contracts.md","status":"done","created_at":"2026-04-28","updated_at":"2026-04-28"}]} {"type":"discussion","id":"DSC-0031","status":"done","ticket":"runtime-mode-separation-game-system","title":"Agenda - Runtime Mode Separation: Game and System","created_at":"2026-05-11","updated_at":"2026-05-14","tags":["runtime","firmware","hub","system-apps","game-mode","scheduler"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0040","file":"discussion/lessons/DSC-0031-runtime-mode-separation-game-system/LSN-0040-system-pipeline-separation.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14"}]} -{"type":"discussion","id":"DSC-0032","status":"open","ticket":"system-os-lifecycle-process-task-contract","title":"Agenda - SystemOS Lifecycle, Process and Task Contract","created_at":"2026-05-14","updated_at":"2026-05-14","tags":["runtime","os","lifecycle","process","task","shell","firmware"],"agendas":[{"id":"AGD-0032","file":"AGD-0032-system-os-lifecycle-process-task-contract.md","status":"open","created_at":"2026-05-14","updated_at":"2026-05-14"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0032","status":"in_progress","ticket":"system-os-lifecycle-process-task-contract","title":"Agenda - SystemOS Lifecycle, Process and Task Contract","created_at":"2026-05-14","updated_at":"2026-05-15","tags":["runtime","os","lifecycle","process","task","shell","firmware"],"agendas":[{"id":"AGD-0032","file":"AGD-0032-system-os-lifecycle-process-task-contract.md","status":"accepted","created_at":"2026-05-14","updated_at":"2026-05-15"}],"decisions":[{"id":"DEC-0025","file":"DEC-0025-systemos-lifecycle-authority-for-tasks-and-processes.md","status":"accepted","created_at":"2026-05-15","updated_at":"2026-05-15","ref_agenda":"AGD-0032"}],"plans":[{"id":"PLN-0055","file":"PLN-0055-systemos-lifecycle-core-api.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0025"]},{"id":"PLN-0056","file":"PLN-0056-firmware-lifecycle-delegation.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0025"]},{"id":"PLN-0057","file":"PLN-0057-lifecycle-tests-and-invariants.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0025"]}],"lessons":[]} {"type":"discussion","id":"DSC-0033","status":"in_progress","ticket":"system-os-service-ownership-and-module-layout","title":"Agenda - SystemOS Service Ownership and Module Layout","created_at":"2026-05-14","updated_at":"2026-05-14","tags":["runtime","os","services","module-layout","vm","window-manager","logging"],"agendas":[{"id":"AGD-0033","file":"AGD-0033-system-os-service-ownership-and-module-layout.md","status":"accepted","created_at":"2026-05-14","updated_at":"2026-05-14"}],"decisions":[{"id":"DEC-0024","file":"DEC-0024-systemos-service-ownership-and-module-layout.md","status":"accepted","created_at":"2026-05-14","updated_at":"2026-05-14","ref_agenda":"AGD-0033"}],"plans":[{"id":"PLN-0051","file":"PLN-0051-move-vm-runtime-into-services.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14","ref_decisions":["DEC-0024"]},{"id":"PLN-0052","file":"PLN-0052-promote-window-manager-to-systemos-service.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14","ref_decisions":["DEC-0024"]},{"id":"PLN-0053","file":"PLN-0053-move-logging-ownership-to-systemos.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14","ref_decisions":["DEC-0024"]},{"id":"PLN-0054","file":"PLN-0054-move-fs-and-memcard-ownership-to-systemos.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14","ref_decisions":["DEC-0024"]}],"lessons":[]} diff --git a/discussion/workflow/agendas/AGD-0032-system-os-lifecycle-process-task-contract.md b/discussion/workflow/agendas/AGD-0032-system-os-lifecycle-process-task-contract.md index 425b59cb..913f6184 100644 --- a/discussion/workflow/agendas/AGD-0032-system-os-lifecycle-process-task-contract.md +++ b/discussion/workflow/agendas/AGD-0032-system-os-lifecycle-process-task-contract.md @@ -2,10 +2,10 @@ id: AGD-0032 ticket: system-os-lifecycle-process-task-contract title: Agenda - SystemOS Lifecycle, Process and Task Contract -status: open +status: accepted created: 2026-05-14 -resolved: -decision: +resolved: 2026-05-15 +decision: DEC-0025 tags: [runtime, os, lifecycle, process, task, shell, firmware] --- @@ -65,7 +65,7 @@ AppMode::Game -> marca task como Foreground -> entra em GameRunningStep com TaskId -AppMode::System +AppMode::Shell -> cria processo VmShell -> cria task Shell -> marca task como Foreground @@ -249,38 +249,83 @@ O primeiro corte deve ser pequeno: O `WindowManager` deve ser a agenda/decisão seguinte: promovê-lo a serviço real do sistema quando a base `Task/Process/Lifecycle` estiver fechada. -Também precisamos resolver a nomenclatura `System` versus `Shell`. O código e o -contrato anterior usam `AppMode::System`; a narrativa nova fala em -`AppMode::Shell`. A agenda deve decidir se: - -- mantemos `AppMode::System` como perfil formal e usamos `Shell` como tipo de - task/processo/UI; ou -- renomeamos o perfil público para `Shell`, com migração explícita. - -Minha inclinação inicial é manter `AppMode::System` por compatibilidade de -contrato e usar `Shell` para a camada de UI/app gerenciado dentro do OS. +`AppMode::System` já foi renomeado para `AppMode::Shell`. A nomenclatura pública +fica alinhada com o modelo atual: `AppMode::Game` executa cartuchos como +jogo/session fullscreen, enquanto `AppMode::Shell` executa cartuchos como apps +gerenciados pela Shell do Prometeu OS. ## Perguntas em Aberto -- [ ] `SystemOS` deve expor `suspend_task`, `resume_task`, `close_task` e - `crash_task` já no próximo corte? -- [ ] `foreground_task` e `background_task` devem ser operações explícitas do - `SystemOS`, ou apenas detalhe do `TaskManager`? -- [ ] Qual é a semântica exata de `Background`: processo ainda `Running`, +- [x] `SystemOS` deve expor `suspend_task`, `resume_task`, `close_task` e + `crash_task` já no próximo corte? Sim. Eles são a próxima API semântica de + lifecycle e evitam que firmware ou outros chamadores manipulem + `TaskManager` e `ProcessManager` separadamente. +- [x] `foreground_task` e `background_task` devem ser operações explícitas do + `SystemOS`, ou apenas detalhe do `TaskManager`? `set_foreground_task(task_id)` + deve ser operação explícita do `SystemOS`. `background_task` fica fora da + primeira wave. +- [x] Qual é a semântica exata de `Background`: processo ainda `Running`, processo `Suspended`, ou estado de presença sem garantia de execução? -- [ ] `resume_task` deve sempre mover a task para `Foreground`, ou pode retomar - para `Background`? -- [ ] O que acontece quando a task existe mas o processo associado não existe? -- [ ] `close_task` remove imediatamente entidades fechadas ou apenas marca - `Closed`/`Stopped` para coleta posterior? -- [ ] `crash_task` deve receber/armazenar `CrashReport`, ou isso fica em outro - serviço de diagnóstico? -- [ ] `GameRunningStep` deve transicionar para Hub/Home quando sua task deixa de - estar `Foreground`, ou apenas não executar frame? -- [ ] O perfil de manifesto continua `AppMode::System`, ou abrimos uma migração - formal para `AppMode::Shell`? -- [ ] A decisão de ownership/layout em `AGD-0033` deve ser pré-requisito para o - primeiro plan de lifecycle, ou pode ser executada em paralelo? + Fora de escopo nesta wave. `Background` permanece estado reservado sem + semântica normativa no primeiro contrato. +- [x] `resume_task` deve sempre mover a task para `Foreground`, ou pode retomar + para `Background`? Nesta wave, `resume_task` sempre move para + `Foreground` e move o processo associado para `Running`. +- [x] O que acontece quando a task existe mas o processo associado não existe? + Isso é erro de lifecycle e deve retornar erro tipado, não `bool`. +- [x] `close_task` remove imediatamente entidades fechadas ou apenas marca + `Closed`/`Stopped` para coleta posterior? Apenas marca `TaskState::Closed` e + `ProcessState::Stopped`; coleta é operação futura separada. +- [x] `crash_task` deve receber/armazenar `CrashReport`, ou isso fica em outro + serviço de diagnóstico? A operação deve aceitar ou preparar o contrato para + `CrashReport`, mas a primeira wave pode apenas coordenar estados e deixar o + armazenamento para diagnóstico/logging futuro. +- [x] `GameRunningStep` deve transicionar para Hub/Home quando sua task deixa de + estar `Foreground`, ou apenas não executar frame? Nesta wave, mantém a + invariant forte: `GameRunningStep` só suporta game task em `Foreground`; outro + estado é inválido e deve gerar diagnóstico/crash claro. +- [x] O perfil de manifesto continua `AppMode::System`, ou abrimos uma migração + formal para `AppMode::Shell`? Resolvido: `AppMode::System` já virou + `AppMode::Shell`. +- [x] A decisão de ownership/layout em `AGD-0033` deve ser pré-requisito para o + primeiro plan de lifecycle, ou pode ser executada em paralelo? Pode executar + em paralelo e não bloqueia esta wave. O plan de lifecycle deve evitar mover + `WindowManager`, `VirtualMachineRuntime`, Hub ou background services. + +## Resolucao + +A agenda fecha pela **Opcao B**: centralizar lifecycle no `SystemOS`. + +`SystemOS` deixa de ser apenas um agregador de serviços e passa a ser a +autoridade normativa para coordenar `TaskState` e `ProcessState`. `TaskManager` +e `ProcessManager` continuam existindo, mas como mecanismos internos de +armazenamento e transição simples. O firmware não deve decidir diretamente como +task e processo mudam juntos. + +A primeira wave deve cobrir apenas: + +- `set_foreground_task(task_id)` +- `suspend_task(task_id)` +- `resume_task(task_id)` +- `close_task(task_id)` +- `crash_task(task_id, report?)` + +O mapeamento inicial é: + +```text +TaskState::Foreground -> ProcessState::Running +TaskState::Suspended -> ProcessState::Suspended +TaskState::Closed -> ProcessState::Stopped +TaskState::Crashed -> ProcessState::Crashed +``` + +`Background` permanece reservado e explicitamente fora da primeira wave. + +O primeiro plan recomendado é `PLAN-0032-A: SystemOS Lifecycle API`, com escopo +limitado a erro tipado, métodos de lifecycle em `SystemOS`, coordenação de +`TaskState`/`ProcessState`, testes unitários no nível `SystemOS` e nenhuma +mudança em `WindowManager`, `PrometeuHub`, background services ou UX completa de +suspensão/retomada. ## Criterio para Encerrar diff --git a/discussion/workflow/decisions/DEC-0025-systemos-lifecycle-authority-for-tasks-and-processes.md b/discussion/workflow/decisions/DEC-0025-systemos-lifecycle-authority-for-tasks-and-processes.md new file mode 100644 index 00000000..de7fe393 --- /dev/null +++ b/discussion/workflow/decisions/DEC-0025-systemos-lifecycle-authority-for-tasks-and-processes.md @@ -0,0 +1,203 @@ +--- +id: DEC-0025 +ticket: system-os-lifecycle-process-task-contract +title: SystemOS Lifecycle Authority for Tasks and Processes +status: accepted +created: 2026-05-15 +ref_agenda: AGD-0032 +tags: [runtime, os, lifecycle, process, task, shell, firmware] +--- + +## Status + +Accepted. Esta decisão foi aceita em 2026-05-15 e passa a ser referência +normativa para a primeira wave de lifecycle de `Task` e `Process` no +`SystemOS`. + +## Contexto + +`SystemOS` já agrega serviços centrais do Prometeu OS, incluindo runtime VM, +processos e tasks. A arquitetura anterior ainda permitia que firmware, +`TaskManager` e `ProcessManager` decidissem transições de lifecycle de forma +distribuída. + +Esse arranjo é insuficiente para um console OS: `Task` representa presença +navegável e experiência do usuário, enquanto `Process` representa execução +técnica. Essas duas dimensões precisam mudar juntas por uma autoridade +semântica única. + +`DEC-0024` já separou ownership/layout de serviços do `SystemOS`. Esta decisão +é focada apenas em lifecycle de `Task`/`Process` e não reabre a migração de +serviços, `WindowManager`, Hub ou ownership da VM. + +`AppMode::System` já foi renomeado para `AppMode::Shell`. O contrato público +fica: + +- `AppMode::Game`: cartucho executado como jogo/session fullscreen. +- `AppMode::Shell`: cartucho executado como app gerenciado pela Shell do + Prometeu OS. + +## Decisao + +`SystemOS` SHALL ser a autoridade normativa de lifecycle para `Task` e +`Process`. + +`TaskManager` e `ProcessManager` SHALL continuar existindo como mecanismos +internos de armazenamento e transição local, mas MUST NOT ser a API semântica +principal para lifecycle coordenado. Firmware e outros chamadores MUST delegate +transições semânticas de lifecycle para `SystemOS`. + +A primeira wave de lifecycle SHALL expor o contrato mínimo: + +```text +set_foreground_task(task_id) +suspend_task(task_id) +resume_task(task_id) +close_task(task_id) +crash_task(task_id, report?) +``` + +Essas operações SHALL coordenar `TaskState` e `ProcessState` juntos. + +O mapeamento normativo inicial SHALL ser: + +```text +TaskState::Foreground -> ProcessState::Running +TaskState::Suspended -> ProcessState::Suspended +TaskState::Closed -> ProcessState::Stopped +TaskState::Crashed -> ProcessState::Crashed +``` + +`Background` SHALL remain reserved and MUST NOT guide any public lifecycle API +in the first wave. + +`resume_task` SHALL mean resuming active user-perceived execution. In this first +wave, it MUST move a suspended task to `TaskState::Foreground` and its associated +process to `ProcessState::Running`. + +`close_task` SHALL mark task/process state only. It MUST NOT remove task or +process entities immediately. Collection of closed/stopped entities is a future +separate operation. + +`crash_task` SHALL be the semantic crash operation for a task. The first wave MAY +only coordinate `TaskState::Crashed` and `ProcessState::Crashed`, but the +signature SHOULD either accept or remain compatible with a future `CrashReport` +handoff to diagnostics/logging. + +`GameRunningStep` SHALL keep the strong invariant that an active game execution +step only supports a game task in `TaskState::Foreground`. In this wave, any +non-foreground game task observed by `GameRunningStep` is invalid and should +produce clear diagnostic/crash behavior rather than silently running frames. + +## Rationale + +Centralizing lifecycle in `SystemOS` prevents firmware from becoming the de +facto OS policy layer. Firmware should remain a boot/execution state machine; it +should not know how to update `TaskState` and `ProcessState` in lockstep. + +Keeping `TaskManager` and `ProcessManager` as internal mechanisms preserves +simple storage and local transition responsibilities while giving the OS a +single semantic boundary for suspend, resume, close, crash and foreground +selection. + +Leaving `Background` out of the first wave avoids freezing an incomplete model +for docked apps, background services, non-focused shell apps or capability-gated +execution. The reserved enum state can remain, but no normative public lifecycle +API should depend on it yet. + +Making `resume_task` return to `Foreground` keeps the first contract simple and +matches the user-perceived meaning of resume. Future background wake/service +semantics should be introduced as separate concepts rather than overloading +`resume_task`. + +Marking closed/stopped entities instead of removing them immediately preserves +room for diagnostics, crash handling, system UI history and later collection +policy. + +## Invariantes / Contrato + +- `SystemOS` MUST coordinate lifecycle operations that affect both task and + process state. +- Firmware MUST NOT call separate task/process manager transitions to implement + semantic lifecycle operations such as suspend, resume, close or crash. +- `set_foreground_task(task_id)` MUST set the task to `TaskState::Foreground` + and its associated process to `ProcessState::Running`. +- `suspend_task(task_id)` MUST set the task to `TaskState::Suspended` and its + associated process to `ProcessState::Suspended`. +- `resume_task(task_id)` MUST move from suspended user-visible execution back to + foreground execution: `TaskState::Foreground` and `ProcessState::Running`. +- `close_task(task_id)` MUST set `TaskState::Closed` and + `ProcessState::Stopped`. +- `crash_task(task_id, report?)` MUST set `TaskState::Crashed` and + `ProcessState::Crashed`. +- A missing task MUST return a typed lifecycle error such as + `LifecycleError::TaskNotFound(TaskId)`. +- A task whose associated process is missing MUST return a typed lifecycle error + such as `LifecycleError::ProcessNotFound(ProcessId)`. +- Invalid lifecycle transitions SHOULD return a typed error carrying the task, + source state and requested operation. +- `Background` MUST NOT have normative first-wave semantics. +- This decision MUST NOT move `WindowManager`, redesign `PrometeuHub`, introduce + background services, implement docking, implement app switching, or make + `ProcessManager` own the VM. + +The expected first-wave error shape is: + +```rust +pub enum LifecycleError { + TaskNotFound(TaskId), + ProcessNotFound(ProcessId), + InvalidTransition { + task_id: TaskId, + from: TaskState, + operation: LifecycleOperation, + }, +} +``` + +`LifecycleOperation` SHOULD identify at least: + +```text +SetForeground +Suspend +Resume +Close +Crash +``` + +## Impactos + +- `prometeu-system` must expose lifecycle methods on `SystemOS`. +- `TaskManager` and `ProcessManager` may need smaller internal transition + helpers, but not public semantic ownership of lifecycle. +- Firmware code should call `system_os.suspend_task(...)`, + `system_os.resume_task(...)`, `system_os.close_task(...)`, + `system_os.crash_task(...)` and `system_os.set_foreground_task(...)` instead + of coordinating task/process managers directly. +- Tests should move lifecycle assertions to the `SystemOS` level. +- Specs do not need to change before the first code plan unless the lifecycle + API is promoted to a public external contract. +- `DEC-0024` remains valid and complementary; ownership/layout work can execute + in parallel and does not block this first lifecycle wave. + +## Referencias + +- Agenda: `AGD-0032` +- Related decision: `DEC-0024` + +## Propagacao Necessaria + +- Plan: create `PLAN-0032-A: SystemOS Lifecycle API`. +- Code: update `prometeu-system` `SystemOS`, task/process lifecycle helpers, + firmware callsites if they currently coordinate managers directly, and unit + tests. +- Tests: add unit coverage for successful lifecycle mappings, missing task, + missing process and invalid transition handling. +- Discussion: keep `Background` reserved for a future agenda covering dock, + services, non-focused shell apps and capability-gated execution. +- Learn: after implementation is complete, write or update lessons only from the + published state. + +## Revisao + +- 2026-05-15: Initial accepted decision from `AGD-0032`. diff --git a/discussion/workflow/plans/PLN-0055-systemos-lifecycle-core-api.md b/discussion/workflow/plans/PLN-0055-systemos-lifecycle-core-api.md new file mode 100644 index 00000000..51bfd989 --- /dev/null +++ b/discussion/workflow/plans/PLN-0055-systemos-lifecycle-core-api.md @@ -0,0 +1,121 @@ +--- +id: PLN-0055 +ticket: system-os-lifecycle-process-task-contract +title: SystemOS Lifecycle Core API +status: done +created: 2026-05-15 +ref_decisions: [DEC-0025] +tags: [runtime, os, lifecycle, process, task, shell, firmware] +--- + +## Briefing + +Implement the first-wave `SystemOS` lifecycle API required by `DEC-0025`. +`SystemOS` becomes the semantic coordinator for `TaskState` and `ProcessState`; +`TaskManager` and `ProcessManager` remain storage and simple transition +mechanisms. + +## Source Decisions + +- `DEC-0025`: `SystemOS` is the lifecycle authority for tasks and processes. + +## Target + +Add typed lifecycle operations to `prometeu-system` without changing firmware +behavior, WindowManager ownership, Hub UI, background services, docking, app +switching, or VM ownership. + +## Scope + +- Add `LifecycleError` and `LifecycleOperation` in `prometeu-system`. +- Add lifecycle methods on `SystemOS`: + - `set_foreground_task(task_id)` + - `suspend_task(task_id)` + - `resume_task(task_id)` + - `close_task(task_id)` + - `crash_task(task_id, report?)` +- Coordinate task and process state changes inside `SystemOS`. +- Preserve `TaskManager` and `ProcessManager` as internal state managers. +- Keep `Background` reserved and out of public lifecycle semantics. + +## Out of Scope + +- Background semantics. +- Docking. +- Background services. +- App switcher. +- WindowManager migration. +- PrometeuHub redesign. +- Game suspend/resume UX. +- ProcessManager owning VM execution. +- Collection/removal of closed or stopped entities. + +## Execution Plan + +1. Add lifecycle types. + - Create a lifecycle module or colocate the types under + `crates/console/prometeu-system/src/os/`. + - Define `LifecycleOperation` with `SetForeground`, `Suspend`, `Resume`, + `Close`, and `Crash`. + - Define `LifecycleError` with `TaskNotFound(TaskId)`, + `ProcessNotFound(ProcessId)`, and `InvalidTransition { task_id, from, + operation }`. + - Re-export the types through `prometeu-system` only if the existing public + module style requires it. + +2. Add the `SystemOS` coordination helpers. + - Target file: `crates/console/prometeu-system/src/os/system_os.rs`. + - Resolve the task first through `task_manager`. + - Extract the task's associated `ProcessId`. + - Validate that the process exists before mutating state. + - Return typed errors instead of `bool`. + +3. Implement first-wave operations. + - `set_foreground_task`: call `task_manager.set_foreground(task_id)` and + `process_manager.mark_running(process_id)`. + - `suspend_task`: set `TaskState::Suspended` and + `ProcessState::Suspended`. + - `resume_task`: require a suspended task, then set + `TaskState::Foreground` and `ProcessState::Running`. + - `close_task`: set `TaskState::Closed` and `ProcessState::Stopped`. + - `crash_task`: set `TaskState::Crashed` and `ProcessState::Crashed`. + +4. Keep manager APIs mechanical. + - Target files: + `crates/console/prometeu-system/src/services/task/task_manager.rs` and + `crates/console/prometeu-system/src/services/process/process_manager.rs`. + - Do not remove existing manager methods in this plan. + - Add only narrow helpers if needed to support the `SystemOS` API cleanly. + +5. Update task creation to use the lifecycle API internally. + - In `SystemOS::create_vm_game_task`, `create_vm_shell_task`, and + `create_native_shell_task`, replace direct foreground manager calls with + `set_foreground_task`. + - Keep the constructors returning `TaskId`; handle impossible internal + errors with explicit debug/assert behavior or an internal helper that + cannot fail after creation. + +## Acceptance Criteria + +- `SystemOS` exposes all five first-wave lifecycle methods from `DEC-0025`. +- All five methods coordinate both task and process state. +- Missing task returns `LifecycleError::TaskNotFound`. +- Missing associated process returns `LifecycleError::ProcessNotFound`. +- Invalid resume from a non-suspended task returns `InvalidTransition`. +- `Background` is not exposed through a new lifecycle API. +- Closed and stopped entities are not removed by `close_task`. + +## Tests / Validation + +- Add or update unit tests in `prometeu-system` for each lifecycle mapping. +- Add tests for missing task, missing process, and invalid resume transition. +- Run the affected Rust test suite for `prometeu-system`. + +## Risks + +- Existing direct manager access is public enough that callsites may keep using + it. This plan does not remove those methods; enforcement continues in + `PLN-0056`. +- `TaskManager::set_foreground` currently moves the previous foreground task to + `Background`. Since `Background` is out of first-wave semantics, tests must + avoid treating that transition as normative lifecycle policy. diff --git a/discussion/workflow/plans/PLN-0056-firmware-lifecycle-delegation.md b/discussion/workflow/plans/PLN-0056-firmware-lifecycle-delegation.md new file mode 100644 index 00000000..5c2500d9 --- /dev/null +++ b/discussion/workflow/plans/PLN-0056-firmware-lifecycle-delegation.md @@ -0,0 +1,119 @@ +--- +id: PLN-0056 +ticket: system-os-lifecycle-process-task-contract +title: Firmware Lifecycle Delegation +status: done +created: 2026-05-15 +ref_decisions: [DEC-0025] +tags: [runtime, os, lifecycle, process, task, shell, firmware] +--- + +## Briefing + +Move firmware lifecycle callsites to the `SystemOS` semantic API introduced by +`PLN-0055`. Firmware remains a boot and execution state machine; it must not +coordinate task/process lifecycle by calling `TaskManager` and `ProcessManager` +separately. + +## Source Decisions + +- `DEC-0025`: firmware must delegate semantic lifecycle transitions to + `SystemOS`. + +## Target + +Replace direct lifecycle manipulation in firmware with `SystemOS` lifecycle +methods while preserving current observable behavior. + +## Scope + +- Update cartridge launch paths to use `SystemOS` lifecycle authority when + placing newly created tasks in foreground. +- Update crash paths where a task crash is detected so task/process state moves + through `SystemOS::crash_task`. +- Preserve the strong `GameRunningStep` invariant: a running game step only + supports a game task in `TaskState::Foreground`. +- Keep `SystemRunningStep`, Hub, and WindowManager behavior unchanged except for + lifecycle delegation required by `DEC-0025`. + +## Out of Scope + +- Full game suspend/resume UX. +- Returning to Hub/Home on suspend or close. +- Shell UI redesign. +- App switcher. +- Background task API. +- WindowManager migration. +- Diagnostics service or crash report persistence. + +## Execution Plan + +1. Audit direct lifecycle manager usage. + - Target files: + `crates/console/prometeu-firmware/src/firmware/*.rs`, + `crates/console/prometeu-system/src/os/system_os.rs`. + - Search for direct calls to `task_manager.set_foreground`, + `task_manager.mark_suspended`, `task_manager.close_task`, + `task_manager.mark_crashed`, `process_manager.mark_suspended`, + `process_manager.mark_stopped`, and `process_manager.mark_crashed`. + +2. Keep launch construction inside `SystemOS`. + - Target file: + `crates/console/prometeu-system/src/os/system_os.rs`. + - Ensure `create_vm_game_task`, `create_vm_shell_task`, and + `create_native_shell_task` coordinate foreground/running state through the + lifecycle core API from `PLN-0055`. + +3. Update game crash handling. + - Target file: + `crates/console/prometeu-firmware/src/firmware/firmware_step_game_running.rs`. + - When the task is missing, preserve current crash-screen behavior. If the + task does not exist, `SystemOS::crash_task` cannot attach state and the + firmware should continue producing a clear crash report. + - When a foreground invariant fails for an existing task, call + `ctx.os.crash_task(task_id, report?)` before transitioning to + `AppCrashesStep`. + - When `tick_vm` returns a crash report for the active task, call + `ctx.os.crash_task(task_id, report?)` before transitioning to + `AppCrashesStep`. + +4. Keep shell launch behavior unchanged. + - Target file: + `crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs`. + - Do not move `WindowManager` logic. + - Do not change shell window creation or focus behavior. + - Rely on `SystemOS::create_vm_shell_task` to coordinate task/process + foreground state. + +5. Avoid introducing background lifecycle. + - Do not add `background_task`. + - Do not treat `TaskState::Background` as a public lifecycle result in + firmware. + +## Acceptance Criteria + +- Firmware does not coordinate task/process lifecycle by manually changing both + managers. +- Game VM crash results in `SystemOS::crash_task` for the active task when the + task still exists. +- Foreground invariant violation in `GameRunningStep` crashes the task through + `SystemOS`. +- Launch behavior for `AppMode::Game` and `AppMode::Shell` remains unchanged. +- WindowManager and Hub behavior are not redesigned or moved. + +## Tests / Validation + +- Run firmware tests that cover cartridge launch for `AppMode::Game` and + `AppMode::Shell`. +- Add or update tests for `GameRunningStep` crash delegation if the existing + test harness supports firmware step testing. +- Run the affected Rust test suite for `prometeu-firmware`. + +## Risks + +- Crash report ownership is not final. This plan should pass the report through + the lifecycle API only to the extent supported by `PLN-0055`; persistence + remains out of scope. +- `GameRunningStep` currently reads task state directly. This is acceptable for + enforcing the foreground invariant, but it must not become a place that + coordinates lifecycle state across managers. diff --git a/discussion/workflow/plans/PLN-0057-lifecycle-tests-and-invariants.md b/discussion/workflow/plans/PLN-0057-lifecycle-tests-and-invariants.md new file mode 100644 index 00000000..99a4b181 --- /dev/null +++ b/discussion/workflow/plans/PLN-0057-lifecycle-tests-and-invariants.md @@ -0,0 +1,110 @@ +--- +id: PLN-0057 +ticket: system-os-lifecycle-process-task-contract +title: Lifecycle Tests and Invariants +status: done +created: 2026-05-15 +ref_decisions: [DEC-0025] +tags: [runtime, os, lifecycle, process, task, shell, firmware] +--- + +## Briefing + +Add focused test coverage and invariant checks for the lifecycle contract +introduced by `PLN-0055` and consumed by `PLN-0056`. + +## Source Decisions + +- `DEC-0025`: first-wave lifecycle maps task states to process states through + `SystemOS` and keeps `Background` outside the public lifecycle contract. + +## Target + +Make the lifecycle contract executable through unit tests and regression tests, +without expanding scope into background execution, UI behavior, or service +ownership. + +## Scope + +- Add `SystemOS` unit tests for all first-wave lifecycle operations. +- Add negative tests for missing task, missing process, and invalid transition. +- Add firmware-level regression coverage for lifecycle delegation where the + existing test harness can observe it. +- Keep manager tests focused on local storage and simple transitions. + +## Out of Scope + +- End-to-end UX tests for suspend/resume. +- Hub/Home navigation after lifecycle changes. +- Background services. +- Docking. +- App switching. +- Diagnostics persistence. +- WindowManager behavior changes. + +## Execution Plan + +1. Add core lifecycle mapping tests. + - Target file: `crates/console/prometeu-system/src/os/system_os.rs` or a + dedicated `os` test module following existing crate style. + - Verify: + - `set_foreground_task`: task becomes `Foreground`, process becomes + `Running`. + - `suspend_task`: task becomes `Suspended`, process becomes `Suspended`. + - `resume_task`: suspended task becomes `Foreground`, process becomes + `Running`. + - `close_task`: task becomes `Closed`, process becomes `Stopped`. + - `crash_task`: task becomes `Crashed`, process becomes `Crashed`. + +2. Add error tests. + - Verify missing task returns `LifecycleError::TaskNotFound`. + - Verify a task whose process no longer exists returns + `LifecycleError::ProcessNotFound`. + - Verify invalid resume from non-suspended task returns + `LifecycleError::InvalidTransition` with operation `Resume`. + +3. Add non-removal tests. + - Verify `close_task` leaves the closed task available through + `TaskManager::get`. + - Verify `close_task` leaves the stopped process available through + `ProcessManager::get`. + - Do not call `remove_closed` or `remove_stopped` as part of lifecycle API + tests. + +4. Add background exclusion tests. + - Assert there is no `SystemOS::background_task` API added by this wave. + - Keep any existing `TaskState::Background` behavior in manager-level tests + documented as manager behavior, not first-wave lifecycle semantics. + +5. Add firmware regression tests after `PLN-0056`. + - Target file: `crates/console/prometeu-firmware/src/firmware/firmware.rs` + or the relevant step test module. + - Verify game launch still enters `GameRunningStep`. + - Verify shell launch still enters `SystemRunningStep`. + - Verify a game crash path marks the active task as crashed when the test + harness can observe `ctx.os.task_manager`. + +## Acceptance Criteria + +- Every lifecycle method from `DEC-0025` has a positive `SystemOS` test. +- Missing task and missing process return typed lifecycle errors. +- Invalid resume transition is covered by a test. +- `close_task` is tested as mark-only, not removal. +- No test encodes `Background` as a normative first-wave lifecycle operation. +- Firmware regression tests still pass for game and shell launch. + +## Tests / Validation + +- Run `cargo test -p prometeu-system`. +- Run `cargo test -p prometeu-firmware`. +- If workspace-level tests are practical after these changes, run the relevant + workspace command used by this repository. + +## Risks + +- Some negative tests may require test-only helpers to create an inconsistent + task/process relationship. Prefer narrow test fixtures over making corruption + APIs public. +- Compile-time absence of `background_task` may not be directly testable + without compile-fail infrastructure. In that case, validate it by code review + and keep runtime tests focused on the exposed first-wave methods.