Skip to content

fix(windows): stop os.fchmod crash, correct Desktop config path, add Windows CI#122

Merged
hyoshi merged 4 commits into
mainfrom
fix/windows-fs-compat
May 19, 2026
Merged

fix(windows): stop os.fchmod crash, correct Desktop config path, add Windows CI#122
hyoshi merged 4 commits into
mainfrom
fix/windows-fs-compat

Conversation

@hyoshi
Copy link
Copy Markdown
Collaborator

@hyoshi hyoshi commented May 19, 2026

Problem

mureo crashed on Windows: os.fchmod is Unix-only (AttributeError) in every credential/config write path. The configure WebUI also used the wrong Claude Desktop config location on Windows. No Windows CI existed, so this was invisible.

Fix

  • New mureo/fsutil.pysecure_fchmod / secure_chmod: owner-only 0o600 on POSIX (byte-identical to before — no Linux/macOS regression), best-effort never-raising no-op on Windows. NTFS confidentiality relies on the %USERPROFILE% profile ACL (documented best-effort).
  • Replaced all 6 os.fchmod / os.chmod(...,0o600) sites: auth._save_meta_token, providers.config_writer, cli.settings_remove, auth_setup, web._helpers, mcp.plugin_audit. Dropped now-unused imports.
  • host_paths: added Windows branch — Claude Desktop config is %APPDATA%\Claude\claude_desktop_config.json. macOS unchanged; Linux keeps the Code-style fallback (Claude Desktop has no Linux build).
  • CI: new test-windows (windows-latest, Py 3.12) job — the real-Windows verification, since the fixes were otherwise only simulated on Linux.

Verification

  • Simulation tests: test_fsutil.py removes os.fchmod (emulates Windows); test_web_host_paths.py patches platform.system()='Windows'.
  • Clean-env full suite: 3262 passed; ruff/black clean; two code-reviewer passes APPROVE (no CRITICAL/HIGH/MEDIUM).
  • This PR's test-windows job is the first real-Windows run.

Out of scope (not crashes)

  • mureo install-desktop CLI is still macOS-only by explicit design (needs a Windows launcher — separate feature; it errors gracefully today).
  • NTFS 0o600 is best-effort (documented).

hyoshi added 4 commits May 19, 2026 13:22
mureo crashed on Windows: os.fchmod is Unix-only (AttributeError) in
every credential/config write path. And the configure WebUI used the
wrong Claude Desktop config location on Windows.

- New mureo/fsutil.py: secure_fchmod / secure_chmod — apply owner-only
  0o600 on POSIX (byte-identical to before, no Linux/macOS regression),
  best-effort never-raising no-op on Windows. NTFS confidentiality
  relies on the %USERPROFILE% profile ACL (documented best-effort, not
  a silent regression).
- Replace all 6 os.fchmod / os.chmod(...,0o600) sites: auth
  (_save_meta_token), providers.config_writer, cli.settings_remove,
  auth_setup, web._helpers, mcp.plugin_audit. Drop now-unused
  imports (contextlib in plugin_audit, os in auth_setup).
- host_paths: add Windows branch — Claude Desktop config is
  %APPDATA%\Claude\claude_desktop_config.json
  (~/AppData/Roaming/...). macOS unchanged; Linux keeps the
  Code-style fallback (Claude Desktop has no Linux build).

Verified by simulation (no Windows box): test_fsutil.py removes
os.fchmod to emulate Windows; test_web_host_paths.py patches
platform.system()='Windows'. Clean-env full suite 3262 passed;
ruff/black clean; two code-reviewer passes APPROVE (no CRIT/HIGH/MED).

Out of scope (not crashes): mureo install-desktop is still macOS-only
by explicit design (needs a Windows launcher); no Windows CI yet
(added next).
No Windows dev machines exist; the os.fchmod crash fix and the
%APPDATA% Claude Desktop path are otherwise only simulated on Linux.
Single Python 3.12, no coverage (ubuntu job owns that) — a compat
tripwire that runs the full test suite on real Windows.
The new test-windows job surfaced 37 real-Windows failures. Fixes:

