diff --git a/.gitignore b/.gitignore
index e88f367..d5bb5f2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,9 +11,11 @@
*.sln.docstates
AGENTS.md
+.opencode/
+.code-review-graph/
+
ref/
docfx/
-.opencode/
NUL
# User-specific files (MonoDevelop/Xamarin Studio)
diff --git a/docs/notes/shader_pipeline_architecture.md b/docs/notes/shader_pipeline_architecture.md
new file mode 100644
index 0000000..16d0707
--- /dev/null
+++ b/docs/notes/shader_pipeline_architecture.md
@@ -0,0 +1,457 @@
+# Shader Pipeline Architecture — Proposed Design
+
+> Presented as a design walkthrough. Take what's useful, ignore what doesn't fit your vision.
+
+---
+
+## 1. System Topology
+
+The first decision: **where does each responsibility live?**
+
+```mermaid
+graph TB
+ subgraph EditorProcess["Ghost.Editor Process"]
+ FW["FileWatcher
(monitors .ghost DSL files)"]
+ AR["AssetRegistry
(GUID ↔ file path mapping)"]
+ EP["Editor UI
(status bar, material inspector)"]
+ end
+
+ subgraph CompilerProcess["GhostShaderServer Process"]
+ DSL["DSL Compiler
(Ghost DSL → HLSL)"]
+ DXC["DXC Compiler
(HLSL → DXIL bytecode)"]
+ MW["Manifest Writer
(updates variant → hash mapping)"]
+ end
+
+ subgraph RuntimeGraphics["Ghost.Graphics (Runtime)"]
+ SL["ShaderLibrary
(reads bytecode from cache)"]
+ PL["PipelineLibrary
(PSO creation + double-buffer)"]
+ RGC["RenderGraphContext
(binds PSO per draw call)"]
+ BR["IShaderCompilationBridge
(interface, 2 methods)"]
+ end
+
+ subgraph SharedDisk["Shared Disk (ShaderCache/)"]
+ MF["ShaderManifest.bin
(GUID+variant → content hash)"]
+ BC["Bytecode Files
(content-addressed .bin blobs)"]
+ end
+
+ FW -- "file changed event" --> AR
+ AR -- "GUID + file path
(named pipe)" --> CompilerProcess
+ DSL --> DXC
+ DXC -- "bytecode bytes" --> MW
+ MW -- "write blob" --> BC
+ MW -- "update entry" --> MF
+
+ SL -- "read blob" --> BC
+ SL -- "read mapping" --> MF
+ BR -- "status query
(named pipe)" --> CompilerProcess
+
+ EP -- "poll status" --> BR
+
+ style CompilerProcess fill:#1a1a2e,stroke:#e94560,color:#eee
+ style EditorProcess fill:#1a1a2e,stroke:#0f3460,color:#eee
+ style RuntimeGraphics fill:#1a1a2e,stroke:#16213e,color:#eee
+ style SharedDisk fill:#0f3460,stroke:#533483,color:#eee
+```
+
+### Why a Separate Process?
+
+| Concern | In-process compiler | Separate process |
+|---------|-------------------|------------------|
+| DXC crash | Editor dies | Server restarts, editor lives |
+| DXC memory leak | Editor bloats over time | Kill & restart server periodically |
+| Parallelism | Threads compete with editor UI | Fully independent CPU budget |
+| Build pipeline reuse | Need separate build-time path | Same server binary, different mode |
+| Complexity | Lower (one process) | Higher (IPC needed) |
+
+> [!TIP]
+> If the separate process feels like overkill for your current stage, **start with in-process behind the `IShaderCompilationBridge` interface**, then swap the implementation to out-of-process later. The interface is the same either way.
+
+---
+
+## 2. Data Model — The Manifest
+
+This is the most important data structure in the entire system. It decouples **identity** from **content**.
+
+```mermaid
+graph LR
+ subgraph ShaderAsset["Shader Asset (on disk)"]
+ GUID["Asset GUID
e.g. 7f3a-...-c82b
stable forever"]
+ SRC["Source Code
.ghost DSL file
changes on edit"]
+ end
+
+ subgraph Manifest["ShaderManifest"]
+ E1["Entry:
GUID=7f3a | Pass=0 | Variant=0x00
→ ContentHash=0xABCD"]
+ E2["Entry:
GUID=7f3a | Pass=0 | Variant=0x01
→ ContentHash=0x1234"]
+ E3["Entry:
GUID=7f3a | Pass=1 | Variant=0x00
→ ContentHash=0x5678"]
+ end
+
+ subgraph Cache["ShaderCache/ (content addressed)"]
+ B1["AB/shader_cache_ABCD...bin"]
+ B2["12/shader_cache_1234...bin"]
+ B3["56/shader_cache_5678...bin"]
+ end
+
+ GUID --> E1
+ GUID --> E2
+ GUID --> E3
+ E1 --> B1
+ E2 --> B2
+ E3 --> B3
+
+ style ShaderAsset fill:#16213e,stroke:#0f3460,color:#eee
+ style Manifest fill:#1a1a2e,stroke:#e94560,color:#eee
+ style Cache fill:#0f3460,stroke:#533483,color:#eee
+```
+
+### Manifest Entry Structure
+
+```
+ManifestKey = Hash(AssetGUID + PassIndex + VariantKeywordMask)
+ManifestValue = ContentHash (= Hash of compiled bytecode)
+```
+
+- **ManifestKey** is *structurally* derived — same shader, same pass, same keywords = same key, regardless of source changes.
+- **ContentHash** is *content-derived* — changes every time the source code changes.
+- When source changes: the ManifestKey stays the same, but the ContentHash it points to gets updated.
+
+> [!IMPORTANT]
+> The `Shader` struct in runtime only needs to know the **AssetGUID**. It never stores or cares about content hashes. The `ShaderLibrary` uses the manifest to translate `(GUID, Pass, Variant) → ContentHash → File`.
+
+---
+
+## 3. Compilation Flow — What Happens When You Save a Shader
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant FileSystem
+ participant Editor as Ghost.Editor
+ participant Server as ShaderServer
+ participant Cache as ShaderCache/
+
+ User->>FileSystem: Save "water.ghost"
+ FileSystem-->>Editor: FileWatcher event
+
+ Editor->>Editor: Lookup GUID for "water.ghost"
via AssetRegistry
+ Editor->>Server: CompileRequest {
guid: 7f3a-...,
filePath: "water.ghost",
defines: [...],
platform: D3D12
}
+
+ Note over Server: Mark status = Compiling
for this GUID
+
+ Server->>Server: Read .ghost DSL file
+ Server->>Server: DSL Compiler: DSL → HLSL
+
+ alt DSL has syntax errors
+ Server->>Server: Mark status = Error
+ Server-->>Editor: CompileResult {
status: Error,
errors: [...]
}
+ Editor->>Editor: Show errors in
console/inspector
+ else DSL is valid
+ Server->>Server: For each (pass, variant):
DXC Compile HLSL → DXIL
+
+ alt Any DXC error
+ Server->>Server: Mark status = Error
+ Server-->>Editor: CompileResult {
status: Error,
errors: [...]
}
+ else All variants compiled
+ Server->>Cache: Write bytecode blobs
(content-addressed)
+ Server->>Cache: Update manifest entries:
(GUID+pass+variant) → new hash
+ Server->>Server: Mark status = Ready
+ Server-->>Editor: CompileResult {
status: Ready,
variantCount: N
}
+ Editor->>Editor: Show ✓ in status bar
+ end
+ end
+```
+
+### Key Design Decision: Compile All Variants Upfront?
+
+**No.** Only compile variants that are *currently referenced* by materials in the scene. The editor knows which materials reference which shader (via AssetRegistry), and which keyword combinations those materials use. Ship only what's needed.
+
+For the edit-time hot-reload, you really only need the specific variants the viewport is currently rendering. The full permutation set is a build-time concern.
+
+---
+
+## 4. Runtime PSO Resolution — The Frame-by-Frame Flow
+
+This is where most of the complexity lives. Here's what `SetActiveMaterial` does every frame:
+
+```mermaid
+flowchart TD
+ A["SetActiveMaterial(material)"] --> B["Compute ManifestKey
= f(shader.GUID, passIndex, variantMask)"]
+ B --> C{"PipelineLibrary
has PSO for
ManifestKey?"}
+
+ C -- "Yes (cache hit)" --> D["Bind existing PSO
to command buffer"]
+ D --> Z["Done ✓"]
+
+ C -- "No (cache miss)" --> E{"ShaderLibrary
has bytecode for
ManifestKey?"}
+
+ E -- "Yes (manifest hit)" --> F["Read bytecode
from cache file"]
+ F --> G["Create PSO from bytecode"]
+ G --> H["Store in PipelineLibrary"]
+ H --> D
+
+ E -- "No (manifest miss)" --> I{"Is this Editor
or Runtime?"}
+
+ I -- "Runtime
(shipped game)" --> J["Bind Fallback
ERROR PSO ⚠️"]
+ J --> K["Log error:
missing shader"]
+ K --> Z
+
+ I -- "Editor" --> L{"Query Bridge:
IsCompiling?"}
+
+ L -- "Status = Compiling" --> M["Bind OLD PSO
(keep previous frame's shader)"]
+ M --> Z
+
+ L -- "Status = Error" --> N["Bind ERROR PSO
(magenta)"]
+ N --> Z
+
+ L -- "Status = Ready" --> O["The manifest was just updated.
Re-read manifest entry."]
+ O --> F
+
+ L -- "Status = NotAvailable" --> J
+
+ style A fill:#533483,stroke:#e94560,color:#eee
+ style D fill:#16213e,stroke:#0f3460,color:#eee
+ style J fill:#e94560,stroke:#1a1a2e,color:#eee
+ style M fill:#0f3460,stroke:#533483,color:#eee
+ style N fill:#e94560,stroke:#1a1a2e,color:#eee
+ style Z fill:#16213e,stroke:#16213e,color:#eee
+```
+
+### The "Keep Old PSO" Strategy — How It Works Mechanically
+
+This is the part that makes the UX feel seamless. The trick:
+
+```mermaid
+graph LR
+ subgraph PipelineLibrary
+ direction TB
+ K["ManifestKey 0xAABB"]
+ K --> CURRENT["current: PSO_v2 ✓
(what we render with)"]
+ K --> PENDING["pending: null
(set during recompilation)"]
+ end
+
+ style CURRENT fill:#16213e,stroke:#0f3460,color:#eee
+ style PENDING fill:#1a1a2e,stroke:#e94560,color:#eee
+```
+
+When shader source changes and recompilation starts:
+
+```mermaid
+graph LR
+ subgraph PipelineLibrary_During["During Recompilation"]
+ direction TB
+ K2["ManifestKey 0xAABB"]
+ K2 --> CURRENT2["current: PSO_v2 ✓
(still rendering with this)"]
+ K2 --> PENDING2["pending: COMPILING
(server is working...)"]
+ end
+
+ style CURRENT2 fill:#16213e,stroke:#0f3460,color:#eee
+ style PENDING2 fill:#e94560,stroke:#1a1a2e,color:#eee
+```
+
+When recompilation finishes successfully:
+
+```mermaid
+graph LR
+ subgraph PipelineLibrary_After["After Swap"]
+ direction TB
+ K3["ManifestKey 0xAABB"]
+ K3 --> CURRENT3["current: PSO_v3 ✓
(new shader, rendering now)"]
+ K3 --> PENDING3["pending: null
(swap complete)"]
+ end
+
+ style CURRENT3 fill:#16213e,stroke:#0f3460,color:#eee
+ style PENDING3 fill:#1a1a2e,stroke:#533483,color:#eee
+```
+
+> [!NOTE]
+> The old `PSO_v2` is **not immediately destroyed**. It stays alive until the GPU is done with any in-flight frames referencing it (tracked by fence value). This prevents use-after-free on the GPU timeline.
+
+---
+
+## 5. Hot-Reload Sequence — The Complete Picture
+
+Everything combined into one timeline:
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant Editor
+ participant Server as ShaderServer
+ participant Cache as Disk Cache
+ participant Runtime as RenderGraphContext
+ participant GPU
+
+ Note over Runtime,GPU: Frame N: Rendering with PSO_v2
+
+ User->>Editor: Edit & save "water.ghost"
+ Editor->>Server: CompileRequest(guid=7f3a)
+ Server->>Server: status[7f3a] = Compiling
+
+ Note over Runtime,GPU: Frame N+1
+ Runtime->>Runtime: SetActiveMaterial()
+ Runtime->>Runtime: ManifestKey lookup → old hash still there
+ Runtime->>Runtime: PipelineLibrary has PSO → use it
+ Note over Runtime: Still rendering with PSO_v2
(user sees no flicker)
+
+ Note over Server: Background: DSL→HLSL→DXC...
+
+ Note over Runtime,GPU: Frame N+2, N+3, ...
+ Runtime->>Runtime: Same as N+1, no visible change
+
+ Server->>Cache: Write new bytecode files
+ Server->>Cache: Update manifest:
key(7f3a,0,0) → new_hash
+ Server->>Server: status[7f3a] = Ready
+
+ Note over Runtime,GPU: Frame N+K (compilation done)
+ Runtime->>Runtime: SetActiveMaterial()
+ Runtime->>Runtime: Manifest read → NEW content hash
+ Runtime->>Runtime: PipelineLibrary miss for new hash
+ Runtime->>Cache: Read new bytecode
+ Runtime->>GPU: Create PSO_v3
+ Runtime->>Runtime: PipelineLibrary: current=PSO_v3
+ Runtime->>Runtime: Bind PSO_v3
+
+ Note over Runtime,GPU: Frame N+K+1: Rendering with PSO_v3 ✓
+
+ Runtime->>Runtime: Defer release PSO_v2
(after GPU fence)
+```
+
+### What the User Sees
+
+| Frame | Viewport | Status Bar |
+|-------|----------|------------|
+| N | Water renders normally | — |
+| N+1 | Water renders normally (old shader) | 🔄 Compiling water.ghost... |
+| N+2 | Water renders normally (old shader) | 🔄 Compiling water.ghost... |
+| N+K | Water renders with new shader | ✅ water.ghost compiled (2 variants) |
+
+**Zero flicker. Zero blocking. Zero pink frames.**
+
+---
+
+## 6. How the Manifest Key Replaces Your Current Hash Problem
+
+Here's a before/after of your `Shader` struct:
+
+### Current Design (problematic)
+```mermaid
+graph TD
+ subgraph Current["Current: Hash = f(source code)"]
+ S1["Shader struct"] --> P1["Pass[0].Key = 0xABCD
derived from source hash"]
+ P1 --> V1["ShaderVariantKey = f(0xABCD, keywords)"]
+ V1 --> PK1["PipelineKey = f(variant, rtv, dsv)"]
+ PK1 --> PSO1["PSO lookup in PipelineLibrary"]
+
+ EDIT["User edits source"] -.-> STALE["Pass[0].Key is now STALE ❌
Still 0xABCD, but source changed"]
+ STALE -.-> WRONG["Looks up OLD bytecode
or worse, the old PSO"]
+ end
+
+ style STALE fill:#e94560,stroke:#1a1a2e,color:#eee
+ style WRONG fill:#e94560,stroke:#1a1a2e,color:#eee
+```
+
+### Proposed Design (stable)
+```mermaid
+graph TD
+ subgraph Proposed["Proposed: Key = f(GUID, pass index)"]
+ S2["Shader struct
assetGUID = 7f3a-..."] --> P2["Pass[0]: index=0
no source hash stored"]
+ P2 --> MK["ManifestKey = f(7f3a, 0, keywords)"]
+ MK --> MANIFEST["Manifest Lookup
→ ContentHash = 0x9999"]
+ MANIFEST --> SL2["ShaderLibrary
→ read 99/shader_cache_9999.bin"]
+ SL2 --> PSO2["Create or get PSO"]
+
+ EDIT2["User edits source"] -.-> RECOMP["Server recompiles
→ new ContentHash = 0xBBBB"]
+ RECOMP -.-> MUPD["Manifest updated:
same key → 0xBBBB"]
+ MUPD -.-> NEXT["Next frame: manifest read
picks up 0xBBBB automatically"]
+ end
+
+ style RECOMP fill:#0f3460,stroke:#533483,color:#eee
+ style MUPD fill:#0f3460,stroke:#533483,color:#eee
+ style NEXT fill:#16213e,stroke:#0f3460,color:#eee
+```
+
+> [!IMPORTANT]
+> **The `Shader` struct never changes.** No unload, no recreate, no generation counter bump. The manifest is the *only* mutable state, and it lives on disk, outside the runtime's object graph. The runtime just reads it.
+
+---
+
+## 7. The Two Interfaces That Make This Work
+
+Only two abstractions are needed in `Ghost.Graphics` to support the full pipeline:
+
+```mermaid
+classDiagram
+ class IShaderCompilationBridge {
+ <>
+ +TryGetBytecode(manifestKey: ulong, out bytecode: ReadOnlyMemory~byte~) bool
+ +IsCompiling(manifestKey: ulong) bool
+ }
+
+ class RuntimeStub {
+ +TryGetBytecode() → always from ShaderLibrary cache
+ +IsCompiling() → always false
+ }
+
+ class EditorImplementation {
+ -NamedPipeClient _serverConnection
+ +TryGetBytecode() → check manifest, read cache
+ +IsCompiling() → query server status
+ }
+
+ IShaderCompilationBridge <|.. RuntimeStub : "Shipped game"
+ IShaderCompilationBridge <|.. EditorImplementation : "Editor mode"
+
+ class ShaderLibrary {
+ -string _cacheDirectory
+ +GetCache(contentHash: ulong) Result~bytes~
+ +GetFromManifest(manifestKey: ulong) Result~bytes~
+ }
+
+ EditorImplementation --> ShaderLibrary : reads cache
+ RuntimeStub --> ShaderLibrary : reads cache
+```
+
+> [!TIP]
+> `RenderGraphContext` doesn't talk to the bridge directly. It talks to `ShaderLibrary`, which internally consults the bridge on cache miss. This keeps the rendering code clean — it never sees compilation status. It just gets bytecode or it doesn't.
+
+---
+
+## 8. Build Pipeline — How Shipped Games Work
+
+For completeness, here's how the same architecture handles builds:
+
+```mermaid
+flowchart LR
+ subgraph BuildTime["Build Pipeline"]
+ SCAN["Scan all materials
in scenes/assets"] --> COLLECT["Collect all referenced
(GUID, pass, variant) tuples"]
+ COLLECT --> COMPILE["Compile all variants
via ShaderServer"]
+ COMPILE --> PACK["Package manifest +
bytecode blobs into
game data archive"]
+ end
+
+ subgraph ShippedGame["Runtime (shipped game)"]
+ LOAD["Load manifest +
bytecode from archive"] --> LIB["ShaderLibrary
(read-only, all variants pre-cached)"]
+ LIB --> MISS{"Cache miss?"}
+ MISS -- "Never
(if build is correct)" --> OK["Create PSO normally"]
+ MISS -- "Somehow yes
(bug or modding)" --> ERR["Error PSO
+ log warning"]
+ end
+
+ BuildTime --> ShippedGame
+
+ style BuildTime fill:#1a1a2e,stroke:#0f3460,color:#eee
+ style ShippedGame fill:#16213e,stroke:#533483,color:#eee
+```
+
+The beauty: **the same `ShaderLibrary` and `PipelineLibrary` code runs in both editor and shipped game**. The only difference is whether `IShaderCompilationBridge` is the editor implementation or the runtime stub.
+
+---
+
+## Summary of Key Design Decisions
+
+| # | Decision | Rationale |
+|---|----------|-----------|
+| 1 | Stable GUID identity, not content hash | Shader struct never needs recreation on edit |
+| 2 | Content-addressed cache | Deduplication, easy invalidation, git-friendly |
+| 3 | Manifest as the bridge | Decouples identity from compiled output cleanly |
+| 4 | Keep old PSO during recompile | Zero flicker, seamless UX |
+| 5 | Separate compiler process | Crash isolation, independent resource budget |
+| 6 | Two-method interface in runtime | Minimal coupling, easy to stub for shipped game |
+| 7 | Deferred PSO release via fence | Prevents GPU use-after-free |
+| 8 | Same code path for editor + shipped | Fewer bugs, one pipeline to maintain |
diff --git a/docs/superpowers/plans/2026-03-28-dock-layout.md b/docs/superpowers/plans/2026-03-28-dock-layout.md
deleted file mode 100644
index f10dafa..0000000
--- a/docs/superpowers/plans/2026-03-28-dock-layout.md
+++ /dev/null
@@ -1,669 +0,0 @@
-# DockLayout Implementation Plan
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-
-**Goal:** Create a fully-featured, dynamically splittable, multi-window docking layout system using WinUI 3.
-
-**Architecture:** A C# data model (node tree) drives the recursive generation of XAML `Grid` and `NavigationTabView` controls. Drag and drop events mutate the node tree, and the UI automatically reflects the changes.
-
-**Tech Stack:** C#, WinUI 3, CommunityToolkit.Mvvm (for ObservableObject).
-
----
-
-### Task 1: Create Core Data Models
-
-**Files:**
-- Create: `src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockNode.cs`
-- Create: `src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockGroupNode.cs`
-- Create: `src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockPanelNode.cs`
-
-- [ ] **Step 1: Write `DockNode` base class**
-
-```csharp
-using CommunityToolkit.Mvvm.ComponentModel;
-
-namespace Ghost.Editor.Core.Controls.Internal.Docking;
-
-public abstract partial class DockNode : ObservableObject
-{
- [ObservableProperty]
- private DockGroupNode? _parent;
-}
-```
-
-- [ ] **Step 2: Write `DockGroupNode` class**
-
-```csharp
-using System.Collections.ObjectModel;
-using Microsoft.UI.Xaml.Controls;
-
-namespace Ghost.Editor.Core.Controls.Internal.Docking;
-
-public partial class DockGroupNode : DockNode
-{
- [ObservableProperty]
- private Orientation _orientation = Orientation.Horizontal;
-
- public ObservableCollection Children { get; } = new();
-
- public void AddChild(DockNode node)
- {
- node.Parent = this;
- Children.Add(node);
- }
-
- public void RemoveChild(DockNode node)
- {
- if (Children.Remove(node))
- {
- node.Parent = null;
- }
- }
-}
-```
-
-- [ ] **Step 3: Write `DockPanelNode` class**
-
-```csharp
-using System.Collections.ObjectModel;
-using CommunityToolkit.Mvvm.ComponentModel;
-
-namespace Ghost.Editor.Core.Controls.Internal.Docking;
-
-public partial class DockPanelNode : DockNode
-{
- public ObservableCollection