diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,18 @@
 
 ## Unreleased
 
+## 0.2.0.0 - 2026-07-05
+
+### Changed
+
+- **BREAKING** Compiled state JSON now stores a structural shape fingerprint plus
+  parameters. Legacy bare parameter arrays are rejected and must be re-encoded.
+- `decodeCompiledOnto` now fails with a shape-mismatch error when saved state is
+  loaded onto the wrong compiled program template.
+- RAG now stores retrieved context in serializable node parameters, so RAG state
+  survives compiled-state encode/decode.
+- Refreshed the internal `shikumi` bound for the `0.3` series.
+
 ## 0.1.1.0 - 2026-06-28
 
 ### Changed
diff --git a/shikumi-compile.cabal b/shikumi-compile.cabal
--- a/shikumi-compile.cabal
+++ b/shikumi-compile.cabal
@@ -1,6 +1,6 @@
 cabal-version:   3.4
 name:            shikumi-compile
-version:         0.1.1.0
+version:         0.2.0.0
 synopsis:        The compiler layer for shikumi LM programs (EP-9)
 category:        AI
 description:
@@ -54,7 +54,7 @@
     , effectful
     , generic-lens  ^>=2.2
     , lens          ^>=5.3
-    , shikumi       ^>=0.2.0.0
+    , shikumi       ^>=0.3.0.0
     , text          ^>=2.1
 
 test-suite shikumi-compile-test
@@ -69,14 +69,14 @@
 
   build-depends:
     , aeson
-    , baikai           >=0.2      && <0.3
+    , baikai           >=0.3      && <0.4
     , base
     , bytestring
     , effectful
     , generic-lens
     , lens
-    , shikumi          ^>=0.2.0.0
-    , shikumi-compile  ^>=0.1.1.0
+    , shikumi          ^>=0.3.0.0
+    , shikumi-compile  ^>=0.2.0.0
     , tasty
     , tasty-hunit
     , text
diff --git a/src/Shikumi/Compile/ChainOfThought.hs b/src/Shikumi/Compile/ChainOfThought.hs
--- a/src/Shikumi/Compile/ChainOfThought.hs
+++ b/src/Shikumi/Compile/ChainOfThought.hs
@@ -31,6 +31,11 @@
 -- carries demos, they must be chain-of-thought-shaped (include a @reasoning@ field)
 -- to decode under the augmented output; otherwise compile chain-of-thought before
 -- few-shot.
+--
+-- Serialized state from this compiler must be decoded onto the same compiled
+-- shape, not onto the plain base program. In practice, re-apply
+-- 'chainOfThoughtCompiler' to the base template before calling
+-- 'Shikumi.Compile.Serialize.decodeCompiledOnto'.
 module Shikumi.Compile.ChainOfThought
   ( chainOfThoughtCompiler,
   )
@@ -57,6 +62,8 @@
 
 -- | Rewrite every 'Predict' node into its chain-of-thought form, recursing through
 -- every composite/combinator node so nodes nested arbitrarily deep are reached.
+-- Saved state produced by this compiler should be loaded onto this compiled shape,
+-- not onto the original base program.
 chainOfThoughtCompiler :: Compiler
 chainOfThoughtCompiler = Compiler cot
 
@@ -73,6 +80,6 @@
 cot (Retry n p) = Retry n (cot p)
 cot (RetryWhen ok n p) = RetryWhen ok n (cot p)
 cot (Validate v p) = Validate v (cot p)
-cot (MajorityVote k sched p) = MajorityVote k sched (cot p)
+cot (MajorityVote k sched r p) = MajorityVote k sched r (cot p)
 cot (Ensemble ps reduce) = Ensemble (map cot ps) reduce
 cot (Embed f) = Embed f -- an agent node has no 'Predict' to rewrite; pass through
diff --git a/src/Shikumi/Compile/RAG.hs b/src/Shikumi/Compile/RAG.hs
--- a/src/Shikumi/Compile/RAG.hs
+++ b/src/Shikumi/Compile/RAG.hs
@@ -16,27 +16,30 @@
 -- query-independent of the actual program input; wiring true per-input retrieval
 -- awaits an EP-4 embed node and is left as a TODO.
 --
