Base directory for this skill: /Users/agent/.claude/plugins/cache/superpowers-marketplace/superpowers/4.0.3/skills/writing-plans

# Writing Plans

## Overview

Write comprehensive implementation plans assuming the engineer has zero context for our codebase and questionable taste. Document everything they need to know: which files to touch for each task, code, testing, docs they might need to check, how to test it. Give them the whole plan as bite-sized tasks. DRY. YAGNI. TDD. Frequent commits.

Assume they are a skilled developer, but know almost nothing about our toolset or problem domain. Assume they don't know good test design very well.

**Announce at start:** "I'm using the writing-plans skill to create the implementation plan."

**Context:** This should be run in a dedicated worktree (created by brainstorming skill).

**Save plans to:** `docs/plans/YYYY-MM-DD-<feature-name>.md`

## Bite-Sized Task Granularity

**Each step is one action (2-5 minutes):**
- "Write the failing test" - step
- "Run it to make sure it fails" - step
- "Implement the minimal code to make the test pass" - step
- "Run the tests and make sure they pass" - step
- "Commit" - step

## Plan Document Header

**Every plan MUST start with this header:**

```markdown
# [Feature Name] Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** [One sentence describing what this builds]

**Architecture:** [2-3 sentences about approach]

**Tech Stack:** [Key technologies/libraries]

---
```

## Task Structure

```markdown
### Task N: [Component Name]

**Files:**
- Create: `exact/path/to/file.py`
- Modify: `exact/path/to/existing.py:123-145`
- Test: `tests/exact/path/to/test.py`

**Step 1: Write the failing test**

```python
def test_specific_behavior():
    result = function(input)
    assert result == expected
```

**Step 2: Run test to verify it fails**

Run: `pytest tests/path/test.py::test_name -v`
Expected: FAIL with "function not defined"

**Step 3: Write minimal implementation**

```python
def function(input):
    return expected
```

**Step 4: Run test to verify it passes**

Run: `pytest tests/path/test.py::test_name -v`
Expected: PASS

**Step 5: Commit**

```bash
git add tests/path/test.py src/path/file.py
git commit -m "feat: add specific feature"
```
```

## Remember
- Exact file paths always
- Complete code in plan (not "add validation")
- Exact commands with expected output
- Reference relevant skills with @ syntax
- DRY, YAGNI, TDD, frequent commits

## Execution Handoff

After saving the plan, offer execution choice:

**"Plan complete and saved to `docs/plans/<filename>.md`. Two execution options:**

**1. Subagent-Driven (this session)** - I dispatch fresh subagent per task, review between tasks, fast iteration

**2. Parallel Session (separate)** - Open new session with executing-plans, batch execution with checkpoints

**Which approach?"**

**If Subagent-Driven chosen:**
- **REQUIRED SUB-SKILL:** Use superpowers:subagent-driven-development
- Stay in this session
- Fresh subagent per task + code review

**If Parallel Session chosen:**
- Guide them to open new session in worktree
- **REQUIRED SUB-SKILL:** New session uses superpowers:executing-plans


I'm using the writing-plans skill to create the implementation plan. Let me explore the codebase to understand the current state before writing the plan.

I need to:
1. Move `ConsistencyPolicy` enum from `types.py` to `workspace/consistency.py`
2. Move `is_fresh()` from `workspace/cache/policy.py` to `workspace/consistency.py`
3. Make `io_read()`, `io_stat()`, `io_readdir()` go through Workspace methods

Let me first explore the relevant files.


I'm using the writing-plans skill to create the implementation plan.


     1→from __future__ import annotations
     2→
     3→import time
     4→
     5→from mirage.backend.base import BaseBackend
     6→from mirage.backend.filetype import guess_type
     7→from mirage.types import (ConsistencyPolicy, FileStat, Inode, MountMode,
     8→                          SyncPolicy, SyncResult, TransferStats)
     9→from mirage.workspace.cache import LRUCache
    10→from mirage.workspace.cache.policy import is_fresh
    11→from mirage.workspace.mount import MountRegistry
    12→from mirage.workspace.sync import DirtyTracker
    13→
    14→
    15→class _PathLock:
    16→
    17→    def __init__(self, ws: Workspace, paths: list[str]) -> None:
    18→        self._ws = ws
    19→        self._paths = paths
    20→
    21→    def __enter__(self) -> _PathLock:
    22→        return self
    23→
    24→    def __exit__(self, *_: object) -> None:
    25→        self._ws.unlock_paths(self._paths)
    26→
    27→
    28→class Workspace:
    29→    """
    30→    Unified virtual filesystem over heterogeneous backends.
    31→
    32→    Mounts backends at virtual path prefixes and provides a command-level API
    33→    (cat, ls, write, cp, mv, rm, ...) with transparent caching
    34→    and write staging.
    35→
    36→    Example::
    37→
    38→        ws = Workspace(
    39→            {"/data/": S3Backend(S3Config(bucket="my-bucket")),
    40→             "/tmp/":  MemoryBackend()},
    41→            cache_limit="256MB",
    42→            sync_policy=SyncPolicy.STAGED,
    43→        )
    44→        content = ws.cat("/data/reports/q1.json")
    45→        ws.write("/tmp/summary.txt", b"hello")
    46→        ws.sync()
    47→    """
    48→
    49→    def __init__(
    50→        self,
    51→        backends: dict[str, BaseBackend | tuple],
    52→        cache_limit: str | int = "512MB",
    53→        sync_policy: SyncPolicy = SyncPolicy.STAGED,
    54→        mode: MountMode = MountMode.READ,
    55→        consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED,
    56→        ttl_seconds: int = 300,
    57→    ) -> None:
    58→        self._registry = MountRegistry()
    59→        self._cache = LRUCache(cache_limit)
    60→        self._tracker = DirtyTracker()
    61→        self._sync_policy = sync_policy
    62→        self._ttl_seconds = ttl_seconds
    63→        self._locked_paths: set[str] = set()
    64→        self._transfer_stats: dict[str, TransferStats] = {}
    65→        self._created_at: float = time.monotonic()
    66→
    67→        for prefix, value in backends.items():
    68→            if isinstance(value, tuple) and len(value) == 3:
    69→                backend, mount_mode, mount_consistency = value
    70→            elif isinstance(value, tuple):
    71→                backend, mount_mode = value
    72→                mount_consistency = consistency
    73→            else:
    74→                backend = value
    75→                mount_mode = mode
    76→                mount_consistency = consistency
    77→            self._registry.mount(prefix, backend, mount_mode,
    78→                                 mount_consistency)
    79→
    80→    # ── mount management ────────────────────────────────────────────────────
    81→
    82→    def mount(self,
    83→              prefix: str,
    84→              backend: BaseBackend,
    85→              mode: MountMode = MountMode.READ,
    86→              consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED) -> None:
    87→        self._registry.mount(prefix, backend, mode, consistency)
    88→
    89→    def backend(self, path: str) -> BaseBackend:
    90→        backend, _, _ = self._registry.resolve(path)
    91→        return backend
    92→
    93→    # ── read ops ────────────────────────────────────────────────────────────
    94→
    95→    def cat(self, path: str) -> bytes:
    96→        inode = self._tracker.get(path)
    97→        if inode and inode.dirty:
    98→            entry = self._cache.get(path)
    99→            if entry is not None:
   100→                self._record_transfer(path,
   101→                                      "read",
   102→                                      len(entry.data),
   103→                                      0,
   104→                                      cache_hit=True)
   105→                return entry.data
   106→
   107→        entry = self._cache.get(path)
   108→        remote_stat = None
   109→        if entry is not None:
   110→            consistency = self._registry.mount_for(path).consistency
   111→            backend, bp, _ = self._registry.resolve(path)
   112→            fresh, remote_stat = is_fresh(entry, consistency,
   113→                                          self._ttl_seconds, backend, bp)
   114→            if fresh:
   115→                self._record_transfer(path,
   116→                                      "read",
   117→                                      len(entry.data),
   118→                                      0,
   119→                                      cache_hit=True)
   120→                return entry.data
   121→        else:
   122→            backend, bp, _ = self._registry.resolve(path)
   123→
   124→        t0 = time.monotonic()
   125→        data = backend.cat(bp)
   126→        duration_ms = int((time.monotonic() - t0) * 1000)
   127→        self._record_transfer(path,
   128→                              "read",
   129→                              len(data),
   130→                              duration_ms,
   131→                              cache_hit=False)
   132→        if remote_stat is None:
   133→            remote_stat = backend.stat(bp)
   134→        self._cache.put(path,
   135→                        data,
   136→                        etag=remote_stat.extra.get("etag"),
   137→                        modified=remote_stat.modified,
   138→                        size=remote_stat.size)
   139→        self._tracker.on_read(path, data, self._mount_prefix(path))
   140→        return data
   141→
   142→    def ls(self, path: str) -> list[str]:
   143→        backend, bp, _ = self._registry.resolve(path)
   144→        prefix = self._mount_prefix(path).rstrip("/")
   145→        backend_ok = False
   146→        try:
   147→            backend_entries = backend.ls(bp)
   148→            backend_ok = True
   149→        except FileNotFoundError:
   150→            backend_entries = []
   151→        result: set[str] = {
   152→            prefix + "/" + e.lstrip("/")
   153→            for e in backend_entries
   154→        }
   155→        norm_path = "/" + path.strip("/")
   156→        dir_prefix = norm_path.rstrip("/") + "/"
   157→        has_staged_children = False
   158→        for inode in self._tracker.dirty():
   159→            vp = inode.virtual_path
   160→            if not vp.startswith(dir_prefix):
   161→                continue
   162→            if inode.checksum is None:
   163→                result.discard(vp)
   164→                continue
   165→            has_staged_children = True
   166→            remainder = vp[len(dir_prefix):]
   167→            first = remainder.split("/")[0]
   168→            if not first:
   169→                continue
   170→            if "/" in remainder:
   171→                result.add(dir_prefix + first + "/")
   172→            else:
   173→                result.add(dir_prefix + first)
   174→        if not backend_ok and not has_staged_children:
   175→            raise FileNotFoundError(path)
   176→        return sorted(result)
   177→
   178→    def ls_l(self, path: str) -> list[FileStat]:
   179→        backend, bp, _ = self._registry.resolve(path)
   180→        return backend.ls_l(bp)
   181→
   182→    def stat(self, path: str) -> FileStat:
   183→        inode = self._tracker.get(path)
   184→        if inode and inode.dirty and inode.checksum is not None:
   185→            entry = self._cache.get(path)
   186→            size = len(entry.data) if entry is not None else inode.size
   187→            return FileStat(
   188→                name=path.rstrip("/").rsplit("/", 1)[-1],
   189→                size=size,
   190→                type=guess_type(path),
   191→            )
   192→        backend, bp, _ = self._registry.resolve(path)
   193→        return backend.stat(bp)
   194→
   195→    def exists(self, path: str) -> bool:
   196→        try:
   197→            self.stat(path)
   198→            return True
   199→        except (FileNotFoundError, ValueError):
   200→            return False
   201→
   202→    def head(self, path: str, lines: int = 10) -> bytes:
   203→        backend, bp, _ = self._registry.resolve(path)
   204→        return backend.head(bp, lines)
   205→
   206→    def tail(self, path: str, lines: int = 10) -> bytes:
   207→        backend, bp, _ = self._registry.resolve(path)
   208→        return backend.tail(bp, lines)
   209→
   210→    def read(self, path: str, offset: int, size: int) -> bytes:
   211→        inode = self._tracker.get(path)
   212→        if inode and inode.dirty:
   213→            entry = self._cache.get(path)
   214→            if entry is not None:
   215→                return entry.data[offset:offset + size]
   216→        backend, bp, _ = self._registry.resolve(path)
   217→        return backend.read(bp, offset, size)
   218→
   219→    def grep(self, pattern: str, path: str) -> list[str]:
   220→        backend, bp, _ = self._registry.resolve(path)
   221→        return backend.grep(pattern, bp)
   222→
   223→    # ── write ops ───────────────────────────────────────────────────────────
   224→
   225→    def write(self, path: str, data: bytes) -> None:
   226→        self._assert_not_locked(path)
   227→        self._assert_writable(path)
   228→        backend, bp, _ = self._registry.resolve(path)
   229→        self._cache.put(path, data)
   230→        self._tracker.on_write(path, data, self._mount_prefix(path))
   231→        self._record_transfer(path, "write", len(data), 0)
   232→        if self._sync_policy == SyncPolicy.IMMEDIATE:
   233→            backend.put(bp, data)
   234→            self._tracker.on_read(path, data, self._mount_prefix(path))
   235→
   236→    def mkdir(self, path: str) -> None:
   237→        self._assert_writable(path)
   238→        backend, bp, _ = self._registry.resolve(path)
   239→        backend.mkdir(bp)
   240→
   241→    def rm(self, path: str) -> None:
   242→        self._assert_writable(path)
   243→        backend, bp, _ = self._registry.resolve(path)
   244→        inode = self._tracker.get(path)
   245→        staged_only = inode and inode.dirty and inode.remote_checksum is None
   246→        if not staged_only:
   247→            backend.unlink(bp)
   248→        self._cache.invalidate(path)
   249→        self._tracker.on_delete(path)
   250→
   251→    def mv(self, src: str, dst: str) -> None:
   252→        self._assert_writable(src)
   253→        sb, sbp, _ = self._registry.resolve(src)
   254→        db, dbp, _ = self._registry.resolve(dst)
   255→        src_inode = self._tracker.get(src)
   256→        staged_only = (src_inode and src_inode.dirty
   257→                       and src_inode.remote_checksum is None)
   258→        if staged_only:
   259→            entry = self._cache.get(src)
   260→            data = entry.data if entry else b""
   261→            self._cache.put(dst, data)
   262→            self._tracker.on_write(dst, data, self._mount_prefix(dst))
   263→            self._cache.invalidate(src)
   264→            self._tracker.on_delete(src)
   265→        elif sb is db:
   266→            sb.rename(sbp, dbp)
   267→            self._cache.invalidate(src)
   268→        else:
   269→            data = sb.cat(sbp)
   270→            db.put(dbp, data)
   271→            sb.unlink(sbp)
   272→            self._cache.invalidate(src)
   273→
   274→    def cp(self, src: str, dst: str) -> None:
   275→        self._assert_writable(dst)
   276→        data = self.cat(src)
   277→        db, dbp, _ = self._registry.resolve(dst)
   278→        db.put(dbp, data)
   279→
   280→    # ── sync lifecycle ──────────────────────────────────────────────────────
   281→
   282→    def sync(self, path: str | None = None) -> list[SyncResult]:
   283→        results: list[SyncResult] = []
   284→        targets = [
   285→            i for i in self._tracker.dirty()
   286→            if path is None or i.virtual_path == path
   287→        ]
   288→        for inode in targets:
   289→            backend, bp, _ = self._registry.resolve(inode.virtual_path)
   290→            entry = self._cache.get(inode.virtual_path)
   291→            data = entry.data if entry else None
   292→            result = self._tracker.on_sync(inode.virtual_path, bp, data,
   293→                                           backend)
   294→            results.append(result)
   295→        return results
   296→
   297→    def status(self) -> list[Inode]:
   298→        return self._tracker.status()
   299→
   300→    def reset(self) -> None:
   301→        self._tracker.reset()
   302→        self._cache.clear()
   303→
   304→    # ── path locking ─────────────────────────────────────────────────────────
   305→
   306→    def _lock_path(self, path: str) -> None:
   307→        self._locked_paths.add(path)
   308→
   309→    def unlock_paths(self, paths: list[str]) -> None:
   310→        for p in paths:
   311→            self._locked_paths.discard(p)
   312→
   313→    def lock_paths(self, paths: list[str]) -> _PathLock:
   314→        for p in paths:
   315→            self._lock_path(p)
   316→        return _PathLock(self, paths)
   317→
   318→    def _assert_not_locked(self, path: str) -> None:
   319→        if path in self._locked_paths:
   320→            raise PermissionError(
   321→                f"path {path!r} is locked by remote execution")
   322→
   323→    # ── helpers ─────────────────────────────────────────────────────────────
   324→
   325→    def _mount_prefix(self, path: str) -> str:
   326→        norm = "/" + path.strip("/")
   327→        for m in self._registry.mounts():
   328→            if norm == m.prefix.rstrip("/") or norm.startswith(m.prefix):
   329→                return m.prefix
   330→        return "/"
   331→
   332→    def _assert_writable(self, path: str) -> None:
   333→        _, _, mode = self._registry.resolve(path)
   334→        if mode == MountMode.READ:
   335→            raise PermissionError(f"mount at {path!r} is read-only")
   336→
   337→    def _record_transfer(self,
   338→                         path: str,
   339→                         op: str,
   340→                         nbytes: int,
   341→                         duration_ms: int,
   342→                         cache_hit: bool | None = None) -> None:
   343→        prefix = self._mount_prefix(path)
   344→        if prefix not in self._transfer_stats:
   345→            self._transfer_stats[prefix] = TransferStats()
   346→        stats = self._transfer_stats[prefix]
   347→        if op == "read":
   348→            stats.bytes_read += nbytes
   349→            stats.read_ops += 1
   350→            stats.total_read_ms += duration_ms
   351→            if cache_hit is True:
   352→                stats.cache_hits += 1
   353→            elif cache_hit is False:
   354→                stats.cache_misses += 1
   355→        elif op == "write":
   356→            stats.bytes_written += nbytes
   357→            stats.write_ops += 1
   358→            stats.total_write_ms += duration_ms
   359→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

