diff --git a/crates/console/prometeu-hal/src/cartridge.rs b/crates/console/prometeu-hal/src/cartridge.rs index 972c755f..6ab7e048 100644 --- a/crates/console/prometeu-hal/src/cartridge.rs +++ b/crates/console/prometeu-hal/src/cartridge.rs @@ -297,7 +297,7 @@ pub enum Capability { All, } -#[derive(Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct CartridgeManifest { pub magic: String, pub cartridge_version: u32, diff --git a/crates/console/prometeu-system/src/lib.rs b/crates/console/prometeu-system/src/lib.rs index c0c052f0..bdce77e5 100644 --- a/crates/console/prometeu-system/src/lib.rs +++ b/crates/console/prometeu-system/src/lib.rs @@ -13,6 +13,9 @@ pub use services::async_work::{ AsyncWorkLaneController, AsyncWorkLaneError, AsyncWorkLaneTelemetry, AsyncWorkPriority, }; pub use services::fs; +pub use services::game_library::{ + GameLibrary, GameLibraryDiagnostic, GameLibraryEntry, discover_games_root, +}; pub use services::memcard::MemcardAsyncLaneOperation; pub use services::process; pub use services::task; diff --git a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs index 947a8551..8c1a9efb 100644 --- a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs +++ b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs @@ -1,4 +1,4 @@ -use crate::{CrashReport, SystemOS}; +use crate::{CrashReport, GameLibrary, SystemOS}; use prometeu_hal::color::Color; use prometeu_hal::primitives::Rect; use prometeu_hal::{ @@ -29,6 +29,7 @@ const COLOR_BUTTON_ACTIVE: Color = Color::rgb(72, 91, 122); /// PrometeuHub: Launcher and system UI environment. pub struct PrometeuHub { home_input_armed: bool, + game_library: GameLibrary, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -91,13 +92,21 @@ impl Default for PrometeuHub { impl PrometeuHub { pub fn new() -> Self { - Self { home_input_armed: false } + Self { home_input_armed: false, game_library: GameLibrary::empty() } } pub fn init(&mut self) { self.home_input_armed = false; } + pub fn set_game_library(&mut self, game_library: GameLibrary) { + self.game_library = game_library; + } + + pub fn game_library(&self) -> &GameLibrary { + &self.game_library + } + pub fn gui_update( &mut self, os: &mut SystemOS, diff --git a/crates/console/prometeu-system/src/services/game_library.rs b/crates/console/prometeu-system/src/services/game_library.rs new file mode 100644 index 00000000..bbf9728c --- /dev/null +++ b/crates/console/prometeu-system/src/services/game_library.rs @@ -0,0 +1,270 @@ +use prometeu_hal::app_mode::AppMode; +use prometeu_hal::cartridge::CartridgeManifest; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::SystemTime; + +#[derive(Debug, Clone)] +pub struct GameLibraryEntry { + pub manifest: CartridgeManifest, + pub title: String, + pub app_id: u32, + pub app_version: String, + pub path: PathBuf, + pub discovered_at: SystemTime, +} + +impl GameLibraryEntry { + fn new(path: PathBuf, manifest: CartridgeManifest, discovered_at: SystemTime) -> Self { + Self { + title: manifest.title.clone(), + app_id: manifest.app_id, + app_version: manifest.app_version.clone(), + manifest, + path, + discovered_at, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GameLibraryDiagnostic { + pub path: PathBuf, + pub reason: GameLibraryDiagnosticReason, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GameLibraryDiagnosticReason { + RootNotDirectory, + CandidateReadFailed, + MissingManifest, + InvalidManifest, + UnsupportedVersion, + MissingProgram, + NonGameAppMode, +} + +#[derive(Debug, Clone, Default)] +pub struct GameLibrary { + entries: Vec, + diagnostics: Vec, +} + +impl GameLibrary { + pub fn empty() -> Self { + Self::default() + } + + pub fn entries(&self) -> &[GameLibraryEntry] { + &self.entries + } + + pub fn diagnostics(&self) -> &[GameLibraryDiagnostic] { + &self.diagnostics + } + + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } +} + +pub fn discover_games_root(root: impl AsRef) -> GameLibrary { + let root = root.as_ref(); + let discovered_at = SystemTime::now(); + + if !root.is_dir() { + return GameLibrary { + entries: Vec::new(), + diagnostics: vec![GameLibraryDiagnostic { + path: root.to_path_buf(), + reason: GameLibraryDiagnosticReason::RootNotDirectory, + }], + }; + } + + let read_dir = match fs::read_dir(root) { + Ok(read_dir) => read_dir, + Err(_) => { + return GameLibrary { + entries: Vec::new(), + diagnostics: vec![GameLibraryDiagnostic { + path: root.to_path_buf(), + reason: GameLibraryDiagnosticReason::CandidateReadFailed, + }], + }; + } + }; + + let mut entries = Vec::new(); + let mut diagnostics = Vec::new(); + + for item in read_dir { + let Ok(item) = item else { + diagnostics.push(GameLibraryDiagnostic { + path: root.to_path_buf(), + reason: GameLibraryDiagnosticReason::CandidateReadFailed, + }); + continue; + }; + + let path = item.path(); + if !path.is_dir() { + continue; + } + + match discover_candidate(&path, discovered_at) { + Ok(entry) => entries.push(entry), + Err(reason) => diagnostics.push(GameLibraryDiagnostic { path, reason }), + } + } + + entries.sort_by(|left, right| { + left.title + .cmp(&right.title) + .then_with(|| left.app_id.cmp(&right.app_id)) + .then_with(|| left.path.cmp(&right.path)) + }); + + GameLibrary { entries, diagnostics } +} + +fn discover_candidate( + path: &Path, + discovered_at: SystemTime, +) -> Result { + let manifest_path = path.join("manifest.json"); + if !manifest_path.exists() { + return Err(GameLibraryDiagnosticReason::MissingManifest); + } + + let manifest_content = fs::read_to_string(manifest_path) + .map_err(|_| GameLibraryDiagnosticReason::InvalidManifest)?; + let manifest: CartridgeManifest = serde_json::from_str(&manifest_content) + .map_err(|_| GameLibraryDiagnosticReason::InvalidManifest)?; + + if manifest.magic != "PMTU" { + return Err(GameLibraryDiagnosticReason::InvalidManifest); + } + if manifest.cartridge_version != 1 { + return Err(GameLibraryDiagnosticReason::UnsupportedVersion); + } + if manifest.app_mode != AppMode::Game { + return Err(GameLibraryDiagnosticReason::NonGameAppMode); + } + if !path.join("program.pbx").exists() { + return Err(GameLibraryDiagnosticReason::MissingProgram); + } + + Ok(GameLibraryEntry::new(path.to_path_buf(), manifest, discovered_at)) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + use std::sync::atomic::{AtomicU64, Ordering}; + use std::time::UNIX_EPOCH; + + static TEST_DIR_COUNTER: AtomicU64 = AtomicU64::new(0); + + struct TestRoot { + path: PathBuf, + } + + impl TestRoot { + fn new() -> Self { + let unique = TEST_DIR_COUNTER.fetch_add(1, Ordering::Relaxed); + let path = std::env::temp_dir().join(format!( + "prometeu-game-library-test-{}-{}", + std::process::id(), + unique + )); + fs::create_dir_all(&path).expect("test root should be created"); + Self { path } + } + + fn add_candidate(&self, name: &str, manifest: serde_json::Value, with_program: bool) { + let candidate = self.path.join(name); + fs::create_dir_all(&candidate).expect("candidate should be created"); + fs::write(candidate.join("manifest.json"), manifest.to_string()) + .expect("manifest should be written"); + if with_program { + fs::write(candidate.join("program.pbx"), [0_u8]) + .expect("program should be written"); + } + } + } + + impl Drop for TestRoot { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.path); + } + } + + fn manifest(title: &str, app_id: u32, app_mode: &str) -> serde_json::Value { + json!({ + "magic": "PMTU", + "cartridge_version": 1, + "app_id": app_id, + "title": title, + "app_version": "1.0.0", + "app_mode": app_mode + }) + } + + #[test] + fn discovery_returns_only_immediate_valid_game_directories() { + let root = TestRoot::new(); + root.add_candidate("zeta", manifest("Zeta", 2, "Game"), true); + root.add_candidate("alpha", manifest("Alpha", 1, "Game"), true); + root.add_candidate("shell", manifest("Shell", 3, "Shell"), true); + root.add_candidate("missing-program", manifest("Missing Program", 4, "Game"), false); + fs::write(root.path.join("loose-file.txt"), "ignored").expect("file should be written"); + + let nested_parent = root.path.join("nested-parent"); + fs::create_dir_all(nested_parent.join("nested-game")).expect("nested dir should exist"); + fs::write( + nested_parent.join("nested-game").join("manifest.json"), + manifest("Nested", 5, "Game").to_string(), + ) + .expect("nested manifest should be written"); + fs::write(nested_parent.join("nested-game").join("program.pbx"), [0_u8]) + .expect("nested program should be written"); + + let library = discover_games_root(&root.path); + + assert_eq!(library.entries().len(), 2); + assert_eq!(library.entries()[0].title, "Alpha"); + assert_eq!(library.entries()[0].app_id, 1); + assert_eq!(library.entries()[0].app_version, "1.0.0"); + assert_eq!(library.entries()[0].manifest.app_mode, AppMode::Game); + assert!(library.entries()[0].path.ends_with("alpha")); + assert!(library.entries()[0].discovered_at >= UNIX_EPOCH); + assert_eq!(library.entries()[1].title, "Zeta"); + + assert!(library.diagnostics().iter().any(|diagnostic| { + diagnostic.path.ends_with("shell") + && diagnostic.reason == GameLibraryDiagnosticReason::NonGameAppMode + })); + assert!(library.diagnostics().iter().any(|diagnostic| { + diagnostic.path.ends_with("missing-program") + && diagnostic.reason == GameLibraryDiagnosticReason::MissingProgram + })); + assert!(library.diagnostics().iter().any(|diagnostic| { + diagnostic.path.ends_with("nested-parent") + && diagnostic.reason == GameLibraryDiagnosticReason::MissingManifest + })); + } + + #[test] + fn missing_root_returns_diagnostic_and_no_entries() { + let root = TestRoot::new(); + let missing = root.path.join("missing"); + + let library = discover_games_root(&missing); + + assert!(library.is_empty()); + assert_eq!(library.diagnostics().len(), 1); + assert_eq!(library.diagnostics()[0].reason, GameLibraryDiagnosticReason::RootNotDirectory); + } +} diff --git a/crates/console/prometeu-system/src/services/mod.rs b/crates/console/prometeu-system/src/services/mod.rs index efe827c6..1af5a6c7 100644 --- a/crates/console/prometeu-system/src/services/mod.rs +++ b/crates/console/prometeu-system/src/services/mod.rs @@ -1,5 +1,6 @@ pub mod async_work; pub mod fs; +pub mod game_library; pub mod memcard; pub mod process; pub mod task; diff --git a/crates/host/prometeu-host-desktop-winit/src/lib.rs b/crates/host/prometeu-host-desktop-winit/src/lib.rs index a91c231f..0ce99800 100644 --- a/crates/host/prometeu-host-desktop-winit/src/lib.rs +++ b/crates/host/prometeu-host-desktop-winit/src/lib.rs @@ -33,6 +33,10 @@ struct Cli { #[arg(long, value_name = "PATH")] fs_root: Option, + /// Root directory for local game cartridge discovery + #[arg(long, value_name = "PATH")] + games_root: Option, + /// Path to the capability configuration file #[arg(long, value_name = "PATH")] cap: Option, @@ -55,6 +59,9 @@ pub fn run() -> Result<(), Box> { let event_loop = EventLoop::new()?; let mut runner = HostRunner::new(fs_root, cap_config); + if let Some(games_root) = cli.games_root { + runner.set_games_root(games_root); + } runner.set_boot_target(boot_target); event_loop.run_app(&mut runner)?; diff --git a/crates/host/prometeu-host-desktop-winit/src/runner.rs b/crates/host/prometeu-host-desktop-winit/src/runner.rs index feb56c4a..c6a9cc8c 100644 --- a/crates/host/prometeu-host-desktop-winit/src/runner.rs +++ b/crates/host/prometeu-host-desktop-winit/src/runner.rs @@ -12,7 +12,7 @@ use prometeu_drivers::{AudioCommand, LocalFramebufferRenderBackend, MemoryBanks} use prometeu_firmware::{BootTarget, Firmware, FirmwareState}; use prometeu_hal::RuntimePlatform; use prometeu_hal::telemetry::CertificationConfig; -use prometeu_system::{LatestRenderFrameStore, RenderWorkerConfig}; +use prometeu_system::{LatestRenderFrameStore, RenderWorkerConfig, discover_games_root}; use std::sync::Arc; use std::time::{Duration, Instant}; use winit::application::ApplicationHandler; @@ -151,6 +151,18 @@ impl HostRunner { self.debugger.setup_boot_target(&boot_target, &mut self.firmware); } + pub(crate) fn set_games_root(&mut self, games_root: String) { + let library = discover_games_root(&games_root); + for diagnostic in library.diagnostics() { + eprintln!( + "games-root: omitted candidate {}: {:?}", + diagnostic.path.display(), + diagnostic.reason + ); + } + self.firmware.hub.set_game_library(library); + } + /// Creates a new desktop runner instance. pub(crate) fn new(fs_root: Option, cap_config: Option) -> Self { let target_fps = 60; diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 141796e0..f60f71c1 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -14,7 +14,7 @@ {"type":"discussion","id":"DSC-0001","status":"done","ticket":"legacy-runtime-learn-import","title":"Import legacy runtime learn into discussion lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["migration","tech-debt"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0001-prometeu-learn-index.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0002","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0002-historical-asset-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0003","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0003-historical-audio-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0004","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0004-historical-cartridge-boot-protocol-and-manifest-authority.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0005","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0005-historical-game-memcard-slots-surface-and-semantics.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0006","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0006-historical-gfx-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0007","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0007-historical-retired-fault-and-input-decisions.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0008","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0008-historical-vm-core-and-assets.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0009","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0009-mental-model-asset-management.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0010","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0010-mental-model-audio.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0011","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0011-mental-model-gfx.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0012","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0012-mental-model-input.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0013","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0013-mental-model-observability-and-debugging.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0014","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0014-mental-model-portability-and-cross-platform.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0015","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0015-mental-model-save-memory-and-memcard.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0016","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0016-mental-model-status-first-and-fault-thinking.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0017","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0017-mental-model-time-and-cycles.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0018","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0018-mental-model-touch.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]} {"type":"discussion","id":"DSC-0002","status":"done","ticket":"runtime-edge-test-plan","title":"Agenda - Runtime Edge Test Plan","created_at":"2026-03-27","updated_at":"2026-04-21","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0037","file":"discussion/lessons/DSC-0002-runtime-edge-test-plan/LSN-0037-domain-gates-must-be-owned-by-the-repository.md","status":"done","created_at":"2026-04-21","updated_at":"2026-04-21"}]} {"type":"discussion","id":"DSC-0003","status":"open","ticket":"packed-cartridge-loader-pmc","title":"Agenda - Packed Cartridge Loader PMC","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0002","file":"AGD-0002-packed-cartridge-loader-pmc.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0004","status":"in_progress","ticket":"system-run-cart","title":"Agenda - System Run Cart","created_at":"2026-03-27","updated_at":"2026-07-03","tags":[],"agendas":[{"id":"AGD-0003","file":"AGD-0003-system-run-cart.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-07-03"},{"id":"AGD-0045","file":"AGD-0045-systemos-library-root-home-game-launch.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0035","file":"DEC-0035-remove-userland-system-run-cart-and-preserve-direct-boot.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0003"},{"id":"DEC-0036","file":"DEC-0036-systemos-games-root-and-home-game-launch.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0045"}],"plans":[{"id":"PLN-0129","file":"PLN-0129-remove-userland-run-cart-abi-surface.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0035"]},{"id":"PLN-0130","file":"PLN-0130-remove-runtime-run-cart-stub-dispatch.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0035"]},{"id":"PLN-0131","file":"PLN-0131-preserve-direct-cartridge-boot-coverage.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0035"]},{"id":"PLN-0132","file":"PLN-0132-specify-games-root-home-launch-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]},{"id":"PLN-0133","file":"PLN-0133-implement-games-root-library-discovery.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]},{"id":"PLN-0134","file":"PLN-0134-wire-home-game-list-launch-flow.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]},{"id":"PLN-0135","file":"PLN-0135-validate-games-root-launch-end-to-end.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0004","status":"in_progress","ticket":"system-run-cart","title":"Agenda - System Run Cart","created_at":"2026-03-27","updated_at":"2026-07-03","tags":[],"agendas":[{"id":"AGD-0003","file":"AGD-0003-system-run-cart.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-07-03"},{"id":"AGD-0045","file":"AGD-0045-systemos-library-root-home-game-launch.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0035","file":"DEC-0035-remove-userland-system-run-cart-and-preserve-direct-boot.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0003"},{"id":"DEC-0036","file":"DEC-0036-systemos-games-root-and-home-game-launch.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0045"}],"plans":[{"id":"PLN-0129","file":"PLN-0129-remove-userland-run-cart-abi-surface.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0035"]},{"id":"PLN-0130","file":"PLN-0130-remove-runtime-run-cart-stub-dispatch.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0035"]},{"id":"PLN-0131","file":"PLN-0131-preserve-direct-cartridge-boot-coverage.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0035"]},{"id":"PLN-0132","file":"PLN-0132-specify-games-root-home-launch-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]},{"id":"PLN-0133","file":"PLN-0133-implement-games-root-library-discovery.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]},{"id":"PLN-0134","file":"PLN-0134-wire-home-game-list-launch-flow.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]},{"id":"PLN-0135","file":"PLN-0135-validate-games-root-launch-end-to-end.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]}],"lessons":[]} {"type":"discussion","id":"DSC-0005","status":"open","ticket":"system-fault-semantics-and-control-surface","title":"Agenda - System Fault Semantics and Control Surface","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0004","file":"AGD-0004-system-fault-semantics-and-control-surface.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0006","status":"open","ticket":"vm-owned-random-service","title":"Agenda - VM-Owned Random Service","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0005","file":"AGD-0005-vm-owned-random-service.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0007","status":"open","ticket":"app-home-filesystem-surface-and-semantics","title":"Agenda - App Home Filesystem Surface and Semantics","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0006","file":"AGD-0006-app-home-filesystem-surface-and-semantics.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} diff --git a/discussion/workflow/plans/PLN-0133-implement-games-root-library-discovery.md b/discussion/workflow/plans/PLN-0133-implement-games-root-library-discovery.md index 40bee690..65bc21f2 100644 --- a/discussion/workflow/plans/PLN-0133-implement-games-root-library-discovery.md +++ b/discussion/workflow/plans/PLN-0133-implement-games-root-library-discovery.md @@ -2,7 +2,7 @@ id: PLN-0133 ticket: system-run-cart title: Implement Games Root Library Discovery -status: open +status: done created: 2026-07-03 ref_decisions: [DEC-0036] tags: []