--- The injection is a /structural/ rewrite (like "Shikumi.Compile.ChainOfThought"):
--- each @Predict sig ps@ leaf becomes @Predict (setInstruction (base <> context)
--- sig) ps@, so the original task instruction is preserved /and/ the retrieved
--- context is added (it appears in the rendered system prompt). The node's existing
--- 'Shikumi.Program.Params' are untouched; the same @instructionOverride@-precedence
--- caveat as chain-of-thought applies (an override set on a node replaces the whole
--- instruction at run time, context included), so apply 'rag' before a zero-shot
--- instruction if both are wanted.
+-- The injection is a serializable parameter rewrite: each @Predict sig ps@ leaf
+-- keeps its signature and receives an @instructionOverride@ containing the
+-- effective instruction (an existing override if present, otherwise the
+-- signature's base instruction) plus the retrieved context. This means RAG state
+-- survives 'Shikumi.Compile.Serialize.encodeCompiled' /
+-- 'Shikumi.Compile.Serialize.decodeCompiledOnto'. Composition order still matters:
+-- applying 'zeroShot' after 'rag' replaces the whole override and drops the
+-- context, while applying 'rag' after 'zeroShot' appends context to the zero-shot
+-- instruction.
 module Shikumi.Compile.RAG
   ( rag,
     formatPassages,
   )
 where
 
+import Data.Maybe (fromMaybe)
 import Data.Text (Text)
 import Data.Text qualified as T
 import Effectful (runPureEff)
 import Shikumi.Compile.Retriever (Passage (..), Retriever (..))
 import Shikumi.Compile.Types (Compiler (..))
 import Shikumi.Program
-  ( Program
+  ( Params (..),
+    Program
       ( Compose,
         Embed,
         Ensemble,
@@ -50,7 +53,7 @@
         Validate
       ),
   )
-import Shikumi.Signature (getInstruction, setInstruction)
+import Shikumi.Signature (getInstruction)
 
 -- | Install retrieved context at every node. Retrieval happens once, now, against
 -- @query@ (the documented compile-time fallback); the formatted passages are then
@@ -69,15 +72,17 @@
   "Use the following retrieved context to answer:\n"
     <> T.unlines ["- " <> text p | p <- ps]
 
--- | Append the context block to every node's signature instruction, recursing
--- through every composite/combinator node. A no-op when @ctx@ is empty.
+-- | Append the context block to every node's effective instruction, storing the
+-- result in serializable node parameters. A no-op when @ctx@ is empty.
 install :: Text -> Program i o -> Program i o
 install ctx
   | T.null ctx = id
   | otherwise = go
   where
     go :: forall i o. Program i o -> Program i o
-    go (Predict sig ps) = Predict (setInstruction (getInstruction sig <> "\n\n" <> ctx) sig) ps
+    go (Predict sig ps) =
+      let base = fromMaybe (getInstruction sig) (instructionOverride ps)
+       in Predict sig ps {instructionOverride = Just (base <> "\n\n" <> ctx)}
     go (Compose a b) = Compose (go a) (go b)
     go (FMap k p) = FMap k (go p)
     go (Map w p) = Map w (go p)
@@ -85,6 +90,6 @@
     go (Retry n p) = Retry n (go p)
     go (RetryWhen ok n p) = RetryWhen ok n (go p)
     go (Validate v p) = Validate v (go p)
-    go (MajorityVote k sched p) = MajorityVote k sched (go p)
+    go (MajorityVote k sched r p) = MajorityVote k sched r (go p)
     go (Ensemble ps reduce) = Ensemble (map go ps) reduce
     go (Embed f) = Embed f -- an agent node has no 'Predict' to rewrite; pass through
diff --git a/src/Shikumi/Compile/Serialize.hs b/src/Shikumi/Compile/Serialize.hs
--- a/src/Shikumi/Compile/Serialize.hs
+++ b/src/Shikumi/Compile/Serialize.hs
@@ -1,57 +1,97 @@
 -- | Serialization of a 'CompiledProgram'.
 --
--- __Parameter-state, not whole-structure.__ A @Program@'s structure cannot be
--- serialized in general (its @FMap@ nodes hold opaque functions and its GADT type
--- indices are not reflected at runtime). What /can/ be saved — and is exactly what
--- the optimizer (EP-10) and CLI (EP-12) need — is each node's tunable
--- 'Shikumi.Program.Params', against a known program /template/. This mirrors DSPy's
+-- __Parameter-state plus shape fingerprint, not whole-structure.__ A @Program@'s
+-- executable structure cannot be serialized in general (its @FMap@ nodes hold
+-- opaque functions and its GADT type indices are not reflected at runtime). What
+-- /can/ be saved — and is exactly what the optimizer (EP-10) and CLI (EP-12)
+-- need — is each node's tunable 'Shikumi.Program.Params', plus a closure-free
+-- structural fingerprint used to reject the wrong /template/. This mirrors DSPy's
 -- @dump_state@/@load_state@: dump the parameters, load them back onto a program you
 -- still hold as code.
 --
 -- This reuses EP-4's already-provided parameter interface verbatim:
--- 'Shikumi.Program.programParams' (the ordered @[Params]@ vector, in @foldParams@
--- order) and 'Shikumi.Program.setProgramParams' (re-apply a vector onto a matching
--- program, with a length-mismatch guard). So this module is a thin JSON wrapper —
--- the node-order contract and the count check are EP-4's, not re-invented here.
+-- 'Shikumi.Program.programShape', 'Shikumi.Program.programParams' (the ordered
+-- @[Params]@ vector, in @foldParams@ order), and
+-- 'Shikumi.Program.setProgramParams' (re-apply a vector onto a matching program,
+-- with a length-mismatch guard). So this module is a thin JSON wrapper — the shape
+-- and node-order contracts are EP-4's, not re-invented here.
 module Shikumi.Compile.Serialize
   ( encodeCompiled,
     decodeCompiledOnto,
   )
 where
 
-import Data.Aeson (eitherDecode, encode)
+import Data.Aeson (FromJSON, ToJSON, eitherDecode, encode)
 import Data.ByteString.Lazy (ByteString)
+import GHC.Generics (Generic)
 import Shikumi.Compile.Types (CompiledProgram (..))
 import Shikumi.Program
   ( Params,
     Program,
+    ProgramShape,
     ProgramShapeError (..),
     programParams,
+    programShape,
     setProgramParams,
   )
 
--- | Emit a compiled program's ordered parameter vector as JSON (a JSON array of
--- @Params@, in 'Shikumi.Program.foldParams' order).
+-- | The persisted form of a compiled program: a closure-free structural
+-- fingerprint plus the ordered parameter vector.
+data CompiledState = CompiledState
+  { shape :: !ProgramShape,
+    params :: ![Params]
+  }
+  deriving stock (Generic)
+
+instance ToJSON CompiledState
+
+instance FromJSON CompiledState
+
+-- | Emit a compiled program's shape fingerprint and ordered parameter vector as
+-- JSON.
 encodeCompiled :: CompiledProgram i o -> ByteString
-encodeCompiled (CompiledProgram p) = encode (programParams p)
+encodeCompiled (CompiledProgram p) = encode (CompiledState (programShape p) (programParams p))
 
--- | Re-apply a saved parameter vector onto a structural /template/ (the base
--- program, held as code), in node order. Fails with a descriptive message if the
--- JSON is malformed or if its node count does not match the template — which would
--- mean the template is not the program the parameters were saved from. The signature
--- mirrors DSPy's @load_state(program)@.
+-- | Re-apply saved state onto a structural /template/ (the program, held as code),
+-- in node order. Fails with a descriptive message if the JSON is malformed, if the
+-- shape fingerprint does not match the template, or if the parameter count is
+-- inconsistent with the template. The template must have the same compiled shape
+-- that produced the saved state: for structural compilers such as
+-- 'Shikumi.Compile.ChainOfThought.chainOfThoughtCompiler', re-apply the same
+-- compiler to the base program before loading. The signature mirrors DSPy's
+-- @load_state(program)@.
 decodeCompiledOnto ::
   Program i o ->
   ByteString ->
   Either String (CompiledProgram i o)
 decodeCompiledOnto template bs = do
-  ps <- eitherDecode bs :: Either String [Params]
-  case setProgramParams ps template of
-    Left (ParamCountMismatch (expected, got)) ->
+  st <-
+    either
+      (Left . decodeError)
+      Right
+      (eitherDecode bs :: Either String CompiledState)
+  let templateShape = programShape template
+  if shape st /= templateShape
+    then
       Left
-        ( "shikumi-compile: parameter count mismatch — the template has "
-            <> show expected
-            <> " node(s) but the saved state has "
-            <> show got
+        ( "shikumi-compile: shape mismatch — the saved state was produced by a structurally different program. saved shape: "
+            <> show (shape st)
+            <> "; template shape: "
+            <> show templateShape
         )
-    Right prog -> Right (CompiledProgram prog)
+    else applyParams (params st)
+  where
+    decodeError e =
+      "shikumi-compile: expected a compiled-state envelope with shape and params; "
+        <> "legacy bare parameter arrays are not supported and must be re-encoded. Aeson error: "
+        <> e
+    applyParams ps =
+      case setProgramParams ps template of
+        Left (ParamCountMismatch (expected, got)) ->
+          Left
+            ( "shikumi-compile: parameter count mismatch — the template has "
+                <> show expected
+                <> " node(s) but the saved state has "
+                <> show got
+            )
+        Right prog -> Right (CompiledProgram prog)
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -3,6 +3,7 @@
 -- the nodes/ (a pure @foldParams@ read) — never merely "a type was added".
 module Main (main) where
 
+import Data.Aeson qualified as Aeson
 import Data.Text (Text)
 import Data.Text qualified as T
 import Effectful (runPureEff)
@@ -210,10 +211,44 @@
             (origPrompts, _) <- runWithCapture [answerResponse] (compiledProgram compiled) q
             (rtPrompts, _) <- runWithCapture [answerResponse] (compiledProgram roundTripped) q
             rtPrompts @?= origPrompts,
-      testCase "decoding onto a wrong-shaped template returns Left" $ do
+      testCase "decoding onto a wrong-shaped template returns a shape Left" $ do
         -- a 2-node payload onto a 1-node template
         let bytes = encodeCompiled (compile (zeroShot "x") qaPipeline)
         case decodeCompiledOnto qaBase bytes of
-          Left _ -> pure ()
-          Right _ -> assertBool "expected a count-mismatch Left" False
+          Left e -> assertBool "shape mismatch mentioned" (contains "shape mismatch" (T.pack e))
+          Right _ -> assertBool "expected a shape-mismatch Left" False,
+      testCase "legacy bare Params arrays are rejected with a re-encode hint" $ do
+        let bytes = Aeson.encode [Params (Just "legacy instruction") []]
+        case decodeCompiledOnto qaBase bytes of
+          Left e -> do
+            assertBool "legacy format mentioned" (contains "legacy bare parameter arrays" (T.pack e))
+            assertBool "re-encode hint mentioned" (contains "re-encoded" (T.pack e))
+          Right _ -> assertBool "expected a legacy-format Left" False,
+      testCase "RAG context survives encode/decode" $ do
+        let compiled = compile (rag (inMemoryRetriever 1 corpus) "shikumi mechanism") qaBase
+            bytes = encodeCompiled compiled
+        case decodeCompiledOnto qaBase bytes of
+          Left e -> assertBool ("unexpected decode failure: " <> e) False
+          Right roundTripped -> do
+            (origPrompts, _) <- runWithCapture [answerResponse] (compiledProgram compiled) q
+            (rtPrompts, _) <- runWithCapture [answerResponse] (compiledProgram roundTripped) q
+            rtPrompts @?= origPrompts
+            case rtPrompts of
+              [p] -> assertBool "target passage present after round-trip" (contains "mechanism behind how a system works" p)
+              _ -> assertBool "one prompt" False,
+      testCase "CoT state does not decode onto the base template" $ do
+        let compiled = compile chainOfThoughtCompiler qaBase
+            bytes = encodeCompiled compiled
+        case decodeCompiledOnto qaBase bytes of
+          Left e -> assertBool "shape mismatch mentioned" (contains "shape mismatch" (T.pack e))
+          Right _ -> assertBool "expected a shape-mismatch Left" False,
+      testCase "CoT round-trips onto the CoT template" $ do
+        let compiled = compile chainOfThoughtCompiler qaBase
+            bytes = encodeCompiled compiled
+        case decodeCompiledOnto (compiledProgram compiled) bytes of
+          Left e -> assertBool ("unexpected decode failure: " <> e) False
+          Right roundTripped -> do
+            (origPrompts, _) <- runWithCapture [cotAnswerResponse] (compiledProgram compiled) q
+            (rtPrompts, _) <- runWithCapture [cotAnswerResponse] (compiledProgram roundTripped) q
+            rtPrompts @?= origPrompts
     ]