from __future__ import annotations

import time

from mirage.backend.base import BaseBackend
from mirage.backend.filetype import guess_type
from mirage.types import (ConsistencyPolicy, FileStat, Inode, MountMode,
                          SyncPolicy, SyncResult, TransferStats)
from mirage.workspace.cache import LRUCache
from mirage.workspace.cache.policy import is_fresh
from mirage.workspace.mount import MountRegistry
from mirage.workspace.sync import DirtyTracker

class _PathLock:

    def __init__(self, ws: Workspace, paths: list[str]) -> None:
        self._ws = ws
        self._paths = paths

    def __enter__(self) -> _PathLock:
        return self

    def __exit__(self, *_: object) -> None:
        self._ws.unlock_paths(self._paths)

class Workspace:
    """
    Unified virtual filesystem over heterogeneous backends.

    Mounts backends at virtual path prefixes and provides a command-level API
    (cat, ls, write, cp, mv, rm, ...) with transparent caching
    and write staging.

    Example::

        ws = Workspace(
            {"/data/": S3Backend(S3Config(bucket="my-bucket")),
             "/tmp/":  MemoryBackend()},
            cache_limit="256MB",
            sync_policy=SyncPolicy.STAGED,
        )
        content = ws.cat("/data/reports/q1.json")
        ws.write("/tmp/summary.txt", b"hello")
        ws.sync()
    """

    def __init__(
        self,
        backends: dict[str, BaseBackend | tuple],
        cache_limit: str | int = "512MB",
        sync_policy: SyncPolicy = SyncPolicy.STAGED,
        mode: MountMode = MountMode.READ,
        consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED,
        ttl_seconds: int = 300,
    ) -> None:
        self._registry = MountRegistry()
        self._cache = LRUCache(cache_limit)
        self._tracker = DirtyTracker()
        self._sync_policy = sync_policy
        self._ttl_seconds = ttl_seconds
        self._locked_paths: set[str] = set()
        self._transfer_stats: dict[str, TransferStats] = {}
        self._created_at: float = time.monotonic()

        for prefix, value in backends.items():
            if isinstance(value, tuple) and len(value) == 3:
                backend, mount_mode, mount_consistency = value
            elif isinstance(value, tuple):
                backend, mount_mode = value
                mount_consistency = consistency
            else:
                backend = value
                mount_mode = mode
                mount_consistency = consistency
            self._registry.mount(prefix, backend, mount_mode,
                                 mount_consistency)

    # ── mount management ────────────────────────────────────────────────────

    def mount(self,
              prefix: str,
              backend: BaseBackend,
              mode: MountMode = MountMode.READ,
              consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED) -> None:
        self._registry.mount(prefix, backend, mode, consistency)

    def backend(self, path: str) -> BaseBackend:
        backend, _, _ = self._registry.resolve(path)
        return backend

    # ── read ops ────────────────────────────────────────────────────────────

    def cat(self, path: str) -> bytes:
        inode = self._tracker.get(path)
        if inode and inode.dirty:
            entry = self._cache.get(path)
            if entry is not None:
                self._record_transfer(path,
                                      "read",
                                      len(entry.data),
                                      0,
                                      cache_hit=True)
                return entry.data

        entry = self._cache.get(path)
        remote_stat = None
        if entry is not None:
            consistency = self._registry.mount_for(path).consistency
            backend, bp, _ = self._registry.resolve(path)
            fresh, remote_stat = is_fresh(entry, consistency,
                                          self._ttl_seconds, backend, bp)
            if fresh:
                self._record_transfer(path,
                                      "read",
                                      len(entry.data),
                                      0,
                                      cache_hit=True)
                return entry.data
        else:
            backend, bp, _ = self._registry.resolve(path)

        t0 = time.monotonic()
        data = backend.cat(bp)
        duration_ms = int((time.monotonic() - t0) * 1000)
        self._record_transfer(path,
                              "read",
                              len(data),
                              duration_ms,
                              cache_hit=False)
        if remote_stat is None:
            remote_stat = backend.stat(bp)
        self._cache.put(path,
                        data,
                        etag=remote_stat.extra.get("etag"),
                        modified=remote_stat.modified,
                        size=remote_stat.size)
        self._tracker.on_read(path, data, self._mount_prefix(path))
        return data

    def ls(self, path: str) -> list[str]:
        backend, bp, _ = self._registry.resolve(path)
        prefix = self._mount_prefix(path).rstrip("/")
        backend_ok = False
        try:
            backend_entries = backend.ls(bp)
            backend_ok = True
        except FileNotFoundError:
            backend_entries = []
        result: set[str] = {
            prefix + "/" + e.lstrip("/")
            for e in backend_entries
        }
        norm_path = "/" + path.strip("/")
        dir_prefix = norm_path.rstrip("/") + "/"
        has_staged_children = False
        for inode in self._tracker.dirty():
            vp = inode.virtual_path
            if not vp.startswith(dir_prefix):
                continue
            if inode.checksum is None:
                result.discard(vp)
                continue
            has_staged_children = True
            remainder = vp[len(dir_prefix):]
            first = remainder.split("/")[0]
            if not first:
                continue
            if "/" in remainder:
                result.add(dir_prefix + first + "/")
            else:
                result.add(dir_prefix + first)
        if not backend_ok and not has_staged_children:
            raise FileNotFoundError(path)
        return sorted(result)

    def ls_l(self, path: str) -> list[FileStat]:
        backend, bp, _ = self._registry.resolve(path)
        return backend.ls_l(bp)

    def stat(self, path: str) -> FileStat:
        inode = self._tracker.get(path)
        if inode and inode.dirty and inode.checksum is not None:
            entry = self._cache.get(path)
            size = len(entry.data) if entry is not None else inode.size
            return FileStat(
                name=path.rstrip("/").rsplit("/", 1)[-1],
                size=size,
                type=guess_type(path),
            )
        backend, bp, _ = self._registry.resolve(path)
        return backend.stat(bp)

    def exists(self, path: str) -> bool:
        try:
            self.stat(path)
            return True
        except (FileNotFoundError, ValueError):
            return False

    def head(self, path: str, lines: int = 10) -> bytes:
        backend, bp, _ = self._registry.resolve(path)
        return backend.head(bp, lines)

    def tail(self, path: str, lines: int = 10) -> bytes:
        backend, bp, _ = self._registry.resolve(path)
        return backend.tail(bp, lines)

    def read(self, path: str, offset: int, size: int) -> bytes:
        inode = self._tracker.get(path)
        if inode and inode.dirty:
            entry = self._cache.get(path)
            if entry is not None:
                return entry.data[offset:offset + size]
        backend, bp, _ = self._registry.resolve(path)
        return backend.read(bp, offset, size)

    def grep(self, pattern: str, path: str) -> list[str]:
        backend, bp, _ = self._registry.resolve(path)
        return backend.grep(pattern, bp)

    # ── write ops ───────────────────────────────────────────────────────────

    def write(self, path: str, data: bytes) -> None:
        self._assert_not_locked(path)
        self._assert_writable(path)
        backend, bp, _ = self._registry.resolve(path)
        self._cache.put(path, data)
        self._tracker.on_write(path, data, self._mount_prefix(path))
        self._record_transfer(path, "write", len(data), 0)
        if self._sync_policy == SyncPolicy.IMMEDIATE:
            backend.put(bp, data)
            self._tracker.on_read(path, data, self._mount_prefix(path))

    def mkdir(self, path: str) -> None:
        self._assert_writable(path)
        backend, bp, _ = self._registry.resolve(path)
        backend.mkdir(bp)

    def rm(self, path: str) -> None:
        self._assert_writable(path)
        backend, bp, _ = self._registry.resolve(path)
        inode = self._tracker.get(path)
        staged_only = inode and inode.dirty and inode.remote_checksum is None
        if not staged_only:
            backend.unlink(bp)
        self._cache.invalidate(path)
        self._tracker.on_delete(path)

    def mv(self, src: str, dst: str) -> None:
        self._assert_writable(src)
        sb, sbp, _ = self._registry.resolve(src)
        db, dbp, _ = self._registry.resolve(dst)
        src_inode = self._tracker.get(src)
        staged_only = (src_inode and src_inode.dirty
                       and src_inode.remote_checksum is None)
        if staged_only:
            entry = self._cache.get(src)
            data = entry.data if entry else b""
            self._cache.put(dst, data)
            self._tracker.on_write(dst, data, self._mount_prefix(dst))
            self._cache.invalidate(src)
            self._tracker.on_delete(src)
        elif sb is db:
            sb.rename(sbp, dbp)
            self._cache.invalidate(src)
        else:
            data = sb.cat(sbp)
            db.put(dbp, data)
            sb.unlink(sbp)
            self._cache.invalidate(src)

    def cp(self, src: str, dst: str) -> None:
        self._assert_writable(dst)
        data = self.cat(src)
        db, dbp, _ = self._registry.resolve(dst)
        db.put(dbp, data)

    # ── sync lifecycle ──────────────────────────────────────────────────────

    def sync(self, path: str | None = None) -> list[SyncResult]:
        results: list[SyncResult] = []
        targets = [
            i for i in self._tracker.dirty()
            if path is None or i.virtual_path == path
        ]
        for inode in targets:
            backend, bp, _ = self._registry.resolve(inode.virtual_path)
            entry = self._cache.get(inode.virtual_path)
            data = entry.data if entry else None
            result = self._tracker.on_sync(inode.virtual_path, bp, data,
                                           backend)
            results.append(result)
        return results

    def status(self) -> list[Inode]:
        return self._tracker.status()

    def reset(self) -> None:
        self._tracker.reset()
        self._cache.clear()

    # ── path locking ─────────────────────────────────────────────────────────

    def _lock_path(self, path: str) -> None:
        self._locked_paths.add(path)

    def unlock_paths(self, paths: list[str]) -> None:
        for p in paths:
            self._locked_paths.discard(p)

    def lock_paths(self, paths: list[str]) -> _PathLock:
        for p in paths:
            self._lock_path(p)
        return _PathLock(self, paths)

    def _assert_not_locked(self, path: str) -> None:
        if path in self._locked_paths:
            raise PermissionError(
                f"path {path!r} is locked by remote execution")

    # ── helpers ─────────────────────────────────────────────────────────────

    def _mount_prefix(self, path: str) -> str:
        norm = "/" + path.strip("/")
        for m in self._registry.mounts():
            if norm == m.prefix.rstrip("/") or norm.startswith(m.prefix):
                return m.prefix
        return "/"

    def _assert_writable(self, path: str) -> None:
        _, _, mode = self._registry.resolve(path)
        if mode == MountMode.READ:
            raise PermissionError(f"mount at {path!r} is read-only")

    def _record_transfer(self,
                         path: str,
                         op: str,
                         nbytes: int,
                         duration_ms: int,
                         cache_hit: bool | None = None) -> None:
        prefix = self._mount_prefix(path)
        if prefix not in self._transfer_stats:
            self._transfer_stats[prefix] = TransferStats()
        stats = self._transfer_stats[prefix]
        if op == "read":
            stats.bytes_read += nbytes
            stats.read_ops += 1
            stats.total_read_ms += duration_ms
            if cache_hit is True:
                stats.cache_hits += 1
            elif cache_hit is False:
                stats.cache_misses += 1
        elif op == "write":
            stats.bytes_written += nbytes
            stats.write_ops += 1
            stats.total_write_ms += duration_ms


     1→import time
     2→
     3→from mirage.backend.base import BaseBackend
     4→from mirage.types import ConsistencyPolicy, FileStat
     5→from mirage.workspace.cache.entry import CacheEntry
     6→
     7→
     8→def is_fresh(
     9→    entry: CacheEntry,
    10→    consistency: ConsistencyPolicy,
    11→    ttl_seconds: int,
    12→    backend: BaseBackend,
    13→    backend_path: str,
    14→) -> tuple[bool, FileStat | None]:
    15→    if consistency == ConsistencyPolicy.CACHED:
    16→        return True, None
    17→    if consistency == ConsistencyPolicy.ALWAYS:
    18→        return False, None
    19→    if consistency == ConsistencyPolicy.TTL:
    20→        if (time.monotonic() - entry.cached_at) < ttl_seconds:
    21→            return True, None
    22→    try:
    23→        remote = backend.stat(backend_path)
    24→    except FileNotFoundError:
    25→        return False, None
    26→    return metadata_matches(entry, remote), remote
    27→
    28→
    29→def metadata_matches(entry: CacheEntry, remote: FileStat) -> bool:
    30→    remote_etag = remote.extra.get("etag")
    31→    if entry.etag and remote_etag:
    32→        return entry.etag == remote_etag
    33→    if entry.modified and remote.modified:
    34→        return entry.modified == remote.modified
    35→    if entry.size is not None and remote.size is not None:
    36→        return entry.size == remote.size
    37→    return False
    38→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import time