Production (1):
- auth_setup: simple_term_menu imports Unix-only termios and raises
  NotImplementedError (NOT ImportError) on Windows, so the existing
  plain number-input fallback was unreachable (≈14 tests crashed with
  'Windows is currently not supported'). Widen both
  'except ImportError' → 'except (ImportError, NotImplementedError)'.
  Also degrades gracefully in non-terminal envs (CI/pipe/PyCharm)
  instead of crashing.

Test portability (POSIX-only assertions made platform-aware; product
behaviour unchanged, POSIX byte-identical):
- core/skills/test_models: hardcoded Path('/tmp/...') (not absolute on
  Windows) → OS-anchored _abs() helper; relative negative test kept.
- test_fsutil / test_auth_setup / test_auth_setup_meta: skipif win32
  on the 0o600-mode assertions (NTFS perms documented best-effort;
  never-raise still tested on Windows).
- test_desktop_installer: skipif win32 on 3 POSIX .sh-wrapper/exec-bit
  tests (install-desktop is macOS-only by design).
- test_web_handlers: spoofed-Host tests accept ConnectionError on
  win32 (server closes socket before 403 → WinError 10053); POSIX
  still strictly requires HTTP 403.

code-reviewer APPROVE (no CRIT/HIGH). POSIX suites green; tests/ ruff
not CI-gated, mureo/ ruff+black clean.
…h shim

test-windows (2nd run) left 20 failures in two clusters:

- tests/core/skills/test_matcher.py: the shared SkillEntry builder
  hardcoded Path('/tmp/skills/{name}/SKILL.md') (not absolute on
  Windows). Same OS-anchored fix already approved for test_models.py.
