Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
52c995c
fix(ledgrrr-settings): stop leaking test data; replace BurntToast/Pow…
promptexecutionerr Aug 30, 2026
3b543b1
fix(ledgerr-desktop-agent): stop leaking test registry keys; drop sta…
promptexecutionerr Aug 30, 2026
c7c3ed7
feat(notify): replace BurntToast/PowerShell dependency with native wi…
promptexecutionerr Aug 30, 2026
c78eb8f
refactor(tray): remove dead code, group notification toggles into a s…
promptexecutionerr Aug 30, 2026
19e0b5b
refactor(tray): make ShowWindow's action injectable; harden against a…
promptexecutionerr Aug 30, 2026
4851f9c
feat(tray): give host-tauri's Windows tray full parity with the old h…
promptexecutionerr Aug 30, 2026
b5b07cf
chore(tray): retire host-tray.exe as a build target
promptexecutionerr Aug 30, 2026
ed75630
chore(justfile): repoint host-tray recipes at host-tauri
promptexecutionerr Aug 30, 2026
3d4ee75
docs: port sysml-v2-parser spike findings from PR #187
promptexecutionerr Aug 30, 2026
da467a3
feat(status): add FoundryLocalStatus detection to LedgrrrStatus
promptexecutionerr Aug 30, 2026
da815ab
feat(foundry_install_plan): add winget install-assist plan-before-mut…
promptexecutionerr Aug 30, 2026
75ff9fa
feat(tauri): wire Foundry Local install-plan/action commands
promptexecutionerr Aug 30, 2026
8eaa7ac
docs(foundry_install_plan): fix misleading doc comment (Foundry Local…
promptexecutionerr Aug 30, 2026
2f31b6f
feat(lifecycle): add FoundryLocalLifecycle enum and status derivation
promptexecutionerr Aug 30, 2026
1cac1be
feat(lifecycle): export FoundryLocalLifecycle as an SCXML statechart
promptexecutionerr Aug 30, 2026
a10e402
fix(ledgerr-host): close registry-key test-isolation gap in 3 test files
elasticdotventures Aug 30, 2026
99460aa
docs: add Phase 4 and Phase 5 implementation plans
promptexecutionerr Aug 30, 2026
52eac09
fix: address ledgrrr#216 review findings
promptexecutionerr Aug 30, 2026
81d1cfb
Merge origin/main into feat/tray-client-server-port, resolve settings…
elasticdotventures Aug 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 8 additions & 5 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,21 @@ mcp-stop:
pkill -f ledgerr-mcp-server || true

# Build the Windows host binaries from WSL via PowerShell. This is the canonical
# path for `host-tray.exe` and `host-tauri.exe`.
# path for `host-tauri.exe` — `host-tray.exe` was retired (see
# docs/superpowers/specs/2026-08-29-tray-tauri-integration-design.md);
# host-tauri.exe's own tray now has full feature parity with it.
wsl2-pwsh-build:
powershell.exe -NoProfile -Command '$env:PATH = "C:\Users\wendy\.cargo\bin;C:\msys64\mingw64\bin;" + $env:PATH; Set-Location "D:\Projects\l3dg3rr"; cargo build -p ledgerr-host --bin host-tray --bin host-tauri'
powershell.exe -NoProfile -Command '$env:PATH = "C:\Users\wendy\.cargo\bin;C:\msys64\mingw64\bin;" + $env:PATH; Set-Location "D:\Projects\l3dg3rr"; cargo build -p ledgerr-host --bin host-tauri'

# Full local install: build host binaries, MCP server, and docs.
# Run this from WSL after any code change to get a fresh Windows build.
wsl2-pwsh-install:
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command '$env:PATH = "C:\Users\wendy\.cargo\bin;C:\msys64\mingw64\bin;" + $env:PATH; Set-Location "D:\Projects\l3dg3rr"; Write-Host "[1/3] Building ledgerr-host bins..."; cargo build -p ledgerr-host --bin host-tray --bin host-tauri; if ($LASTEXITCODE -ne 0) { throw "host build failed" }; Write-Host "[2/3] Building MCP server..."; cargo build -p ledgerr-mcp --bin ledgerr-mcp-server; if ($LASTEXITCODE -ne 0) { throw "MCP server build failed" }; Write-Host "[3/3] Build complete."; Write-Host ""; Write-Host "Installed binaries:"; Get-Item "target\debug\host-tray.exe","target\debug\host-tauri.exe","target\debug\ledgerr-mcp-server.exe" | ForEach-Object { " " + $_.FullName + " (" + [math]::Round($_.Length/1KB, 1) + " KB)" }'
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command '$env:PATH = "C:\Users\wendy\.cargo\bin;C:\msys64\mingw64\bin;" + $env:PATH; Set-Location "D:\Projects\l3dg3rr"; Write-Host "[1/3] Building ledgerr-host bins..."; cargo build -p ledgerr-host --bin host-tauri; if ($LASTEXITCODE -ne 0) { throw "host build failed" }; Write-Host "[2/3] Building MCP server..."; cargo build -p ledgerr-mcp --bin ledgerr-mcp-server; if ($LASTEXITCODE -ne 0) { throw "MCP server build failed" }; Write-Host "[3/3] Build complete."; Write-Host ""; Write-Host "Installed binaries:"; Get-Item "target\debug\host-tauri.exe","target\debug\ledgerr-mcp-server.exe" | ForEach-Object { " " + $_.FullName + " (" + [math]::Round($_.Length/1KB, 1) + " KB)" }'

# Rebuild and launch the tray host on Windows.
# Rebuild and launch the tray host on Windows (host-tauri.exe — the retired
# standalone host-tray.exe's tray is now built into it).
wsl2-pwsh-run-tray:
powershell.exe -NoProfile -Command '$env:PATH = "C:\Users\wendy\.cargo\bin;C:\msys64\mingw64\bin;" + $env:PATH; Set-Location "D:\Projects\l3dg3rr"; cargo build -p ledgerr-host --bin host-tray | Out-Null; Get-Process host-tray -ErrorAction SilentlyContinue | Stop-Process -Force; Start-Sleep -Milliseconds 250; Start-Process -FilePath "D:\Projects\l3dg3rr\target\debug\host-tray.exe" -WorkingDirectory "D:\Projects\l3dg3rr"'
powershell.exe -NoProfile -Command '$env:PATH = "C:\Users\wendy\.cargo\bin;C:\msys64\mingw64\bin;" + $env:PATH; Set-Location "D:\Projects\l3dg3rr"; cargo build -p ledgerr-host --bin host-tauri | Out-Null; Get-Process host-tauri -ErrorAction SilentlyContinue | Stop-Process -Force; Start-Sleep -Milliseconds 250; Start-Process -FilePath "D:\Projects\l3dg3rr\target\debug\host-tauri.exe" -WorkingDirectory "D:\Projects\l3dg3rr"'

# Rebuild and launch the Tauri desktop host on Windows (no local LLM).
# The internal endpoint falls back to the deterministic Phi-4 stub.
Expand Down
1 change: 1 addition & 0 deletions crates/ledgerr-desktop-agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ blake3 = { workspace = true }
getrandom = "0.3"
ledgrrr-settings = { path = "../ledgrrr-settings" }
schemars = { version = "0.8", features = ["derive"] }
scxml = "=0.2.2"
serde = { workspace = true }
serde_json = { workspace = true }
sysinfo = { version = "0.33", default-features = false, features = ["system"] }
Expand Down
24 changes: 24 additions & 0 deletions crates/ledgerr-desktop-agent/src/foundry_install_plan.rs
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,28 @@ mod tests {
let args = FoundryInstallActionArgs::default();
assert!(!args.approved);
}

// Compiled ONLY for non-Windows targets — deliberately, not an
// oversight. On a Windows build, `install_plan()`'s `executable_now`
// is always `true` (driven by `cfg!(windows)`), so `invoke(approved:
// true)` would reach the real `Command::new("powershell.exe")…
// spawn()` call and genuinely attempt a winget install as a side
// effect of running the test suite. Gating this test out on Windows
// is what makes it safe to have at all; it exercises the `!plan.
// executable_now` branch on any non-Windows CI target instead; the
// pre-existing `invoke_without_approval_never_launches` test above
// already covers the approval gate safely on every target, Windows
// included, since it never sets `approved: true`.
#[cfg(not(windows))]
#[test]
fn invoke_reports_blocked_on_non_windows_even_when_approved() {
let result = invoke(FoundryInstallActionArgs { approved: true });
assert!(!result.ok);
assert!(!result.launched);
assert_eq!(
result.message,
"Windows Foundry Local can only be installed via winget on Windows."
);
assert!(!result.plan.executable_now);
}
}
1 change: 1 addition & 0 deletions crates/ledgerr-desktop-agent/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
pub mod contract;
pub mod foundry_install_plan;
pub mod install_plan;
pub mod lifecycle;
pub mod office_artifact;
pub mod playbook;
pub mod render;
Expand Down
198 changes: 198 additions & 0 deletions crates/ledgerr-desktop-agent/src/lifecycle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
//! Windows Foundry Local's install/service lifecycle, and its export as a
//! W3C SCXML statechart (`foundry_lifecycle_to_statechart`, added in a
//! later step of this module) — following the exact pattern
//! `ufo-types::statechart::ooda_phases_to_statechart` already establishes
//! for this ecosystem's SysML-v2/statechart spine
//! (`elasticdotventures/_b00t_#1177`).
//!
//! Deliberate divergence from an event-dispatched state machine (e.g.
//! `OodaStateMachine::dispatch`): there is no live "engine" driving
//! transitions here. [`current_state`] is a pure derivation from a
//! [`crate::status::FoundryLocalStatus`] snapshot, computed fresh every
//! time status is collected — that function IS this phase's real source of
//! truth. The SCXML export models the *shape* of how that derived state
//! can move over time (useful for visualization/governance), not a
//! literal transition log; this mirrors how `ooda_phases_to_statechart`'s
//! own doc comment flags its `Cancel`-as-`Final`-state divergence from the
//! live OODA dispatcher.

use crate::status::FoundryLocalStatus;
use scxml::model::{State, Statechart, Transition};

/// Windows Foundry Local's observed lifecycle state.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FoundryLocalLifecycle {
/// The `foundry` CLI is not on PATH.
NotInstalled,
/// The CLI is present but its service is not reporting as running.
InstalledStopped,
/// The CLI is present and its service reports as running.
InstalledRunning,
}

/// Derives the current lifecycle state from a real status snapshot. This
/// is the authoritative logic — the SCXML export (see
/// `foundry_lifecycle_to_statechart`, added later in this file) documents
/// this same shape for visualization, it does not replace this function.
pub fn current_state(status: &FoundryLocalStatus) -> FoundryLocalLifecycle {
if !status.cli_found {
FoundryLocalLifecycle::NotInstalled
} else if status.service_running {
FoundryLocalLifecycle::InstalledRunning
} else {
FoundryLocalLifecycle::InstalledStopped
}
}

pub(crate) fn state_id(state: FoundryLocalLifecycle) -> &'static str {
match state {
FoundryLocalLifecycle::NotInstalled => "not_installed",
FoundryLocalLifecycle::InstalledStopped => "installed_stopped",
FoundryLocalLifecycle::InstalledRunning => "installed_running",
}
}

/// Exports [`FoundryLocalLifecycle`]'s state shape as a W3C SCXML
/// statechart, following `ufo-types::statechart::ooda_phases_to_statechart`'s
/// exact pattern. See this module's doc comment for why these transitions
/// describe the *shape* of the lifecycle rather than a literally-dispatched
/// event stream.
pub fn foundry_lifecycle_to_statechart() -> Statechart {
let mut not_installed = State::atomic(state_id(FoundryLocalLifecycle::NotInstalled));
not_installed.transitions.push(Transition::new(
"InstallSucceeded",
state_id(FoundryLocalLifecycle::InstalledStopped),
));

let mut installed_stopped = State::atomic(state_id(FoundryLocalLifecycle::InstalledStopped));
installed_stopped.transitions.push(Transition::new(
"ServiceStarted",
state_id(FoundryLocalLifecycle::InstalledRunning),
));

let mut installed_running = State::atomic(state_id(FoundryLocalLifecycle::InstalledRunning));
installed_running.transitions.push(Transition::new(
"ServiceStopped",
state_id(FoundryLocalLifecycle::InstalledStopped),
));

Statechart::new(
state_id(FoundryLocalLifecycle::NotInstalled),
vec![not_installed, installed_stopped, installed_running],
)
.with_name("foundry_local_lifecycle")
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn cli_not_found_means_not_installed_regardless_of_service_flag() {
let status = FoundryLocalStatus {
cli_found: false,
service_running: true, // must be ignored — cli_found gates everything
};
assert_eq!(current_state(&status), FoundryLocalLifecycle::NotInstalled);
}

#[test]
fn cli_found_and_service_not_running_is_installed_stopped() {
let status = FoundryLocalStatus {
cli_found: true,
service_running: false,
};
assert_eq!(current_state(&status), FoundryLocalLifecycle::InstalledStopped);
}

#[test]
fn cli_found_and_service_running_is_installed_running() {
let status = FoundryLocalStatus {
cli_found: true,
service_running: true,
};
assert_eq!(current_state(&status), FoundryLocalLifecycle::InstalledRunning);
}

#[test]
fn state_ids_are_distinct_and_stable() {
assert_eq!(state_id(FoundryLocalLifecycle::NotInstalled), "not_installed");
assert_eq!(state_id(FoundryLocalLifecycle::InstalledStopped), "installed_stopped");
assert_eq!(state_id(FoundryLocalLifecycle::InstalledRunning), "installed_running");
}

use scxml::export::xml::to_xml;
use scxml::model::StateKind;
use scxml::parse_xml;
use scxml::validate;

#[test]
fn statechart_has_exactly_the_three_lifecycle_states() {
let chart = foundry_lifecycle_to_statechart();
assert_eq!(chart.states.len(), 3);
for state in [
FoundryLocalLifecycle::NotInstalled,
FoundryLocalLifecycle::InstalledStopped,
FoundryLocalLifecycle::InstalledRunning,
] {
let found = chart.find_state(state_id(state));
assert!(found.is_some(), "missing state {}", state_id(state));
assert_eq!(found.unwrap().kind, StateKind::Atomic);
}
}

#[test]
fn not_installed_transitions_to_installed_stopped_on_install_succeeded() {
let chart = foundry_lifecycle_to_statechart();
let not_installed = chart
.find_state(state_id(FoundryLocalLifecycle::NotInstalled))
.unwrap();
let transition = not_installed
.transitions
.iter()
.find(|t| t.event.as_deref() == Some("InstallSucceeded"))
.expect("NotInstalled must accept InstallSucceeded");
assert_eq!(
transition.targets,
vec![state_id(FoundryLocalLifecycle::InstalledStopped)]
);
}

#[test]
fn installed_stopped_and_running_transition_to_each_other() {
let chart = foundry_lifecycle_to_statechart();
let stopped = chart
.find_state(state_id(FoundryLocalLifecycle::InstalledStopped))
.unwrap();
let start = stopped
.transitions
.iter()
.find(|t| t.event.as_deref() == Some("ServiceStarted"))
.expect("InstalledStopped must accept ServiceStarted");
assert_eq!(start.targets, vec![state_id(FoundryLocalLifecycle::InstalledRunning)]);

let running = chart
.find_state(state_id(FoundryLocalLifecycle::InstalledRunning))
.unwrap();
let stop = running
.transitions
.iter()
.find(|t| t.event.as_deref() == Some("ServiceStopped"))
.expect("InstalledRunning must accept ServiceStopped");
assert_eq!(stop.targets, vec![state_id(FoundryLocalLifecycle::InstalledStopped)]);
}

#[test]
fn statechart_passes_scxml_structural_validation() {
let chart = foundry_lifecycle_to_statechart();
validate(&chart).expect("exported statechart should be structurally valid SCXML");
}

#[test]
fn statechart_round_trips_through_xml_export_and_parse() {
let chart = foundry_lifecycle_to_statechart();
let xml = to_xml(&chart);
let parsed = parse_xml(&xml).expect("exported XML must parse back as valid SCXML");
assert_eq!(parsed, chart);
}
}
2 changes: 1 addition & 1 deletion crates/ledgerr-desktop-agent/src/service_control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ pub fn open_tray() -> ServiceControlResult {
ok: false,
pid: None,
message:
"no tray binary (host-tray, ledgerr-tauri) found next to controller or on PATH"
"no tray binary (host-tauri) found next to controller or on PATH"
.to_string(),
},
}
Expand Down
11 changes: 10 additions & 1 deletion crates/ledgerr-desktop-agent/src/settings_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,18 @@ pub fn accept_once(listener: &TcpListener, store: &SettingsStore) {
mod tests {
use super::*;

/// `SettingsStore::new`'s registry backend (on Windows) ignores its
/// `path` argument and always targets the one fixed production key —
/// correct for real callers, but it would make every test here share
/// one mutable global registry key. Use an explicit `JsonFileBackend`
/// over a tempdir instead, for genuine per-test isolation.
fn store_with_defaults() -> SettingsStore {
let dir = tempfile::tempdir().unwrap();
SettingsStore::new(dir.path().join("settings.json"))
let path = dir.path().join("settings.json");
SettingsStore::with_backend(
path.clone(),
Box::new(ledgrrr_settings::backend::JsonFileBackend::new(path)),
)
}

#[test]
Expand Down
2 changes: 0 additions & 2 deletions crates/ledgerr-desktop-agent/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,6 @@ const TRAY_CANDIDATES: &[&str] = &[
"ledgrrr-tray.exe",
"host-tauri.exe",
"host-tauri",
"host-tray.exe",
"host-tray",
];

/// Finds a tray binary next to this controller's own executable, then on
Expand Down
4 changes: 0 additions & 4 deletions crates/ledgerr-host/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ edition.workspace = true
license.workspace = true
default-run = "host-tauri"

[[bin]]
name = "host-tray"
path = "src/bin/host-tray.rs"

[[bin]]
name = "host-tauri"
path = "src/bin/tauri/main.rs"
Expand Down
13 changes: 0 additions & 13 deletions crates/ledgerr-host/src/bin/host-tray.rs

This file was deleted.

20 changes: 20 additions & 0 deletions crates/ledgerr-host/src/bin/legacy/host-tray.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// DEPRECATED, REFERENCE ONLY — not part of the build (no [[bin]] entry,
// and this directory is outside Cargo's src/bin/*.rs auto-discovery).
// Superseded by host-tauri.exe, whose Windows tray now uses the same
// tray::runtime::run() this binary used. Kept for reference only; may
// bit-rot as the rest of the crate changes (e.g. run()'s signature grew a
// second `show_window` parameter after this file stopped compiling).
//
#![cfg_attr(windows, windows_subsystem = "windows")]

#[cfg(windows)]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = ledgerr_host::settings_client::SettingsClient::new();
ledgerr_host::tray::runtime::run(client)
}

#[cfg(not(windows))]
fn main() {
eprintln!("host-tray is currently supported on Windows builds only");
std::process::exit(1);
}
Loading
Loading