diff --git a/CHANGELOG.md b/CHANGELOG.md index e3dc839..8bfbca4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,10 @@ breaking changes may land in a minor release. ### Fixed +- **Windows installs now pull `psutil` automatically** — moved from the opt-in `non-linux` extra to a + platform-scoped core dependency (`sys_platform == 'win32'`), so the TUI liveness column no longer + shows every run as `?` on a stock install. macOS keeps the `non-linux` extra; Linux stays dep-free. + (#72, closes #71) - **`bmad-loop-setup` no longer deletes live core BMAD config or the installer manifest.** In a multi-module BMAD v6 project the setup scripts hardcoded `core` (and `--also-remove _config`) into their delete lists, destroying `_bmad/core/config.yaml`, per-module config, and the whole diff --git a/docs/game-engine-plugin-guide.md b/docs/game-engine-plugin-guide.md index ac01e69..aad0fc6 100644 --- a/docs/game-engine-plugin-guide.md +++ b/docs/game-engine-plugin-guide.md @@ -242,8 +242,9 @@ guards, by script: - **`unity_teardown.py` — process discovery.** Linux uses a zero-dependency `/proc` scan to find the worktree-bound Editor/MCP-server; non-Linux falls back - to the same scan over **`psutil`**, imported lazily from the optional `non-linux` - extra (`pip install 'bmad-loop[non-linux]'`) with a clear error if missing. The + to the same scan over **`psutil`**, imported lazily — a core dependency on + Windows, the optional `non-linux` extra on macOS (`pip install +'bmad-loop[non-linux]'`) — with a clear error if missing. The hard-kill uses `signal.SIGKILL` where present, degrading to `SIGTERM`/`taskkill` on Windows. Liveness uses `os.kill(pid, 0)` on POSIX but `psutil.pid_exists` on Windows (where `os.kill(pid, 0)` would _terminate_ the process). diff --git a/docs/plugin-authoring-guide.md b/docs/plugin-authoring-guide.md index 5c59905..7049ab5 100644 --- a/docs/plugin-authoring-guide.md +++ b/docs/plugin-authoring-guide.md @@ -634,8 +634,9 @@ seams instead of re-implementing OS primitives. Follow this discipline: script runs anywhere the core does. If you genuinely need a third-party package on one platform, make it an **optional extra** in `pyproject.toml` and **import it lazily** with a clear error if missing — never at module top level. The bundled - Unity plugin does exactly this for `psutil`: a `non-linux` extra it imports only on - non-Linux process _discovery_, so the dep-free Linux/WSL path never pulls it in. + Unity plugin does exactly this for `psutil`: imported only on non-Linux process + _discovery_ (a core dependency on Windows, the `non-linux` extra on macOS), so the + dep-free Linux/WSL path never pulls it in. - **Guard the primitives that have no seam.** Anything absent or differently-shaped on Windows that isn't behind a seam — `cp`/`--reflink`, symlinks, `/proc` scanning, diff --git a/pyproject.toml b/pyproject.toml index 2302cdc..9089eca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,17 +11,20 @@ license = "MIT" license-files = ["LICENSE"] authors = [{ name = "BMad Code, LLC" }] requires-python = ">=3.11" -dependencies = ["pyyaml>=6.0"] +dependencies = [ + "pyyaml>=6.0", + # win32 liveness needs psutil (no /proc; os.kill(pid,0) is destructive there); + # without it the TUI shows every run as `?`. Hard dep so no install path misses it. + "psutil>=7.2.2; sys_platform == 'win32'", +] [project.scripts] bmad-loop = "bmad_loop.cli:main" [project.optional-dependencies] tui = ["textual>=8.0,<9", "tomlkit>=0.13", "pyte>=0.8.2"] -# Process discovery on non-Linux (no /proc): the Unity plugin's teardown lazily -# imports psutil whenever it runs off Linux — macOS as well as a native-Windows -# backend. The core stays dep-free; install with `pip install bmad-loop[non-linux]` -# on those platforms. +# macOS opt-in for psutil (identity/force-kill niceties); win32 gets it as a core +# dep above, Linux stays dep-free. `pip install bmad-loop[non-linux]` on macOS. non-linux = ["psutil>=7.2.2"] [dependency-groups] diff --git a/src/bmad_loop/data/plugins/unity/unity_teardown.py b/src/bmad_loop/data/plugins/unity/unity_teardown.py index 80b8e26..0f06393 100644 --- a/src/bmad_loop/data/plugins/unity/unity_teardown.py +++ b/src/bmad_loop/data/plugins/unity/unity_teardown.py @@ -48,16 +48,17 @@ def _psutil(): - """Lazily import psutil (the optional ``non-linux`` extra), used only for - non-Linux process discovery (macOS and Windows). The dep-free core never imports - it; raise a clear, actionable error if it's missing on a platform that needs it.""" + """Lazily import psutil — a core dep on Windows, the ``non-linux`` extra on + macOS — used only for non-Linux process discovery (macOS and Windows). The + dep-free core never imports it; raise a clear, actionable error if it's missing + on a platform that needs it.""" try: import psutil # noqa: PLC0415 (intentional lazy import — keeps the core dep-free) except ImportError as exc: # pragma: no cover - exercised only off Linux raise RuntimeError( f"unity_teardown: process discovery on {sys.platform!r} needs psutil; " - "install the optional extra (pip install 'bmad-loop[non-linux]') or run " - "under Linux/WSL" + "on Windows reinstall bmad-loop (psutil is a required dependency there), " + "on macOS run `pip install 'bmad-loop[non-linux]'`" ) from exc return psutil diff --git a/src/bmad_loop/process_host.py b/src/bmad_loop/process_host.py index 04be02c..bf3a31e 100644 --- a/src/bmad_loop/process_host.py +++ b/src/bmad_loop/process_host.py @@ -219,17 +219,17 @@ def _proc_starttime(pid: int) -> float | None: def _psutil(): - """Lazily import psutil (the optional ``non-linux`` extra), used only for the - non-destructive Windows/macOS liveness + identity probes. The dep-free core - never imports it on Linux; raise a clear, actionable error if it's missing where - it's needed.""" + """Lazily import psutil — a core dep on Windows, the ``non-linux`` extra on + macOS — used only for the non-destructive Windows/macOS liveness + identity + probes. The dep-free core never imports it on Linux; raise a clear, actionable + error if it's missing where it's needed.""" try: import psutil # noqa: PLC0415 (intentional lazy import — keeps the core dep-free) except ImportError as exc: # pragma: no cover - exercised only off Linux raise ProcessHostError( f"process_host: pid operations on {sys.platform!r} need psutil; " - "install the optional extra (pip install 'bmad-loop[non-linux]') or run " - "under Linux/WSL" + "on Windows reinstall bmad-loop (psutil is a required dependency there), " + "on macOS run `pip install 'bmad-loop[non-linux]'`" ) from exc return psutil diff --git a/uv.lock b/uv.lock index 908e843..e3966c0 100644 --- a/uv.lock +++ b/uv.lock @@ -7,6 +7,7 @@ name = "bmad-loop" version = "0.8.1" source = { editable = "." } dependencies = [ + { name = "psutil", marker = "sys_platform == 'win32'" }, { name = "pyyaml" }, ] @@ -31,6 +32,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "psutil", marker = "sys_platform == 'win32'", specifier = ">=7.2.2" }, { name = "psutil", marker = "extra == 'non-linux'", specifier = ">=7.2.2" }, { name = "pyte", marker = "extra == 'tui'", specifier = ">=0.8.2" }, { name = "pyyaml", specifier = ">=6.0" },