from mirage.backend.base import BaseBackend
from mirage.types import ConsistencyPolicy, FileStat
from mirage.workspace.cache.entry import CacheEntry

def is_fresh(
    entry: CacheEntry,
    consistency: ConsistencyPolicy,
    ttl_seconds: int,
    backend: BaseBackend,
    backend_path: str,
) -> tuple[bool, FileStat | None]:
    if consistency == ConsistencyPolicy.CACHED:
        return True, None
    if consistency == ConsistencyPolicy.ALWAYS:
        return False, None
    if consistency == ConsistencyPolicy.TTL:
        if (time.monotonic() - entry.cached_at) < ttl_seconds:
            return True, None
    try:
        remote = backend.stat(backend_path)
    except FileNotFoundError:
        return False, None
    return metadata_matches(entry, remote), remote

def metadata_matches(entry: CacheEntry, remote: FileStat) -> bool:
    remote_etag = remote.extra.get("etag")
    if entry.etag and remote_etag:
        return entry.etag == remote_etag
    if entry.modified and remote.modified:
        return entry.modified == remote.modified
    if entry.size is not None and remote.size is not None:
        return entry.size == remote.size
    return False


     1→from mirage.backend.base import BaseBackend
     2→from mirage.types import ConsistencyPolicy, MountMode
     3→
     4→
     5→class Mount:
     6→
     7→    def __init__(
     8→            self,
     9→            prefix: str,
    10→            backend: BaseBackend,
    11→            mode: MountMode = MountMode.READ,
    12→            consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED) -> None:
    13→        stripped = prefix.strip("/")
    14→        self.prefix = "/" + stripped + "/" if stripped else "/"
    15→        self.backend = backend
    16→        self.mode = mode
    17→        self.consistency = consistency
    18→
    19→
    20→class MountRegistry:
    21→    """
    22→    Longest-prefix-match router.
    23→
    24→    Given a virtual path like "/s3-prod/data/file.json", resolves to the
    25→    backend mounted at "/s3-prod/" and returns the stripped path
    26→    "/data/file.json".
    27→    """
    28→
    29→    def __init__(self) -> None:
    30→        self._mounts: list[Mount] = []
    31→
    32→    def mount(
    33→            self,
    34→            prefix: str,
    35→            backend: BaseBackend,
    36→            mode: MountMode = MountMode.READ,
    37→            consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED) -> None:
    38→        m = Mount(prefix, backend, mode, consistency)
    39→        for existing in self._mounts:
    40→            if existing.prefix == m.prefix:
    41→                raise ValueError(f"duplicate mount prefix: {m.prefix!r}")
    42→        self._mounts.append(m)
    43→        self._mounts.sort(key=lambda x: len(x.prefix), reverse=True)
    44→
    45→    def resolve(self, path: str) -> tuple[BaseBackend, str, MountMode]:
    46→        """
    47→        Returns (backend, backend_path, mode).
    48→
    49→        backend_path has the mount prefix stripped.
    50→        Raises ValueError if no mount matches.
    51→        """
    52→        norm = "/" + path.strip("/")
    53→        # ensure trailing slash when checking prefix match for directories
    54→        for m in self._mounts:
    55→            if norm == m.prefix.rstrip("/") or norm.startswith(m.prefix):
    56→                backend_path = "/" + norm[len(m.prefix):]
    57→                return m.backend, backend_path, m.mode
    58→        raise ValueError(f"no mount matches path: {path!r}")
    59→
    60→    def mount_for(self, path: str) -> Mount:
    61→        norm = "/" + path.strip("/")
    62→        for m in self._mounts:
    63→            if norm == m.prefix.rstrip("/") or norm.startswith(m.prefix):
    64→                return m
    65→        raise ValueError(f"no mount matches path: {path!r}")
    66→
    67→    def mounts(self) -> list[Mount]:
    68→        return list(self._mounts)
    69→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

from mirage.backend.base import BaseBackend
from mirage.types import ConsistencyPolicy, MountMode

class Mount:

    def __init__(
            self,
            prefix: str,
            backend: BaseBackend,
            mode: MountMode = MountMode.READ,
            consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED) -> None:
        stripped = prefix.strip("/")
        self.prefix = "/" + stripped + "/" if stripped else "/"
        self.backend = backend
        self.mode = mode
        self.consistency = consistency

class MountRegistry:
    """
    Longest-prefix-match router.

    Given a virtual path like "/s3-prod/data/file.json", resolves to the
    backend mounted at "/s3-prod/" and returns the stripped path
    "/data/file.json".
    """

    def __init__(self) -> None:
        self._mounts: list[Mount] = []

    def mount(
            self,
            prefix: str,
            backend: BaseBackend,
            mode: MountMode = MountMode.READ,
            consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED) -> None:
        m = Mount(prefix, backend, mode, consistency)
        for existing in self._mounts:
            if existing.prefix == m.prefix:
                raise ValueError(f"duplicate mount prefix: {m.prefix!r}")
        self._mounts.append(m)
        self._mounts.sort(key=lambda x: len(x.prefix), reverse=True)

    def resolve(self, path: str) -> tuple[BaseBackend, str, MountMode]:
        """
        Returns (backend, backend_path, mode).

        backend_path has the mount prefix stripped.
        Raises ValueError if no mount matches.
        """
        norm = "/" + path.strip("/")
        # ensure trailing slash when checking prefix match for directories
        for m in self._mounts:
            if norm == m.prefix.rstrip("/") or norm.startswith(m.prefix):
                backend_path = "/" + norm[len(m.prefix):]
                return m.backend, backend_path, m.mode
        raise ValueError(f"no mount matches path: {path!r}")

    def mount_for(self, path: str) -> Mount:
        norm = "/" + path.strip("/")
        for m in self._mounts:
            if norm == m.prefix.rstrip("/") or norm.startswith(m.prefix):
                return m
        raise ValueError(f"no mount matches path: {path!r}")

    def mounts(self) -> list[Mount]:
        return list(self._mounts)


Now I have a clear picture of the codebase. Let me also check the types.py to see where ConsistencyPolicy is defined and what all imports it.


Grep

from mirage.types import.*ConsistencyPolicy|from mirage.workspace.cache.policy import


tests/workspace/test_workspace.py:6:from mirage.types import ConsistencyPolicy, MountMode, SyncPolicy
tests/workspace/cache/test_policy.py:3:from mirage.workspace.cache.policy import metadata_matches
tests/cli/test_app.py:32:    from mirage.types import ConsistencyPolicy, MountMode, RuntimeType
tests/cli/test_config.py:9:from mirage.types import ConsistencyPolicy, MountMode
mirage/__init__.py:2:from mirage.types import (ConsistencyPolicy, FileStat, Inode, MountMode,
mirage/cli/commands/workspace.py:19:from mirage.types import ConsistencyPolicy, MountMode, RuntimeType
mirage/cli/config.py:8:from mirage.types import BackendType, ConsistencyPolicy, MountMode
mirage/cli/commands/command.py:5:from mirage.types import ConsistencyPolicy, MountMode, RuntimeType
mirage/workspace/workspace.py:7:from mirage.types import (ConsistencyPolicy, FileStat, Inode, MountMode,
mirage/workspace/workspace.py:10:from mirage.workspace.cache.policy import is_fresh
mirage/workspace/mount.py:2:from mirage.types import ConsistencyPolicy, MountMode
mirage/workspace/cache/policy.py:4:from mirage.types import ConsistencyPolicy, FileStat
mirage/server/fastapi/config.py:3:from mirage.types import ConsistencyPolicy, MountMode, SyncPolicy
mirage/server/service/local.py:25:from mirage.types import (CommandStats, ConsistencyPolicy, FileStat, MountMode,


     1→from enum import Enum
     2→
     3→from pydantic import BaseModel, ConfigDict, Field
     4→
     5→
     6→class FileType(str, Enum):
     7→    DIRECTORY = "directory"
     8→    TEXT = "text"
     9→    BINARY = "binary"
    10→    JSON = "json"
    11→    CSV = "csv"
    12→    IMAGE_PNG = "image/png"
    13→    IMAGE_JPEG = "image/jpeg"
    14→    IMAGE_GIF = "image/gif"
    15→    ZIP = "application/zip"
    16→    GZIP = "application/gzip"
    17→    PDF = "application/pdf"
    18→
    19→
    20→class CostLevel(str, Enum):
    21→    CHEAP = "cheap"
    22→    PARTIAL = "partial"
    23→    EXPENSIVE = "expensive"
    24→    NETWORK = "network"
    25→
    26→
    27→class FileStat(BaseModel):
    28→    model_config = ConfigDict(frozen=True)
    29→
    30→    name: str
    31→    size: int | None = None
    32→    modified: str | None = None
    33→    type: FileType | None = None
    34→    extra: dict = Field(default_factory=dict)
    35→
    36→
    37→class SyncPolicy(str, Enum):
    38→    STAGED = "staged"
    39→    IMMEDIATE = "immediate"
    40→    MANUAL = "manual"
    41→
    42→
    43→class MountMode(str, Enum):
    44→    READ = "read"
    45→    WRITE = "write"
    46→    EXEC = "exec"
    47→
    48→
    49→class ConsistencyPolicy(str, Enum):
    50→    CACHED = "cached"
    51→    LAZY = "lazy"
    52→    TTL = "ttl"
    53→    ALWAYS = "always"
    54→
    55→
    56→class RuntimeType(str, Enum):
    57→    MEMORY = "memory"
    58→    LOCAL = "local"
    59→    REMOTE = "remote"
    60→
    61→
    62→class BackendType(str, Enum):
    63→    MEMORY = "memory"
    64→    LOCAL = "local"
    65→    S3 = "s3"
    66→    R2 = "r2"
    67→    HTTP = "http"
    68→    HTTPS = "https"
    69→
    70→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

from enum import Enum

from pydantic import BaseModel, ConfigDict, Field

class FileType(str, Enum):
    DIRECTORY = "directory"
    TEXT = "text"
    BINARY = "binary"
    JSON = "json"
    CSV = "csv"
    IMAGE_PNG = "image/png"
    IMAGE_JPEG = "image/jpeg"
    IMAGE_GIF = "image/gif"
    ZIP = "application/zip"
    GZIP = "application/gzip"
    PDF = "application/pdf"

class CostLevel(str, Enum):
    CHEAP = "cheap"
    PARTIAL = "partial"
    EXPENSIVE = "expensive"
    NETWORK = "network"

class FileStat(BaseModel):
    model_config = ConfigDict(frozen=True)

    name: str
    size: int | None = None
    modified: str | None = None
    type: FileType | None = None
    extra: dict = Field(default_factory=dict)

class SyncPolicy(str, Enum):
    STAGED = "staged"
    IMMEDIATE = "immediate"
    MANUAL = "manual"

class MountMode(str, Enum):
    READ = "read"
    WRITE = "write"
    EXEC = "exec"

class ConsistencyPolicy(str, Enum):
    CACHED = "cached"
    LAZY = "lazy"
    TTL = "ttl"
    ALWAYS = "always"

class RuntimeType(str, Enum):
    MEMORY = "memory"
    LOCAL = "local"
    REMOTE = "remote"

class BackendType(str, Enum):
    MEMORY = "memory"
    LOCAL = "local"
    S3 = "s3"
    R2 = "r2"
    HTTP = "http"
    HTTPS = "https"


Now I have a complete picture. Let me also check the tests for the io_* methods to understand what tests exist.


     1→import pytest
     2→
     3→from mirage.observe import session_log as observe_session_module
     4→from mirage.server.service.local import LocalService
     5→
     6→
     7→@pytest.fixture
     8→def svc():
     9→    s = LocalService()
    10→    observe_session_module._session_logs.clear()
    11→    yield s
    12→    s._sessions.clear()
    13→    observe_session_module._session_logs.clear()
    14→
    15→
    16→def test_create_and_list(svc):
    17→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    18→    assert isinstance(ws_id, str)
    19→    workspaces = svc.list_workspaces()
    20→    assert len(workspaces) == 1
    21→    assert workspaces[0]["id"] == ws_id
    22→
    23→
    24→def test_exec_touch_ls(svc):
    25→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    26→    ok, _, _, _ = svc.exec(ws_id, "touch /hello.txt")
    27→    assert ok
    28→    ok, output, _, _ = svc.exec(ws_id, "ls /")
    29→    assert ok
    30→    assert "hello.txt" in output
    31→
    32→
    33→def test_exec_blocked_with_runtime(svc):
    34→    ws_id = svc.create_workspace({
    35→        "mounts": {
    36→            "/": "memory"
    37→        },
    38→        "runtime": "memory",
    39→    })
    40→    ok, err, _, _ = svc.exec(ws_id, "python -c 'print(1)'")
    41→    assert not ok
    42→    assert "not allowed" in err
    43→
    44→
    45→def test_exec_not_blocked_without_runtime(svc):
    46→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    47→    ok, _, _, _ = svc.exec(ws_id, "ls /")
    48→    assert ok
    49→
    50→
    51→def test_delete_workspace(svc):
    52→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    53→    svc.delete_workspace(ws_id)
    54→    assert svc.list_workspaces() == []
    55→
    56→
    57→def test_delete_missing_raises(svc):
    58→    with pytest.raises(KeyError):
    59→        svc.delete_workspace("bad-id")
    60→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import pytest

from mirage.observe import session_log as observe_session_module
from mirage.server.service.local import LocalService

@pytest.fixture
def svc():
    s = LocalService()
    observe_session_module._session_logs.clear()
    yield s
    s._sessions.clear()
    observe_session_module._session_logs.clear()

def test_create_and_list(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    assert isinstance(ws_id, str)
    workspaces = svc.list_workspaces()
    assert len(workspaces) == 1
    assert workspaces[0]["id"] == ws_id

def test_exec_touch_ls(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    ok, _, _, _ = svc.exec(ws_id, "touch /hello.txt")
    assert ok
    ok, output, _, _ = svc.exec(ws_id, "ls /")
    assert ok
    assert "hello.txt" in output

def test_exec_blocked_with_runtime(svc):
    ws_id = svc.create_workspace({
        "mounts": {
            "/": "memory"
        },
        "runtime": "memory",
    })
    ok, err, _, _ = svc.exec(ws_id, "python -c 'print(1)'")
    assert not ok
    assert "not allowed" in err

def test_exec_not_blocked_without_runtime(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    ok, _, _, _ = svc.exec(ws_id, "ls /")
    assert ok

def test_delete_workspace(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    svc.delete_workspace(ws_id)
    assert svc.list_workspaces() == []

def test_delete_missing_raises(svc):
    with pytest.raises(KeyError):
        svc.delete_workspace("bad-id")


     1→from mirage.types import FileStat
     2→from mirage.workspace.cache.entry import CacheEntry
     3→from mirage.workspace.cache.policy import metadata_matches
     4→
     5→
     6→def test_metadata_matches_etag_priority():
     7→    entry = CacheEntry(data=b"x",
     8→                       cached_at=0,
     9→                       etag="abc",
    10→                       modified="old",
    11→                       size=1)
    12→    remote = FileStat(name="f",
    13→                      size=999,
    14→                      modified="new",
    15→                      extra={"etag": "abc"})
    16→    assert metadata_matches(entry, remote) is True
    17→
    18→
    19→def test_metadata_matches_fallback_to_modified():
    20→    entry = CacheEntry(data=b"x", cached_at=0, modified="2024-01-01", size=1)
    21→    remote = FileStat(name="f", size=999, modified="2024-01-01")
    22→    assert metadata_matches(entry, remote) is True
    23→    remote2 = FileStat(name="f", size=999, modified="2024-02-01")
    24→    assert metadata_matches(entry, remote2) is False
    25→
    26→
    27→def test_metadata_matches_fallback_to_size():
    28→    entry = CacheEntry(data=b"x", cached_at=0, size=100)
    29→    remote = FileStat(name="f", size=100)
    30→    assert metadata_matches(entry, remote) is True
    31→    remote2 = FileStat(name="f", size=200)
    32→    assert metadata_matches(entry, remote2) is False
    33→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

from mirage.types import FileStat
from mirage.workspace.cache.entry import CacheEntry
from mirage.workspace.cache.policy import metadata_matches

def test_metadata_matches_etag_priority():
    entry = CacheEntry(data=b"x",
                       cached_at=0,
                       etag="abc",
                       modified="old",
                       size=1)
    remote = FileStat(name="f",
                      size=999,
                      modified="new",
                      extra={"etag": "abc"})
    assert metadata_matches(entry, remote) is True

def test_metadata_matches_fallback_to_modified():
    entry = CacheEntry(data=b"x", cached_at=0, modified="2024-01-01", size=1)
    remote = FileStat(name="f", size=999, modified="2024-01-01")
    assert metadata_matches(entry, remote) is True
    remote2 = FileStat(name="f", size=999, modified="2024-02-01")
    assert metadata_matches(entry, remote2) is False

def test_metadata_matches_fallback_to_size():
    entry = CacheEntry(data=b"x", cached_at=0, size=100)
    remote = FileStat(name="f", size=100)
    assert metadata_matches(entry, remote) is True
    remote2 = FileStat(name="f", size=200)
    assert metadata_matches(entry, remote2) is False


Now I have a complete picture. Let me write the plan.

The two changes are:
1. **Move `ConsistencyPolicy` + `is_fresh()` to `mirage/workspace/consistency.py`** (consolidate the enum + logic)
2. **Route `io_*` methods through Workspace methods** so they benefit from consistency/caching

Key facts:
- `ConsistencyPolicy` is in `types.py` — imported in 11 files
- `is_fresh()` + `metadata_matches()` are in `workspace/cache/policy.py`
- `policy.py` will become empty after moving — we can delete it and move `is_fresh`/`metadata_matches` to `workspace/consistency.py`

- `io_read()` and `io_stat()` can route directly to existing Workspace methods, but `io_readdir()` is trickier since Workspace's `ls()` returns strings while `readdir()` needs to return entries with a cursor for pagination.
- Looking at the other methods, some map cleanly (`io_unlink()` → `ws.rm()`, `io_mkdir()` → `ws.mkdir()`, `io_rename()` → `ws.mv()`), but others have no Workspace equivalent (`io_create()`, `io_rmdir()`, `io_truncate()`), suggesting the IO layer is a lower-level FUSE interface that doesn't align perfectly with the Workspace API.

The real question is whether `io_read` should go through the Workspace cache and consistency system. Looking at the Workspace's `read()` method, it checks if the inode is dirty and pulls from cache if available...

Checking cache behavior...

The core issue is that the IO path completely bypasses consistency policies while the Workspace command API respects them, and the user is asking why they don't align.

The straightforward solution would be routing `io_read`, `io_stat`, and `io_readdir` through the workspace methods instead of hitting the backend directly. But that only works if the workspace's `read()` method itself applies consistency policy—which it currently doesn't. So the real fix needs to happen at the workspace level, ensuring consistency checks are applied uniformly across all read operations.

For FUSE specifically, applying consistency to range reads gets tricky since FUSE makes multiple calls for the same file, and the behavior depends on the policy—CACHED would cache the whole file and serve ranges from it, while ALWAYS would re-fetch on every range read. Given the complexity, I'll scope the initial work to extracting the consistency logic into a shared module and making `ws.read()` apply those checks, then route the io functions through workspace methods.

But I should step back and look at what the user actually asked: they wanted to know why the remote path doesn't share the consistency policy and whether we should move consistency to a separate module. From the diagram they approved, they're on board with both unifying the IO path and extracting consistency into its own module. So the practical plan is to focus on what's achievable—moving the policy logic out and ensuring dirty writes are visible through the workspace layer, then tackle the full consistency semantics for range reads as a follow-up.

Now I need to figure out what the backend actually supports—whether `readdir` and `ls` are interchangeable or if they have different semantics that would break the unified path.

For the immediate plan though, I'm moving `ConsistencyPolicy` to its own module in the workspace package, removing it from the current locations, and updating all the import sites since backward compatibility isn't required.

For routing the I/O operations, I need to decide how to handle `io_readdir` since the Workspace interface doesn't have cursor support like the backend does—I could add a `ws.readdir()` method or just leave it calling the backend directly, but I'm probably overthinking this part for now.

The real focus should be on the user's core goal: moving consistency into its own module and having the I/O path use it. The key insight is that `io_read` should call `ws.read()`, which in turn applies the consistency policy. This means every FUSE read could potentially fetch from the backend if consistency requires it, which might be expensive for large files, but that's the trade-off for correctness.

So my plan is to first extract the consistency logic into a dedicated module—pulling `ConsistencyPolicy` from types and the helper functions from the cache policy module.

Then I need to route the I/O operations through the workspace: `io_read` becomes `ws.read()`, `io_stat` becomes `ws.stat()`, and `io_readdir` stays as-is since the workspace doesn't support cursor-based listing.

I also need to be careful about imports—the test file and the public API both reference the old locations, so those need updating when I move the code.

Since we're not maintaining backward compatibility, I'll update all the imports directly rather than keeping re-exports: `mirage/__init__.py`, `workspace.py`, `mount.py`, and any other files that reference `ConsistencyPolicy` from its old location in `types.py`. I'm realizing ConsistencyPolicy is fundamentally a workspace concept, even though the CLI and server both use it for different purposes. Maybe the better approach is to keep the enum in `types.py` since it's referenced throughout the codebase, but move the actual logic like `is_fresh()` and `metadata_matches()` into `workspace/consistency.py` where it belongs with the workspace implementation. That way the type definition stays accessible everywhere while the logic lives next to the workspace code. The user probably wants me to consolidate all consistency-related functionality into a dedicated `workspace/consistency.py` module, so let me reconsider what's currently scattered across `workspace/cache/policy.py` and what should be pulled together there. and `metadata_matches`. Moving them to `workspace/consistency.py` makes sense as that's a better name. But should we also move `ConsistencyPolicy` enum there?

Looking at where `ConsistencyPolicy` is imported:
- `mirage/cli/config.py` - CLI needs it
- `mirage/cli/commands/workspace.py` - CLI needs it
- `mirage/cli/commands/command.py` - CLI needs it
- `mirage/server/fastapi/config.py` - API needs it
- `mirage/server/service/local.py` - Service needs it
- `mirage/workspace/workspace.py` - Workspace needs it
- `mirage/workspace/mount.py` - Workspace needs it
- `mirage/workspace/cache/policy.py` - Cache policy needs it

If I move `ConsistencyPolicy` to `workspace/consistency.py`, the CLI and server modules would need to import from workspace, which creates a cross-module dependency that feels backwards since workspace is core and CLI/server are consumers. Actually, that's probably fine. Alternatively, I could keep `ConsistencyPolicy` in `types.py` and just rename `workspace/cache/policy.py` to `workspace/consistency.py`. The simplest approach is probably to rename the policy file and consolidate the logic there while leaving the enum in types, though the user's feedback suggests they might want everything consolidated into a single consistency module.

Now for Task 2, I need to think through routing `io_read` through the Workspace. Currently it goes directly to the backend with no caching or consistency checks, but after the refactor it should go through `Workspace.read()` which would handle those concerns properly.

Looking at the current implementation, it checks the dirty cache but doesn't validate consistency. I could extend it to check the consistency policy before deciding whether to serve from cache or hit the backend, though that gets tricky with range reads since we'd need the full file to cache it properly.

For the FUSE use case though, this actually works well — files are typically opened once and read in chunks, so the first read would miss the cache, fetch the full file through `cat()` to populate it, then return the slice, and subsequent reads would hit the cache and just verify consistency before returning. I'll add updating `Workspace.read()` to apply consistency to the plan.

For `io_stat()`, it's straightforward — it'll go through `Workspace.stat()` which already checks the dirty tracker first, so no consistency issues there since stat operations are cheap anyway. Now looking at `io_readdir()`...

it currently bypasses the workspace layer entirely, but I could route it through the mount registry at least. The real question is whether to add cursor support to the workspace layer or keep readdir as-is — for now, the simplest approach is to have it resolve through the registry but acknowledge that directories don't need aggressive caching anyway.

For `io_create`, `io_unlink`, `io_mkdir`, and `io_rmdir`, I'm deciding whether to route each through the workspace layer or keep them calling the backend directly. `io_unlink` should go through `ws.rm()` since it handles cache invalidation, and `io_mkdir` should use `ws.mkdir()` for the writability check, but `io_create` doesn't have a workspace equivalent so I'll keep that as-is.

For `io_rename`, the workspace `mv()` method has different semantics around writability and staged files, so I need to be careful about routing there. And `io_truncate` has no workspace equivalent either.

The practical scope for this task narrows down to routing `io_read` through `ws.read()` with consistency applied, `io_stat` through `ws.stat()`, and `io_write` through the workspace layer. I'm realizing I should narrow my focus to what actually matters for consistency — routing `io_read` and `io_stat` through workspace methods to apply the consistency policy, while treating write/delete operations as a separate concern around cache invalidation and dirty tracking. So my plan is to move the `ConsistencyPolicy` enum and freshness logic into a dedicated module, then refactor the I/O operations to use workspace methods where it makes sense.

But I'm hitting a question about `Workspace.read()` — the current implementation checks the dirty tracker cache but doesn't populate it from the backend on a cache miss, so I need to think through whether that's the right behavior or if I should enhance it to fetch from the backend when needed. I'm completing the cache population logic and realizing there's a tradeoff here — fetching the entire file ensures consistency but sacrifices efficiency compared to range reads, which is the right call if correctness matters more than performance for large files.

For FUSE specifically, the kernel typically issues small sequential read requests, so caching the whole file on first access and serving subsequent chunks from cache would actually be ideal under a CACHED policy, whereas an ALWAYS policy would force a full fetch for every small read chunk, which would be inefficient. The user's design choice to route reads through the workspace for consistency enforcement is valid even if it has performance implications, and I should verify how `ws.read()` fits into this flow.

Looking at the current implementation, `ws.read()` calls `backend.read(bp, offset, size)` for range-based fetches, but if we switch to `backend.cat(bp)` with caching, we'd be loading entire files into memory even for small range reads on large files — a tradeoff that's actually consistent with the workspace's design philosophy of caching full file content. Let me write out the plan now.


Bash

ls /Users/agent/strukto/mirage/docs/plans 2>/dev/null || echo "no plans dir"


     1→from mirage.backend.local import LocalBackend, MemoryBackend
     2→from mirage.types import (ConsistencyPolicy, FileStat, Inode, MountMode,
     3→                          SyncPolicy, SyncResult)
     4→from mirage.workspace import Workspace
     5→
     6→__all__ = [
     7→    "Workspace",
     8→    "MemoryBackend",
     9→    "LocalBackend",
    10→    "ConsistencyPolicy",
    11→    "FileStat",
    12→    "Inode",
    13→    "MountMode",
    14→    "SyncPolicy",
    15→    "SyncResult",
    16→]
    17→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

from mirage.backend.local import LocalBackend, MemoryBackend
from mirage.types import (ConsistencyPolicy, FileStat, Inode, MountMode,
                          SyncPolicy, SyncResult)
from mirage.workspace import Workspace

__all__ = [
    "Workspace",
    "MemoryBackend",
    "LocalBackend",
    "ConsistencyPolicy",
    "FileStat",
    "Inode",
    "MountMode",
    "SyncPolicy",
    "SyncResult",
]


ls /Users/agent/strukto/mirage/docs/ 2>/dev/null || echo "no docs dir"


plans


Write

# Consistency Module Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Move `ConsistencyPolicy` and freshness logic into `mirage/workspace/consistency.py`, and route `io_read`/`io_stat` through Workspace so the IO path benefits from caching and consistency.

**Architecture:** Create `mirage/workspace/consistency.py` consolidating the enum and `is_fresh()` logic currently split between `types.py` and `workspace/cache/policy.py`. Then update `Workspace.read()` to apply consistency (like `cat()` but returning a slice), and route `LocalService.io_read` / `io_stat` through Workspace methods.

**Tech Stack:** Python, pydantic, pytest

---

### Task 1: Create workspace/consistency.py

**Files:**
- Create: `mirage/workspace/consistency.py`
- Modify: `mirage/types.py`
- Delete: `mirage/workspace/cache/policy.py`

**Step 1: Write the new module**

Create `mirage/workspace/consistency.py`:

```python
import time
from enum import Enum

from mirage.backend.base import BaseBackend
from mirage.types import FileStat
from mirage.workspace.cache.entry import CacheEntry

class ConsistencyPolicy(str, Enum):
    CACHED = "cached"
    LAZY = "lazy"
    TTL = "ttl"
    ALWAYS = "always"

def is_fresh(
    entry: CacheEntry,
    consistency: ConsistencyPolicy,
    ttl_seconds: int,
    backend: BaseBackend,
    backend_path: str,
) -> tuple[bool, FileStat | None]:
    if consistency == ConsistencyPolicy.CACHED:
        return True, None
    if consistency == ConsistencyPolicy.ALWAYS:
        return False, None
    if consistency == ConsistencyPolicy.TTL:
        if (time.monotonic() - entry.cached_at) < ttl_seconds:
            return True, None
    try:
        remote = backend.stat(backend_path)
    except FileNotFoundError:
        return False, None
    return metadata_matches(entry, remote), remote

def metadata_matches(entry: CacheEntry, remote: FileStat) -> bool:
    remote_etag = remote.extra.get("etag")
    if entry.etag and remote_etag:
        return entry.etag == remote_etag
    if entry.modified and remote.modified:
        return entry.modified == remote.modified
    if entry.size is not None and remote.size is not None:
        return entry.size == remote.size
    return False
```

**Step 2: Remove ConsistencyPolicy from types.py**

In `mirage/types.py`, delete the `ConsistencyPolicy` class (lines 49–53):
```python
class ConsistencyPolicy(str, Enum):
    CACHED = "cached"
    LAZY = "lazy"
    TTL = "ttl"
    ALWAYS = "always"
```

**Step 3: Update all imports — source files**

Files that import `ConsistencyPolicy` from `mirage.types` and need updating:

| File | Change |
|------|--------|
| `mirage/__init__.py` | `from mirage.workspace.consistency import ConsistencyPolicy` |
| `mirage/workspace/workspace.py` | replace `ConsistencyPolicy` in types import → `from mirage.workspace.consistency import ConsistencyPolicy, is_fresh` (also remove `from mirage.workspace.cache.policy import is_fresh`) |
| `mirage/workspace/mount.py` | `from mirage.workspace.consistency import ConsistencyPolicy` |
| `mirage/server/fastapi/config.py` | add `from mirage.workspace.consistency import ConsistencyPolicy` |
| `mirage/server/service/local.py` | add `from mirage.workspace.consistency import ConsistencyPolicy` |
| `mirage/cli/commands/workspace.py` | `from mirage.workspace.consistency import ConsistencyPolicy` |
| `mirage/cli/config.py` | `from mirage.workspace.consistency import ConsistencyPolicy` |
| `mirage/cli/commands/command.py` | `from mirage.workspace.consistency import ConsistencyPolicy` |

For each file that previously had `ConsistencyPolicy` in its `from mirage.types import (...)` line, remove it from that line and add a new import from `mirage.workspace.consistency`.

**Step 4: Update test imports**

| File | Change |
|------|--------|
| `tests/workspace/test_workspace.py` | `from mirage.workspace.consistency import ConsistencyPolicy` |
| `tests/workspace/cache/test_policy.py` | `from mirage.workspace.consistency import metadata_matches` |
| `tests/cli/test_app.py` | `from mirage.workspace.consistency import ConsistencyPolicy` (inside the `if TYPE_CHECKING` block or at top) |
| `tests/cli/test_config.py` | `from mirage.workspace.consistency import ConsistencyPolicy` |

**Step 5: Delete the old policy file**

```bash
git rm mirage/workspace/cache/policy.py
```

**Step 6: Run tests**

```bash
uv run pytest tests/workspace/ tests/cli/ -q
```

Expected: all pass.

**Step 7: Commit**

```bash
git add mirage/workspace/consistency.py mirage/workspace/cache/policy.py mirage/types.py mirage/__init__.py mirage/workspace/workspace.py mirage/workspace/mount.py mirage/server/fastapi/config.py mirage/server/service/local.py mirage/cli/commands/workspace.py mirage/cli/config.py mirage/cli/commands/command.py tests/workspace/test_workspace.py tests/workspace/cache/test_policy.py tests/cli/test_app.py tests/cli/test_config.py
git commit -m "refactor: consolidate ConsistencyPolicy and freshness logic into workspace/consistency.py"
```

---

### Task 2: Apply consistency in Workspace.read()

Currently `Workspace.read()` does a raw `backend.read(offset, size)` on cache miss. Update it to populate the full-file cache (like `cat()`) and apply consistency, then return the requested slice.

**Files:**
- Modify: `mirage/workspace/workspace.py:210-217`
- Test: `tests/workspace/test_workspace.py`

**Step 1: Write the failing test**

In `tests/workspace/test_workspace.py`, add:

```python
def test_read_respects_consistency_always(tmp_path):
    backend = MemoryBackend()
    backend.put("/file.txt", b"hello world")
    ws = Workspace(
        {"/": (backend, MountMode.READ, ConsistencyPolicy.ALWAYS)},
    )
    _ = ws.cat("/file.txt")  # populate cache
    backend.put("/file.txt", b"updated content")
    result = ws.read("/file.txt", 0, 7)
    assert result == b"updated"  # ALWAYS must bypass cache
```

**Step 2: Run test to verify it fails**

```bash
uv run pytest tests/workspace/test_workspace.py::test_read_respects_consistency_always -v
```

Expected: FAIL — `read()` returns stale cached data.

**Step 3: Update Workspace.read()**

Replace the current `read()` method in `mirage/workspace/workspace.py`:

```python
def read(self, path: str, offset: int, size: int) -> bytes:
    inode = self._tracker.get(path)
    if inode and inode.dirty:
        entry = self._cache.get(path)
        if entry is not None:
            return entry.data[offset:offset + size]

    entry = self._cache.get(path)
    remote_stat = None
    if entry is not None:
        consistency = self._registry.mount_for(path).consistency
        backend, bp, _ = self._registry.resolve(path)
        fresh, remote_stat = is_fresh(entry, consistency,
                                      self._ttl_seconds, backend, bp)
        if fresh:
            return entry.data[offset:offset + size]
    else:
        backend, bp, _ = self._registry.resolve(path)

    data = backend.cat(bp)
    if remote_stat is None:
        remote_stat = backend.stat(bp)
    self._cache.put(path,
                    data,
                    etag=remote_stat.extra.get("etag"),
                    modified=remote_stat.modified,
                    size=remote_stat.size)
    self._tracker.on_read(path, data, self._mount_prefix(path))
    return data[offset:offset + size]
```

**Step 4: Run test to verify it passes**

```bash
uv run pytest tests/workspace/test_workspace.py::test_read_respects_consistency_always -v
```

Expected: PASS.

**Step 5: Run full test suite**

```bash
uv run pytest tests/workspace/ -q
```

Expected: all pass.

**Step 6: Commit**

```bash
git add mirage/workspace/workspace.py tests/workspace/test_workspace.py
git commit -m "feat: apply consistency policy in Workspace.read()"
```

---

### Task 3: Route io_read and io_stat through Workspace

**Files:**
- Modify: `mirage/server/service/local.py:296-309`
- Test: `tests/server/service/test_local.py`

**Step 1: Write the failing test**

In `tests/server/service/test_local.py`, add:

```python
def test_io_read_uses_workspace_cache(svc):
    ws_id = svc.create_workspace({
        "mounts": {"/": "memory"},
        "consistency": "cached",
    })
    session = svc._sessions[ws_id]
    session.ws.write("/file.txt", b"hello world")
    session.ws.sync()
    data = svc.io_read(ws_id, "/file.txt", 0, 5)
    assert data == b"hello"

def test_io_stat_uses_workspace(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    session = svc._sessions[ws_id]
    session.ws.write("/file.txt", b"hello")
    st = svc.io_stat(ws_id, "/file.txt")
    assert st.size == 5
```

**Step 2: Run tests to verify they fail**

```bash
uv run pytest tests/server/service/test_local.py::test_io_read_uses_workspace_cache tests/server/service/test_local.py::test_io_stat_uses_workspace -v
```

Expected: FAIL.

**Step 3: Update io_read and io_stat in LocalService**

In `mirage/server/service/local.py`, replace:

```python
def io_read(self, ws_id: str, path: str, offset: int, size: int) -> bytes:
    session = self._get_session(ws_id)
    backend, backend_path, _ = session.ws._registry.resolve(path)
    return backend.read(backend_path, offset, size)
```

With:

```python
def io_read(self, ws_id: str, path: str, offset: int, size: int) -> bytes:
    session = self._get_session(ws_id)
    return session.ws.read(path, offset, size)
```

And replace:

```python
def io_stat(self, ws_id: str, path: str) -> FileStat:
    session = self._get_session(ws_id)
    backend, backend_path, _ = session.ws._registry.resolve(path)
    return backend.stat(backend_path)
```

With:

```python
def io_stat(self, ws_id: str, path: str) -> FileStat:
    session = self._get_session(ws_id)
    return session.ws.stat(path)
```

**Step 4: Run tests to verify they pass**

```bash
uv run pytest tests/server/service/test_local.py -v
```

Expected: all pass.

**Step 5: Run full test suite**

```bash
uv run pytest -q
```

Expected: all pass.

**Step 6: Commit**

```bash
git add mirage/server/service/local.py tests/server/service/test_local.py
git commit -m "feat: route io_read and io_stat through Workspace for consistent caching"
```


File created successfully at: /Users/agent/strukto/mirage/docs/plans/2026-03-17-consistency-module.md

# Consistency Module Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Move `ConsistencyPolicy` and freshness logic into `mirage/workspace/consistency.py`, and route `io_read`/`io_stat` through Workspace so the IO path benefits from caching and consistency.

**Architecture:** Create `mirage/workspace/consistency.py` consolidating the enum and `is_fresh()` logic currently split between `types.py` and `workspace/cache/policy.py`. Then update `Workspace.read()` to apply consistency (like `cat()` but returning a slice), and route `LocalService.io_read` / `io_stat` through Workspace methods.

**Tech Stack:** Python, pydantic, pytest

---

### Task 1: Create workspace/consistency.py

**Files:**
- Create: `mirage/workspace/consistency.py`
- Modify: `mirage/types.py`
- Delete: `mirage/workspace/cache/policy.py`

**Step 1: Write the new module**

Create `mirage/workspace/consistency.py`:

```python
import time
from enum import Enum

from mirage.backend.base import BaseBackend
from mirage.types import FileStat
from mirage.workspace.cache.entry import CacheEntry

class ConsistencyPolicy(str, Enum):
    CACHED = "cached"
    LAZY = "lazy"
    TTL = "ttl"
    ALWAYS = "always"

def is_fresh(
    entry: CacheEntry,
    consistency: ConsistencyPolicy,
    ttl_seconds: int,
    backend: BaseBackend,
    backend_path: str,
) -> tuple[bool, FileStat | None]:
    if consistency == ConsistencyPolicy.CACHED:
        return True, None
    if consistency == ConsistencyPolicy.ALWAYS:
        return False, None
    if consistency == ConsistencyPolicy.TTL:
        if (time.monotonic() - entry.cached_at) < ttl_seconds:
            return True, None
    try:
        remote = backend.stat(backend_path)
    except FileNotFoundError:
        return False, None
    return metadata_matches(entry, remote), remote

def metadata_matches(entry: CacheEntry, remote: FileStat) -> bool:
    remote_etag = remote.extra.get("etag")
    if entry.etag and remote_etag:
        return entry.etag == remote_etag
    if entry.modified and remote.modified:
        return entry.modified == remote.modified
    if entry.size is not None and remote.size is not None:
        return entry.size == remote.size
    return False
```

**Step 2: Remove ConsistencyPolicy from types.py**

In `mirage/types.py`, delete the `ConsistencyPolicy` class (lines 49–53):
```python
class ConsistencyPolicy(str, Enum):
    CACHED = "cached"
    LAZY = "lazy"
    TTL = "ttl"
    ALWAYS = "always"
```

**Step 3: Update all imports — source files**

Files that import `ConsistencyPolicy` from `mirage.types` and need updating:

| File | Change |
|------|--------|
| `mirage/__init__.py` | `from mirage.workspace.consistency import ConsistencyPolicy` |
| `mirage/workspace/workspace.py` | replace `ConsistencyPolicy` in types import → `from mirage.workspace.consistency import ConsistencyPolicy, is_fresh` (also remove `from mirage.workspace.cache.policy import is_fresh`) |
| `mirage/workspace/mount.py` | `from mirage.workspace.consistency import ConsistencyPolicy` |
| `mirage/server/fastapi/config.py` | add `from mirage.workspace.consistency import ConsistencyPolicy` |
| `mirage/server/service/local.py` | add `from mirage.workspace.consistency import ConsistencyPolicy` |
| `mirage/cli/commands/workspace.py` | `from mirage.workspace.consistency import ConsistencyPolicy` |
| `mirage/cli/config.py` | `from mirage.workspace.consistency import ConsistencyPolicy` |
| `mirage/cli/commands/command.py` | `from mirage.workspace.consistency import ConsistencyPolicy` |

For each file that previously had `ConsistencyPolicy` in its `from mirage.types import (...)` line, remove it from that line and add a new import from `mirage.workspace.consistency`.

**Step 4: Update test imports**

| File | Change |
|------|--------|
| `tests/workspace/test_workspace.py` | `from mirage.workspace.consistency import ConsistencyPolicy` |
| `tests/workspace/cache/test_policy.py` | `from mirage.workspace.consistency import metadata_matches` |
| `tests/cli/test_app.py` | `from mirage.workspace.consistency import ConsistencyPolicy` (inside the `if TYPE_CHECKING` block or at top) |
| `tests/cli/test_config.py` | `from mirage.workspace.consistency import ConsistencyPolicy` |

**Step 5: Delete the old policy file**

```bash
git rm mirage/workspace/cache/policy.py
```

**Step 6: Run tests**

```bash
uv run pytest tests/workspace/ tests/cli/ -q
```

Expected: all pass.

**Step 7: Commit**

```bash
git add mirage/workspace/consistency.py mirage/workspace/cache/policy.py mirage/types.py mirage/__init__.py mirage/workspace/workspace.py mirage/workspace/mount.py mirage/server/fastapi/config.py mirage/server/service/local.py mirage/cli/commands/workspace.py mirage/cli/config.py mirage/cli/commands/command.py tests/workspace/test_workspace.py tests/workspace/cache/test_policy.py tests/cli/test_app.py tests/cli/test_config.py
git commit -m "refactor: consolidate ConsistencyPolicy and freshness logic into workspace/consistency.py"
```

---

### Task 2: Apply consistency in Workspace.read()

Currently `Workspace.read()` does a raw `backend.read(offset, size)` on cache miss. Update it to populate the full-file cache (like `cat()`) and apply consistency, then return the requested slice.

**Files:**
- Modify: `mirage/workspace/workspace.py:210-217`
- Test: `tests/workspace/test_workspace.py`

**Step 1: Write the failing test**

In `tests/workspace/test_workspace.py`, add:

```python
def test_read_respects_consistency_always(tmp_path):
    backend = MemoryBackend()
    backend.put("/file.txt", b"hello world")
    ws = Workspace(
        {"/": (backend, MountMode.READ, ConsistencyPolicy.ALWAYS)},
    )
    _ = ws.cat("/file.txt")  # populate cache
    backend.put("/file.txt", b"updated content")
    result = ws.read("/file.txt", 0, 7)
    assert result == b"updated"  # ALWAYS must bypass cache
```

**Step 2: Run test to verify it fails**

```bash
uv run pytest tests/workspace/test_workspace.py::test_read_respects_consistency_always -v
```

Expected: FAIL — `read()` returns stale cached data.

**Step 3: Update Workspace.read()**

Replace the current `read()` method in `mirage/workspace/workspace.py`:

```python
def read(self, path: str, offset: int, size: int) -> bytes:
    inode = self._tracker.get(path)
    if inode and inode.dirty:
        entry = self._cache.get(path)
        if entry is not None:
            return entry.data[offset:offset + size]

    entry = self._cache.get(path)
    remote_stat = None
    if entry is not None:
        consistency = self._registry.mount_for(path).consistency
        backend, bp, _ = self._registry.resolve(path)
        fresh, remote_stat = is_fresh(entry, consistency,
                                      self._ttl_seconds, backend, bp)
        if fresh:
            return entry.data[offset:offset + size]
    else:
        backend, bp, _ = self._registry.resolve(path)

    data = backend.cat(bp)
    if remote_stat is None:
        remote_stat = backend.stat(bp)
    self._cache.put(path,
                    data,
                    etag=remote_stat.extra.get("etag"),
                    modified=remote_stat.modified,
                    size=remote_stat.size)
    self._tracker.on_read(path, data, self._mount_prefix(path))
    return data[offset:offset + size]
```

**Step 4: Run test to verify it passes**

```bash
uv run pytest tests/workspace/test_workspace.py::test_read_respects_consistency_always -v
```

Expected: PASS.

**Step 5: Run full test suite**

```bash
uv run pytest tests/workspace/ -q
```

Expected: all pass.

**Step 6: Commit**

```bash
git add mirage/workspace/workspace.py tests/workspace/test_workspace.py
git commit -m "feat: apply consistency policy in Workspace.read()"
```

---

### Task 3: Route io_read and io_stat through Workspace

**Files:**
- Modify: `mirage/server/service/local.py:296-309`
- Test: `tests/server/service/test_local.py`

**Step 1: Write the failing test**

In `tests/server/service/test_local.py`, add:

```python
def test_io_read_uses_workspace_cache(svc):
    ws_id = svc.create_workspace({
        "mounts": {"/": "memory"},
        "consistency": "cached",
    })
    session = svc._sessions[ws_id]
    session.ws.write("/file.txt", b"hello world")
    session.ws.sync()
    data = svc.io_read(ws_id, "/file.txt", 0, 5)
    assert data == b"hello"

def test_io_stat_uses_workspace(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    session = svc._sessions[ws_id]
    session.ws.write("/file.txt", b"hello")
    st = svc.io_stat(ws_id, "/file.txt")
    assert st.size == 5
```

**Step 2: Run tests to verify they fail**

```bash
uv run pytest tests/server/service/test_local.py::test_io_read_uses_workspace_cache tests/server/service/test_local.py::test_io_stat_uses_workspace -v
```

Expected: FAIL.

**Step 3: Update io_read and io_stat in LocalService**

In `mirage/server/service/local.py`, replace:

```python
def io_read(self, ws_id: str, path: str, offset: int, size: int) -> bytes:
    session = self._get_session(ws_id)
    backend, backend_path, _ = session.ws._registry.resolve(path)
    return backend.read(backend_path, offset, size)
```

With:

```python
def io_read(self, ws_id: str, path: str, offset: int, size: int) -> bytes:
    session = self._get_session(ws_id)
    return session.ws.read(path, offset, size)
```

And replace:

```python
def io_stat(self, ws_id: str, path: str) -> FileStat:
    session = self._get_session(ws_id)
    backend, backend_path, _ = session.ws._registry.resolve(path)
    return backend.stat(backend_path)
```

With:

```python
def io_stat(self, ws_id: str, path: str) -> FileStat:
    session = self._get_session(ws_id)
    return session.ws.stat(path)
```

**Step 4: Run tests to verify they pass**

```bash
uv run pytest tests/server/service/test_local.py -v
```

Expected: all pass.

**Step 5: Run full test suite**

```bash
uv run pytest -q
```

Expected: all pass.

**Step 6: Commit**

```bash
git add mirage/server/service/local.py tests/server/service/test_local.py
git commit -m "feat: route io_read and io_stat through Workspace for consistent caching"
```


Plan saved to [docs/plans/2026-03-17-consistency-module.md](docs/plans/2026-03-17-consistency-module.md).

**3 tasks:**

1. **Create `workspace/consistency.py`** — consolidate `ConsistencyPolicy` enum (from `types.py`) + `is_fresh()` / `metadata_matches()` (from `cache/policy.py`), delete old file, update 8 source + 4 test import sites

2. **Apply consistency in `Workspace.read()`** — currently does raw `backend.read()` on cache miss; update to populate full-file cache + apply `is_fresh()` like `cat()` does, then return the slice

3. **Route `io_read` / `io_stat` through Workspace** — two one-liners in `local.py`, gives FUSE/remote reads the cache + consistency behavior

**Two execution options:**

1. **Subagent-Driven (this session)** — I dispatch a fresh subagent per task, review between tasks
2. **Parallel Session (separate)** — Open new session with the plan file, batch execution with checkpoints

Which approach?


<ide_selection>The user selected the lines 12 to 12 from /Users/agent/strukto/mirage/mirage/fuse/fs.py:
fuse

This may or may not be related to the current task.</ide_selection>

subagent


The user wants to use the subagent-driven development approach. I need to invoke the `superpowers:subagent-driven-development` skill.


Launching skill: superpowers:subagent-driven-development


Base directory for this skill: /Users/agent/.claude/plugins/cache/superpowers-marketplace/superpowers/4.0.3/skills/subagent-driven-development

# Subagent-Driven Development

Execute plan by dispatching fresh subagent per task, with two-stage review after each: spec compliance review first, then code quality review.

**Core principle:** Fresh subagent per task + two-stage review (spec then quality) = high quality, fast iteration

## When to Use

```dot
digraph when_to_use {
    "Have implementation plan?" [shape=diamond];
    "Tasks mostly independent?" [shape=diamond];
    "Stay in this session?" [shape=diamond];
    "subagent-driven-development" [shape=box];
    "executing-plans" [shape=box];
    "Manual execution or brainstorm first" [shape=box];

    "Have implementation plan?" -> "Tasks mostly independent?" [label="yes"];
    "Have implementation plan?" -> "Manual execution or brainstorm first" [label="no"];
    "Tasks mostly independent?" -> "Stay in this session?" [label="yes"];
    "Tasks mostly independent?" -> "Manual execution or brainstorm first" [label="no - tightly coupled"];
    "Stay in this session?" -> "subagent-driven-development" [label="yes"];
    "Stay in this session?" -> "executing-plans" [label="no - parallel session"];
}
```

**vs. Executing Plans (parallel session):**
- Same session (no context switch)
- Fresh subagent per task (no context pollution)
- Two-stage review after each task: spec compliance first, then code quality
- Faster iteration (no human-in-loop between tasks)

## The Process

```dot
digraph process {
    rankdir=TB;

    subgraph cluster_per_task {
        label="Per Task";
        "Dispatch implementer subagent (./implementer-prompt.md)" [shape=box];
        "Implementer subagent asks questions?" [shape=diamond];
        "Answer questions, provide context" [shape=box];
        "Implementer subagent implements, tests, commits, self-reviews" [shape=box];
        "Dispatch spec reviewer subagent (./spec-reviewer-prompt.md)" [shape=box];
        "Spec reviewer subagent confirms code matches spec?" [shape=diamond];
        "Implementer subagent fixes spec gaps" [shape=box];
        "Dispatch code quality reviewer subagent (./code-quality-reviewer-prompt.md)" [shape=box];
        "Code quality reviewer subagent approves?" [shape=diamond];
        "Implementer subagent fixes quality issues" [shape=box];
        "Mark task complete in TodoWrite" [shape=box];
    }

    "Read plan, extract all tasks with full text, note context, create TodoWrite" [shape=box];
    "More tasks remain?" [shape=diamond];
    "Dispatch final code reviewer subagent for entire implementation" [shape=box];
    "Use superpowers:finishing-a-development-branch" [shape=box style=filled fillcolor=lightgreen];

    "Read plan, extract all tasks with full text, note context, create TodoWrite" -> "Dispatch implementer subagent (./implementer-prompt.md)";
    "Dispatch implementer subagent (./implementer-prompt.md)" -> "Implementer subagent asks questions?";
    "Implementer subagent asks questions?" -> "Answer questions, provide context" [label="yes"];
    "Answer questions, provide context" -> "Dispatch implementer subagent (./implementer-prompt.md)";
    "Implementer subagent asks questions?" -> "Implementer subagent implements, tests, commits, self-reviews" [label="no"];
    "Implementer subagent implements, tests, commits, self-reviews" -> "Dispatch spec reviewer subagent (./spec-reviewer-prompt.md)";
    "Dispatch spec reviewer subagent (./spec-reviewer-prompt.md)" -> "Spec reviewer subagent confirms code matches spec?";
    "Spec reviewer subagent confirms code matches spec?" -> "Implementer subagent fixes spec gaps" [label="no"];
    "Implementer subagent fixes spec gaps" -> "Dispatch spec reviewer subagent (./spec-reviewer-prompt.md)" [label="re-review"];
    "Spec reviewer subagent confirms code matches spec?" -> "Dispatch code quality reviewer subagent (./code-quality-reviewer-prompt.md)" [label="yes"];
    "Dispatch code quality reviewer subagent (./code-quality-reviewer-prompt.md)" -> "Code quality reviewer subagent approves?";
    "Code quality reviewer subagent approves?" -> "Implementer subagent fixes quality issues" [label="no"];
    "Implementer subagent fixes quality issues" -> "Dispatch code quality reviewer subagent (./code-quality-reviewer-prompt.md)" [label="re-review"];
    "Code quality reviewer subagent approves?" -> "Mark task complete in TodoWrite" [label="yes"];
    "Mark task complete in TodoWrite" -> "More tasks remain?";
    "More tasks remain?" -> "Dispatch implementer subagent (./implementer-prompt.md)" [label="yes"];
    "More tasks remain?" -> "Dispatch final code reviewer subagent for entire implementation" [label="no"];
    "Dispatch final code reviewer subagent for entire implementation" -> "Use superpowers:finishing-a-development-branch";
}
```

## Prompt Templates

- `./implementer-prompt.md` - Dispatch implementer subagent
- `./spec-reviewer-prompt.md` - Dispatch spec compliance reviewer subagent
- `./code-quality-reviewer-prompt.md` - Dispatch code quality reviewer subagent

## Example Workflow

```
You: I'm using Subagent-Driven Development to execute this plan.

[Read plan file once: docs/plans/feature-plan.md]
[Extract all 5 tasks with full text and context]
[Create TodoWrite with all tasks]

Task 1: Hook installation script

[Get Task 1 text and context (already extracted)]
[Dispatch implementation subagent with full task text + context]

Implementer: "Before I begin - should the hook be installed at user or system level?"

You: "User level (~/.config/superpowers/hooks/)"

Implementer: "Got it. Implementing now..."
[Later] Implementer:
  - Implemented install-hook command
  - Added tests, 5/5 passing
  - Self-review: Found I missed --force flag, added it
  - Committed

[Dispatch spec compliance reviewer]
Spec reviewer: ✅ Spec compliant - all requirements met, nothing extra

[Get git SHAs, dispatch code quality reviewer]
Code reviewer: Strengths: Good test coverage, clean. Issues: None. Approved.

[Mark Task 1 complete]

Task 2: Recovery modes

[Get Task 2 text and context (already extracted)]
[Dispatch implementation subagent with full task text + context]

Implementer: [No questions, proceeds]
Implementer:
  - Added verify/repair modes
  - 8/8 tests passing
  - Self-review: All good
  - Committed

[Dispatch spec compliance reviewer]
Spec reviewer: ❌ Issues:
  - Missing: Progress reporting (spec says "report every 100 items")
  - Extra: Added --json flag (not requested)

[Implementer fixes issues]
Implementer: Removed --json flag, added progress reporting

[Spec reviewer reviews again]
Spec reviewer: ✅ Spec compliant now

[Dispatch code quality reviewer]
Code reviewer: Strengths: Solid. Issues (Important): Magic number (100)

[Implementer fixes]
Implementer: Extracted PROGRESS_INTERVAL constant

[Code reviewer reviews again]
Code reviewer: ✅ Approved

[Mark Task 2 complete]

...

[After all tasks]
[Dispatch final code-reviewer]
Final reviewer: All requirements met, ready to merge

Done!
```

## Advantages

**vs. Manual execution:**
- Subagents follow TDD naturally
- Fresh context per task (no confusion)
- Parallel-safe (subagents don't interfere)
- Subagent can ask questions (before AND during work)

**vs. Executing Plans:**
- Same session (no handoff)
- Continuous progress (no waiting)
- Review checkpoints automatic

**Efficiency gains:**
- No file reading overhead (controller provides full text)
- Controller curates exactly what context is needed
- Subagent gets complete information upfront
- Questions surfaced before work begins (not after)

**Quality gates:**
- Self-review catches issues before handoff
- Two-stage review: spec compliance, then code quality
- Review loops ensure fixes actually work
- Spec compliance prevents over/under-building
- Code quality ensures implementation is well-built

**Cost:**
- More subagent invocations (implementer + 2 reviewers per task)
- Controller does more prep work (extracting all tasks upfront)
- Review loops add iterations
- But catches issues early (cheaper than debugging later)

## Red Flags

**Never:**
- Skip reviews (spec compliance OR code quality)
- Proceed with unfixed issues
- Dispatch multiple implementation subagents in parallel (conflicts)
- Make subagent read plan file (provide full text instead)
- Skip scene-setting context (subagent needs to understand where task fits)
- Ignore subagent questions (answer before letting them proceed)
- Accept "close enough" on spec compliance (spec reviewer found issues = not done)
- Skip review loops (reviewer found issues = implementer fixes = review again)
- Let implementer self-review replace actual review (both are needed)
- **Start code quality review before spec compliance is ✅** (wrong order)
- Move to next task while either review has open issues

**If subagent asks questions:**
- Answer clearly and completely
- Provide additional context if needed
- Don't rush them into implementation

**If reviewer finds issues:**
- Implementer (same subagent) fixes them
- Reviewer reviews again
- Repeat until approved
- Don't skip the re-review

**If subagent fails task:**
- Dispatch fix subagent with specific instructions
- Don't try to fix manually (context pollution)

## Integration

**Required workflow skills:**
- **superpowers:writing-plans** - Creates the plan this skill executes
- **superpowers:requesting-code-review** - Code review template for reviewer subagents
- **superpowers:finishing-a-development-branch** - Complete development after all tasks

**Subagents should use:**
- **superpowers:test-driven-development** - Subagents follow TDD for each task

**Alternative workflow:**
- **superpowers:executing-plans** - Use for parallel session instead of same-session execution


I need to follow the subagent-driven development process:

1. Read the plan file (already done - I created it)
2. Extract all tasks
3. Create TodoWrite with all tasks
4. Dispatch implementation subagent for Task 1
5. Dispatch spec reviewer
6. Dispatch code quality reviewer
7. Repeat for each task

Let me extract the tasks from the plan and create the todo list.

Tasks from the plan:
1. Create `workspace/consistency.py` - move ConsistencyPolicy + is_fresh/metadata_matches
2. Apply consistency in `Workspace.read()`
3. Route `io_read` and `io_stat` through Workspace

Now I'm setting up the TodoWrite with these tasks and preparing to dispatch the implementation subagent for the first one.


I'm using Subagent-Driven Development to execute this plan.