- tests/test_auth_setup.py: 12 tests patch
  'simple_term_menu.TerminalMenu'; unittest.mock.patch must import
  simple_term_menu to resolve the target, which raises
  NotImplementedError on Windows (Unix-only termios) BEFORE the
  production widened except runs. Add a win32-only sys.modules stub
  (only when the real package can't import) so the patch target
  resolves and the existing tests run on Windows as on POSIX. Pure
  test-harness shim: real Windows product has no simple_term_menu and
  uses the numeric fallback (the per-test patch overrides the stub).

POSIX byte-identical (whole shim under sys.platform=='win32').
code-reviewer APPROVE (no CRIT/HIGH/MED). POSIX suites green;
mureo/ ruff+black clean (no production change this round).
@hyoshi hyoshi merged commit 4651cae into main May 19, 2026
9 checks passed
@hyoshi hyoshi deleted the fix/windows-fs-compat branch May 19, 2026 05:16
@hyoshi hyoshi mentioned this pull request May 19, 2026
hyoshi added a commit that referenced this pull request May 19, 2026
Post-merge main CI (run 26077780980) flaked here: the test sampled
stop_event.is_set() immediately after the /api/shutdown response,
racing the server thread that sets the event (passed on PR #122 and
PR #123 with identical code). stop_event is a threading.Event — wait
for it with a bounded 5s timeout instead of sampling once. Same
property asserted; still fails (with a clear message) if /api/shutdown
never triggers stop. Bundled into the v0.9.3 release branch so the
released main is robust against this recurring flake.
hyoshi added a commit that referenced this pull request May 19, 2026
* chore(release): v0.9.3 — Windows compatibility (#122)

* fix(test): deflake test_api_shutdown_route_triggers_stop

Post-merge main CI (run 26077780980) flaked here: the test sampled
stop_event.is_set() immediately after the /api/shutdown response,
racing the server thread that sets the event (passed on PR #122 and
PR #123 with identical code). stop_event is a threading.Event — wait
for it with a bounded 5s timeout instead of sampling once. Same
property asserted; still fails (with a clear message) if /api/shutdown
never triggers stop. Bundled into the v0.9.3 release branch so the
released main is robust against this recurring flake.
hyoshi added a commit that referenced this pull request May 21, 2026
…, and throttle backends (#125)

* feat(core): add extension protocols for credentials, state, knowledge, and throttle backends

Introduce five new Protocol/dataclass types under mureo.core so call
sites can later accept pluggable backends (in-memory for tests,
alternate stores such as keychains, SQLite, Redis, or hosted file
APIs) without touching the legacy file-based defaults.

This commit is purely additive — no existing module imports the new
types yet, so behaviour is unchanged for every current caller. Follow-up
commits will wire concrete default implementations and refactor the
existing helpers in mureo.auth, mureo.context, and mureo.mcp to consume
the Protocols.

* SecretStore — credential round-trip with idempotent delete
* StateStore — STATE.json / STRATEGY.md / action-log persistence
* KnowledgeStore — two-tier /learn knowledge with operator + workspace
* ThrottleStore — async per-key rate-limit gate matching today's
  mureo.throttle.Throttler shape
* RuntimeContext — frozen aggregate, validates non-empty workspace_id

Tests: 27 new structural-contract tests in tests/core/; all 335 tests
under tests/core/ pass; no existing files modified.

* feat(core): add filesystem-backed default implementations for the four extension protocols

Add the in-process default implementations of the Protocols introduced
in c9713d2:

* FilesystemSecretStore — JSON file under ~/.mureo/credentials.json;
  atomic write via tempfile + os.replace, secure_fchmod for 0o600 on
  POSIX, ensure_ascii=False to match the legacy file format
* FilesystemStateStore — composes the existing helpers in
  mureo.context.state and mureo.context.strategy so behaviour matches
  today's CWD-relative call sites verbatim
* FilesystemKnowledgeStore — Markdown files under
  ~/.claude/skills/_mureo-pro-diagnosis/; seeds the same frontmatter
  scaffold that skills/learn/SKILL.md uses today (drift caught by a
  regression test)
* ProcessLocalThrottleStore — per-key dict of mureo.throttle.Throttler;
  register(key, config) pre-installs custom buckets matching the
  MCP server's _PLUGIN_TOOL_THROTTLERS pattern

Still purely additive. No existing module imports the new classes yet;
behaviour is unchanged for every current caller. The follow-up commit
will add the RuntimeContext default factory and re-export the public
surface from mureo.core.

Tests: 37 new tests in tests/core/ (35 implementation + scaffold drift
guard + register custom config); all 372 tests/core/ tests pass; 124
related existing tests (auth/state/strategy/throttle/mcp_context/
fsutil) still pass.

* feat(core): add default_runtime_context factory and export public surface

* mureo.core.runtime_context.default_runtime_context() wires the four
  file-backed defaults from c9713d2/3d57367 into a single RuntimeContext;
  every parameter is keyword-only and optional, falling back to the
  legacy file locations callers already see today.
* DEFAULT_WORKSPACE_ID = "default" promoted to a module-level constant
  and exported so consumers compare against a named sentinel rather
  than an inline literal.
* mureo.core.__init__.py grows from empty to a curated public surface:
  10 Protocols / defaults / RuntimeContext / factory / sentinel, locked
  by __all__ and a contract test that pins both the positive surface
  and the negative (providers/skills sub-packages are NOT re-exported).

Lint cleanup bundled in: TYPE_CHECKING-only imports for Path,
ThrottleConfig, and the three context.models dataclasses; black
formatting for runtime_context.py and knowledge_store.py; HOME-env
monkeypatching replaced with direct Path.home patching across three
tests so the new Windows CI lane (#122) stays green.

Tests: 9 new tests (5 factory + 4 public-API); all 382 tests/core pass;
related existing tests (auth/state/strategy/throttle/fsutil/cli_providers
= 136) still pass; ruff and black clean on mureo/.

* feat(core): add entry-point resolver for pluggable RuntimeContext factories

Introduce mureo.core.runtime_context.get_runtime_context() — the
integration point alternate backends use to inject a custom
RuntimeContext without changing call sites. A third-party distribution
registers a zero-arg callable under the "mureo.runtime_context_factory"
entry-point group; the resolver picks it up automatically.

Resolution rules:

* 0 entry points: returns default_runtime_context() (today's behaviour
  for every existing caller).
* 1 entry point: loads and calls it; the returned RuntimeContext is
  cached for the process lifetime.
* >1 entry points: raises RuntimeContextFactoryError. Unlike
  mureo.core.providers.registry (additive, first-wins+warning), the
  RuntimeContext is a process singleton — silently picking one would
  hide a packaging bug.

Only successfully-constructed contexts are cached; a broken plugin
re-runs ep.load() per call so a fix-in-place is visible without a
process restart.

Public additions to mureo.core: RUNTIME_CONTEXT_FACTORY_ENTRY_POINT_GROUP,
RuntimeContextFactoryError, get_runtime_context, reset_runtime_context.

Still purely additive: no consumer calls get_runtime_context() yet.
Wiring of mureo.auth / mureo.context / mureo.mcp follows in
subsequent commits.

Tests: 9 new tests in tests/core/test_runtime_resolver.py covering
0/1/multi entry points, cache hit/reset, type validation, factory
exception wrapping, and public-API export. 391 tests/core total pass;
ruff and black clean on mureo/.

* refactor(auth): route load_*_credentials through the SecretStore Protocol

`load_google_ads_credentials` and `load_meta_ads_credentials` now read
their credential dict from a `SecretStore` instead of calling the
file-direct `load_credentials` helper. Resolution rules:

* `path=` given → one-shot `FilesystemSecretStore(path=path)`. Bypasses
  the process-wide `RuntimeContext` so the long-standing per-test
  credentials file pattern (`tests/test_auth.py`) keeps working
  unchanged and stays isolated from any installed alternate backend.
* `path=None` → `get_runtime_context().secret_store`. In OSS today
  that is the file-backed default reading `~/.mureo/credentials.json`
  — byte-for-byte identical to the prior behaviour for every existing
  caller. When an alternate backend registers a
  `mureo.runtime_context_factory` entry point (added in c96e99b),
  the credential read transparently routes through it.

`load_credentials` is intentionally kept unchanged: it is part of the
public surface and may have out-of-tree callers; in-repo callers are
limited to `tests/test_auth.py` and the two helpers above. Removing
it would be a separate deprecation.

Behaviour change worth calling out: the prior code distinguished
"section missing" (returns `None` → falls to env) from "section
present but invalid". The Protocol contract returns `{}` for both,
so the helpers now also skip empty dicts (`isinstance(..., dict)
and section`) to preserve the env-var fallback path. Verified by
the existing env-fallback tests in `tests/test_auth.py`.

Tests: 4 new tests in `tests/test_auth_runtime_context.py` covering
runtime-context routing, explicit-path bypass, and env-fallback when
the resolved store is empty. All 26 existing `tests/test_auth.py`
tests pass without modification; `tests/core/` 391 tests pass; ruff
and black clean on `mureo/`.

Test-isolation note: the new test file patches `_cached_context`
directly via `monkeypatch.setattr` and uses an autouse fixture to
`reset_runtime_context()` before and after each test, so the resolver
cache cannot bleed state into other test files.

* refactor(mcp): route mureo_context handlers through StateStore workspace

The five `mureo_state_*` / `mureo_strategy_*` MCP handlers now resolve
paths against the active `StateStore`'s workspace rather than the raw
process CWD. With the default file-backed runtime the two are equal,
so behaviour is unchanged for every existing caller. When an alternate
backend registers via the `mureo.runtime_context_factory` entry-point
group (added in c96e99b), the handlers read and write under that
backend's workspace transparently.

What changed in `mureo/mcp/_handlers_mureo_context.py`:

* `_resolve_path` now takes an optional `store_attr` ("state_path" or
  "strategy_path"). When the `path` argument is missing or empty, it
  returns the backend-owned path if exposed, otherwise
  `workspace / default_name`. When `path` is supplied, it resolves
  relative to the workspace (not raw CWD) and re-runs the
  symlink-following boundary check against that workspace.
* The error message becomes "Refusing to read/write outside workspace"
  (was "outside cwd") so the security boundary is correctly named.
* Removed the now-unused `_opt` import; added
  `get_runtime_context` import.

Empty-string `path` argument behaviour: the old `_opt`-based code
dispatched empty strings to `Path(".")` which silently meant CWD. The
new code treats `None` and `""` identically as "no override", which
is safer and matches what callers actually want.

Test infrastructure:

* `tests/test_mcp_tools_mureo_context.py` gains an autouse fixture
  `_clear_runtime_context_cache` that calls `reset_runtime_context()`
  before and after every test, so the per-test `monkeypatch.chdir` is
  observed by `_resolve_path` instead of a stale `FilesystemStateStore`
  cached by an earlier test.
* `test_path_argument_refuses_traversal` is updated to match the new
  error message and includes a docstring explaining why the workspace
  equals CWD in the default configuration.
* New `test_default_path_follows_runtime_context_workspace` injects a
  workspace different from CWD via `_cached_context` and asserts the
  action-log append lands in the injected workspace (and NOT in CWD).

Tests: 14 in `tests/test_mcp_tools_mureo_context.py` pass (13 existing
+ 1 new); `tests/core/` 391 pass; `tests/test_auth.py` +
`tests/test_auth_runtime_context.py` 30 pass. ruff and black clean
on `mureo/`.

* refactor(mcp): route rollback handlers through StateStore workspace

`mureo/mcp/_handlers_rollback.py`'s `_resolve_state_file` now resolves
the user-supplied `state_file` argument against the active StateStore's
workspace rather than the raw process CWD. Mirrors the refactor
applied to `_handlers_mureo_context.py` in 4f5fcd7.

With the default file-backed runtime the two are equal, so behaviour
is unchanged for every existing caller. When an alternate backend
registers via the `mureo.runtime_context_factory` entry-point group
(added in c96e99b), the rollback surface reads STATE.json under the
backend's workspace transparently.

Differences from the mureo_context refactor:
* Argument name is `state_file` (not `path`) — kept for API stability.
* The state-path attribute is hardcoded to `state_path` (not
  parameterised through `store_attr`) because rollback only reads
  STATE.json, never STRATEGY.md.
* The error message becomes "inside the active workspace" (was
  "inside the current working directory").
* Backend-owned `state_path` is `.resolve()`d so a relative path from
  a custom backend cannot trip `execute_rollback`'s CWD-relative
  interpretation downstream.

Test infrastructure: `tests/test_mcp_tools_rollback.py` gains the same
autouse `_clear_runtime_context_cache` fixture introduced for
mureo_context (`4f5fcd7`); the existing `sandboxed_cwd` fixture is
re-documented to note the autouse runs first. `test_path_traversal_refused`
asserts the updated error message and gains a docstring explaining
why workspace == CWD in the default configuration.

Tests: 9 in `tests/test_mcp_tools_rollback.py` pass; 23 in
`tests/test_mcp_tools_rollback.py` + `tests/test_mcp_tools_mureo_context.py`
combined pass (no cache cross-contamination). 440 tests across
`tests/core/`, `tests/test_auth*.py`, and the two refactored handler
files pass; ruff and black clean on `mureo/`.

* refactor(mcp,cli): route analysis handler and rollback CLI through StateStore workspace

Two more consumers of STATE.json path resolution now honour the active
StateStore's workspace rather than the raw process CWD. With the
default file-backed runtime the two are equal, so behaviour is
unchanged for every existing caller. An alternate backend registered
via the `mureo.runtime_context_factory` entry-point group (added in
c96e99b) redirects both surfaces transparently.

mureo/mcp/_handlers_analysis.py
* `_resolve_state_file` mirrors the workspace-based pattern applied
  to the rollback handler in 931d93a, with one important addition:
  the long-standing symlink refusal is preserved (a symlink anywhere
  in the path chain inside the workspace is rejected because the
  analysis surface returns derived metrics that an attacker could
  influence by swapping the symlink target mid-call).
* Error message becomes "inside the active workspace" (was "inside
  the current working directory"); module docstring updated to match.
* Backend-owned `state_path` is `.resolve()`d so a relative path from
  a custom backend cannot trip downstream callers.

mureo/cli/rollback_cmd.py
* `--state-file` Typer option default flips from `Path("STATE.json")`
  (CWD-relative at command-invocation time) to `None`. A new helper
  `_resolve_default_state_file()` resolves the missing value at
  command time through `get_runtime_context().state_store`.
* `rollback_list` and `rollback_show` accept `Path | None` and resolve
  the default at the top of the body. Callers that pass `--state-file`
  explicitly see no behaviour change.

Test infrastructure:
* `tests/test_mcp_tools_analysis.py` gains the autouse cache-reset
  fixture and asserts the updated workspace-aware error message.
* `tests/test_cli_rollback.py` gains the autouse cache-reset fixture
  and a new `TestStateFileDefault` class with two cases that cover
  the workspace-default path:
  - `test_default_resolves_to_cwd_workspace`: omitted `--state-file`
    + CWD STATE.json → entry found
  - `test_default_resolves_via_injected_runtime_context`: workspace ≠
    CWD via `_cached_context` injection → CLI reads workspace
    STATE.json, not CWD STATE.json
* Stale module docstrings in handler and test fixed to reference
  workspace, not CWD.

Tests: 464 pass across `tests/test_cli_rollback.py` (14),
`tests/test_mcp_tools_analysis.py` (13), `tests/test_mcp_tools_rollback.py`
(9), `tests/test_mcp_tools_mureo_context.py` (14), `tests/test_auth*`
(30), and `tests/core/` (391). ruff and black clean on `mureo/`.

* refactor(mcp): route plugin dispatch throttle through ThrottleStore

The plugin-dispatch branch in `handle_call_tool` now acquires its
throttle slot via `get_runtime_context().throttle_store` instead of
the module-level `_PLUGIN_TOOL_THROTTLERS` / `_PLUGIN_THROTTLER`
attributes directly. An alternate ThrottleStore backend registered
via `mureo.runtime_context_factory` can intercept every plugin call
without each handler having to change.

Default file-backed runtime: byte-equivalent semantics.

* New helper `_acquire_plugin_throttle(name)` performs a lazy,
  idempotent seeding step on the resolved `ProcessLocalThrottleStore`:
  the module-level per-tool `Throttler` instances from
  `_PLUGIN_TOOL_THROTTLERS` are copied (by reference) into
  `store.throttlers`, and `_PLUGIN_THROTTLER` is installed under the
  sentinel `_PLUGIN_DEFAULT_BUCKET`. Known names go through their own
  bucket via `store.acquire(name)`; unknown names go through the
  shared default bucket directly.
* The legacy module attributes `_PLUGIN_THROTTLER` and
  `_PLUGIN_TOOL_THROTTLERS` are kept verbatim because (a) plugin
  semantics derivation at module load still populates them and
  (b) existing tests monkey-patch them to inject spies; the seeding
  helper reuses those exact instances so the contract is unchanged.

Alternate backends (anything other than `ProcessLocalThrottleStore`):
the seeding step is skipped and `store.acquire(name)` is called
once. The backend owns the full per-key + unknown-name fallback
semantics; this is documented explicitly in the helper docstring so
backend authors know to implement their own shared-default bucket if
they want one.

Tests:

* `tests/test_mcp_server_plugin_wiring.py::test_throttle_acquired_before_dispatch`
  now patches `_PLUGIN_THROTTLER`, clears the seeding cache
  (`_throttle_store_seeded`), and resets the RuntimeContext resolver
  so the next dispatch re-seeds with the spy throttler. The contract
  remains: "the plugin dispatch path must await a throttle slot
  before calling the provider".
* `test_declared_throttle_gets_dedicated_bucket` is unchanged — it
  asserts the module-level `_PLUGIN_TOOL_THROTTLERS` still contains a
  dedicated bucket per declared throttle, which is true because that
  dict is still populated at module load.

12 tests in `tests/test_mcp_server_plugin_wiring.py` pass; 484 tests
across `tests/core/`, the four refactored handler test files,
`tests/test_auth*`, and `tests/test_throttle.py` pass. ruff and
black clean on `mureo/`.

* refactor(byod): consult RuntimeContext for non-default workspace dir

`byod_data_dir()` gains a middle-priority resolution path: when the
resolved `RuntimeContext` exposes a non-default workspace (alternate
backend registered via the `mureo.runtime_context_factory` entry-point
group), BYOD data is read from `<workspace>/byod/` instead of the
legacy `~/.mureo/byod/`.

Resolution precedence:

1. `MUREO_BYOD_DIR` env var — unchanged, highest priority (install-
   desktop wrapper still sets this per-workspace).
2. Non-default `RuntimeContext.state_store.workspace` — NEW.
3. Legacy `~/.mureo/byod/` — preserved for existing CLI / Claude Code
   single-workspace users.

The middle path triggers only when `ctx.workspace_id != "default"` so
existing OSS users see no behaviour change. `workspace_id` is the
canonical sentinel pinned by `tests/core/test_runtime_context.py`.

The helper `_runtime_context_workspace()` is added next to
`byod_data_dir()` with a lazy import of `mureo.core.runtime_context`
so the module keeps its "no external dependencies" promise (the
module docstring already states this so the MCP server's startup path
stays light). An ImportError is treated as "no runtime layer"
defensively, although the lazy import is expected to succeed in
practice.

Tests: 489 pass across `tests/test_byod*.py` (49), `tests/core/`
(391), and `tests/test_auth*.py` (49). ruff and black clean on
`mureo/`.

* feat(cli,skills): route /learn through the KnowledgeStore Protocol

Introduce `mureo learn add <text> [--scope {operator,workspace}]`, a
new CLI command that persists diagnostic insights via
`get_runtime_context().knowledge_store` instead of writing files
directly. The `/learn` skill is updated to shell out to this command
so the file-system layout (and the scaffold seeding) becomes an
implementation detail of `FilesystemKnowledgeStore` rather than a
duplicated piece of skill markdown.

New CLI: `mureo/cli/learn_cmd.py`
* `learn add <text>` appends to the operator (cross-workspace) tier
  by default — the existing pro-diagnosis location for the default
  file-backed runtime.
* `--scope workspace` appends to the workspace tier; when the
  resolved KnowledgeStore has no workspace tier (the OSS default),
  the command exits non-zero with a helpful hint pointing at
  `--scope operator`. This is the integration point alternate
  backends use to split per-workspace insights from operator-wide
  ones.
* Wired into the top-level `mureo` app via `mureo/cli/main.py`.

Skill update: `skills/learn/SKILL.md`
* Version bumped 0.7.1 → 0.8.0.
* Step 2 (manual file-existence-check + scaffold seeding) is gone —
  the CLI's KnowledgeStore handles both transparently.
* Step 4 now instructs Claude to invoke `mureo learn add` instead of
  using Write/Edit on the file directly.
* IMPORTANT note rewritten: "Always save through `mureo learn add`,
  never by writing the file path manually."

Test changes:
* New `tests/test_cli_learn.py` (5 tests): default-scope writes to
  operator tier, explicit operator scope, workspace scope when the
  backend supports it, workspace scope without backend support exits
  with `--scope operator` hint, and the subcommand is registered
  under the top-level CLI.
* `tests/core/test_filesystem_knowledge_store.py::test_scaffold_matches_skills_learn_template`
  is replaced by `test_scaffold_has_expected_frontmatter_and_section`.
  The previous drift-vs-skill regression test no longer applies
  because the skill markdown no longer carries its own copy of the
  scaffold; the new test instead pins the shape of `_OPERATOR_SCAFFOLD`
  directly (frontmatter starts, expected name, ends with the
  Learned Insights heading + newline).

407 tests pass across `tests/core/`, `tests/test_cli_learn.py`, and
`tests/test_cli_rollback.py`. ruff and black clean on `mureo/`.

* fix(skills): sync packaged copy of learn skill with canonical source

The canonical skills/learn/SKILL.md was updated in c51b84f to route
through 'mureo learn add' (KnowledgeStore Protocol), but the packaged
mureo/_data/skills/learn/SKILL.md was not synced. The
test_packaged_skills_match_canonical_byte_for_byte invariant requires
both to be byte-identical so PyPI users see the same docs as the
GitHub canonical source.

Tests: tests/test_plugin_manifests.py + tests/test_setup_cmd.py +
tests/test_cli_learn.py + tests/core/ — 414 pass, ruff and black
clean on mureo/.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant