diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Changelog for agentic
 
+## 0.2.0.1 - 2026-10-01
+
+* The README now appears on the Hackage package page.
+
 ## 0.2.0.0 - 2026-10-01
 
 First release of the v2 design.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,514 @@
+# haskell-agentic
+
+[![Hackage](https://img.shields.io/hackage/v/agentic.svg)](https://hackage.haskell.org/package/agentic)
+[![CI](https://github.com/drshade/haskell-agentic/actions/workflows/haskell-ci.yml/badge.svg)](https://github.com/drshade/haskell-agentic/actions/workflows/haskell-ci.yml)
+
+I wrote the first version of this a while back, when the best tool we had for
+getting structured data out of an LLM was "pls respond in JSON". It worked -
+sort of. The model providers have since caught up (strict structured outputs
+just work now), so v2 throws away the clever-but-fragile bits and keeps the idea
+I still think is right: an agentic workflow should be a typed value you can
+compose, look at, and only then run.
+
+So this is a small Haskell library for exactly that. Typed steps, mixing LLMs
+(Claude, OpenAI) with [Jev](https://docs.typesafe.ai) for fast, calibrated
+judgements - and you can draw the whole flow before you spend a single token.
+
+## Packages
+
+It's split into a handful of packages, so the core stays tiny and you only pull
+in the providers you actually use:
+
+| Package | What it's for |
+|---|---|
+| `agentic` | flows, contracts, questions, the runtime and the interpreter (depends only on `base` and `text`) |
+| `agentic-jev` | Jev as System One |
+| `agentic-anthropic` | Claude as System Two (or System One) |
+| `agentic-openai` | OpenAI as System Two (or System One) |
+| `agentic-io` | concurrency, recording and replay, and `.env` loading |
+| `agentic-aeson` | shared by the providers: JSON conversions and strict JSON Schema |
+| `examples` | the examples in this README (not published) |
+
+All the examples below live in `examples/` and run against the real models -
+`cabal run dino`, `cabal run tictactoe`, `cabal run review` and
+`cabal run kitchensink`. Copy `.env.example` to `.env` and fill in your keys
+first.
+
+## The big idea
+
+A workflow is an `Agentic m i o` - a typed description of how to get from an `i`
+to an `o`, running in some effect `m`. I call these flows. You build them out of
+steps and click them together with the normal `Arrow` combinators (`>>>`, `&&&`,
+`|||`), sort of like lego. Nothing runs until you hand the flow to an
+interpreter.
+
+There are four kinds of step:
+
+| Step | What it does | Returns |
+|---|---|---|
+| `draft` | an LLM writes a value, optionally using tools along the way | any `Contract o` |
+| `judge` | Jev answers typed questions about the input | answers with calibrated probabilities |
+| `act` | plain code, with any effect `m` | whatever it returns |
+| `arr` | a pure function - glue between steps | whatever it returns |
+
+Because a flow is just data, you can `describe` it (print it, or draw it) before
+you spend a single token. And because the interpreter is separate, the same flow
+runs against real providers, a mock, or a recording. Same code. No changes.
+
+Under the hood there are two types - `Step` for the leaves, which do the actual
+work, and `Agentic` for the structure, which wires them together:
+
+```haskell
+data Step m i o where
+  Pass  ::                                                        Step m i i  -- returnA
+  Wrap  :: (i -> o)                                            -> Step m i o  -- Left, Right
+  Arr   :: (i -> o)                                            -> Step m i o
+  Act   :: (i -> m o)                                          -> Step m i o
+  Draft :: Codec i -> Codec o -> Instruction -> [Tool m]       -> Step m i o
+  Judge :: Codec i -> Questions o                              -> Step m i o
+
+data Agentic m i o where
+  Step   :: Step m i o                     -> Agentic m i o
+  Seq    :: Agentic m a b -> Agentic m b c -> Agentic m a c        -- >>>
+  Fanout :: Agentic m a b -> Agentic m a c -> Agentic m a (b, c)   -- &&&
+  Split  :: Agentic m a b -> Agentic m c d -> Agentic m (a, c) (b, d)  -- ***
+  First  :: Agentic m a b                  -> Agentic m (a, c) (b, c)
+  Choose :: Agentic m a c -> Agentic m b c -> Agentic m (Either a b) c  -- |||
+  Each   :: Agentic m a b                  -> Agentic m [a] [b]
+  Repeat :: (a -> Bool) -> Agentic m a a   -> Agentic m a a        -- repeatUntil
+  Noted  :: Note -> Agentic m i o          -> Agentic m i o        -- named, note
+```
+
+The `Arrow` and `ArrowChoice` instances build these constructors directly,
+otherwise `describe` would see a tangle of `arr swap` instead of "these run side
+by side".
+
+There's deliberately no `ArrowApply` and no `Monad`. I know, I know. But either
+one would let a flow pick its next step from a runtime value, and then you
+couldn't describe it without running it - which kinda defeats the point.
+
+## Jokes
+
+Let's start with the obvious one:
+
+```haskell
+data Joke = Joke { genre :: Text, setup :: Text, punchline :: Text }
+  deriving (Generic, Show, Contract)
+
+ghci> run (draft @Joke "a joke please") ()
+Joke {genre = "Dad joke", setup = "Why did the scarecrow win an award?", punchline = "Because he was outstanding in his field."}
+```
+
+The step's input becomes the model's context, so you never have to inject it
+yourself:
+
+```haskell
+data BetterJoke
+  = DadJoke    { setup :: Text, punchline :: Text }
+  | OneLiner   { line :: Text }
+  | KnockKnock { whosThere :: Text, punchline :: Text }
+  deriving (Generic, Show, Contract)
+
+ghci> run (draft @BetterJoke "convert this joke") (Joke "knock-knock" "Knock knock. Who's there? Boo." "Don't cry, it's only a joke!")
+KnockKnock {whosThere = "Boo", punchline = "Don't cry, it's only a joke!"}
+```
+
+(`run` is shorthand for `interpret` with a runtime built from your environment -
+more on that in *Actually running stuff*.)
+
+## Contracts (or: the types are the prompt)
+
+Descriptions make a MASSIVE difference to output quality, so you'll usually
+write a contract out rather than derive it:
+
+```haskell
+instance Contract Joke where
+  contract = record "A joke, split into its parts" $ Joke
+    <$> required "genre"     "The style of joke, e.g. pun, dad joke"  genre
+    <*> required "setup"     "The setup line"                         setup
+    <*> required "punchline" "The line that lands it; no explanation" punchline
+
+instance Contract BetterJoke where
+  contract = sumOf "A joke in one of several shapes"
+    [ constructor "DadJoke" "A setup and a groan-worthy punchline" isDadJoke $
+        DadJoke <$> required "setup" "" setup <*> required "punchline" "" punchline
+    , constructor "OneLiner" "A single line" isOneLiner $
+        OneLiner <$> required "line" "" line
+    , constructor "KnockKnock" "The classic call-and-response" isKnockKnock $
+        KnockKnock <$> required "whosThere" "" whosThere <*> required "punchline" "" punchline ]
+```
+
+(Or derive it and add descriptions after: `genericContract & field "punchline" "..."`.)
+
+Contracts compile to the providers' native structured outputs and strict tool
+schemas, not to prompt text, so a reply that doesn't match the schema basically
+can't happen. This was always the weakest part of v0 - it asked the model nicely
+for the right shape and hoped for the best. Checks the schemas can't express,
+like `between 1 10`, are checked locally, and a failed one goes back to the
+model to try again.
+
+This is the core design assumption: the types ARE the prompt. The state's types
+are part of what the model reads, and field names carry meaning. A meeting note
+wrapped in a record with `setup` and `punchline` fields looks like a joke before
+the model reads a word. So give each step the state it should judge, and no more.
+
+And describe an enumeration once - Claude and Jev both see the same wording (see
+`Options` below).
+
+## Is it actually funny?
+
+An LLM is great at writing. Jev is great at judging - quickly, and with a
+probability you can actually put a threshold on.
+
+```haskell
+funny :: Questions YesNo
+funny = yesNo "Would a 10-year-old laugh at this joke?"
+
+ghci> run (draft @[Joke] "ten jokes please" >>> keep 0.7 funny) ()
+[Joke {...}, Joke {...}, Joke {...}]
+```
+
+`keep 0.7` keeps the items Jev says yes to with at least that probability. `gate`
+does the same for one value, sending it `Right` if it passes and `Left` if not:
+
+```haskell
+kidFriendly :: Agentic m Joke Joke
+kidFriendly = gate 0.9 (yesNo "Is this joke suitable for a 10-year-old?")
+          >>> (draft "rewrite this joke for a 10-year-old" ||| returnA)
+```
+
+Jev also has `choice` and `score`, over an `Options` type:
+
+```haskell
+data Groan = Mild | Solid | Unbearable deriving (Generic, Show)
+
+instance Options Groan where
+  options = described "How much the audience groans"
+    [ option Mild       "A polite smile; most people didn't notice"
+    , option Solid      "An audible groan from most of the room"
+    , option Unbearable "People get up and leave" ]
+
+deriving via Enumeration Groan instance Contract Groan
+
+groan :: Questions (Score Groan)
+groan = score "How much will the audience groan?"
+```
+
+For a `score`, the options are levels, lowest first. And questions about the
+same input compose applicatively into ONE request:
+
+```haskell
+review :: Agentic m Joke Review
+review = judge (Review <$> funny <*> groan)
+```
+
+## Give it some tools
+
+Hand a `draft` some tools and it becomes an agent. The model calls them as often
+as it likes, and the step finishes when it responds with the output type.
+
+```haskell
+research :: Agentic IO Text Dino
+research = draftWith [fossilSearch, reliable] "Research this dinosaur. Cite a source for every claim."
+
+fossilSearch :: Tool IO
+fossilSearch = tool "search" "Search the fossil database" (act searchFossils)
+
+reliable :: Tool IO
+reliable = tool "is_reliable" "Is this source trustworthy?" (judge (yesNo "Is this a reliable scientific source?"))
+```
+
+A tool's body is just another `Agentic` - an effect, a Jev judgement, a pipeline,
+or a whole other agent. There's no separate tool system, so anything fancier
+(asking a human before a destructive tool, say) you build out of the same pieces:
+
+```haskell
+deleteRecord :: Tool IO
+deleteRecord = tool "delete" "Delete a fossil record" (act confirmWithHuman >>> act deleteIfApproved)
+```
+
+### How the loop works
+
+The core runs the loop and a provider only ever takes one turn, so it behaves
+the same against a real provider, a mock or a replay. A few things worth
+knowing:
+
+- There's no turn limit - the model decides when it's done. Want a cap? Wrap the
+  runtime (`capped 20`).
+- A bad tool call (unknown name, input that doesn't decode) goes back to the
+  model. But a failure *inside* a tool's body escapes the step, like any other
+  error in `m` - if you want the model to see it, put it in the tool's output
+  type (`Either NotFound Fossil`).
+- A `draftWith` inside a tool is a sub-agent with its own conversation.
+- The conversation stays inside the step. Only the typed result moves on.
+
+## Example: The dino project
+
+A grade 5 project - and a flow that mixes both kinds of model. Claude suggests
+ten prehistoric creatures. Jev sorts the dinosaurs from the rest (pterosaurs and
+plesiosaurs are the classic "not actually dinosaurs" - sorry kids). Code keeps
+the clear dinosaurs. Claude draws each one and makes its trump card, then makes a
+poster with a corner for the creatures that weren't dinosaurs.
+
+```haskell
+dinoProject :: Agentic IO () Poster
+dinoProject =
+  draft @[Creature] "Name 10 prehistoric creatures a grade 5 class might have heard of. Include a mix of kinds, not only dinosaurs."
+    >>> each classify
+    >>> arr (partition (clearly Dinosaur 0.8)) `named` "split off the clear dinosaurs (≥ 0.8)"
+    >>> (each (arr fst >>> exhibit) *** arr (map notADinosaur) `named` "note what the others were")
+    >>> arr (uncurry Exhibit)
+    >>> draft @Poster "Create a poster of these dinosaurs for a grade 5 class. Add a corner about the creatures that weren't dinosaurs, and what they were."
+
+-- Jev decides what kind of animal each creature was.
+classify :: Agentic IO Creature (Creature, Choice Kind)
+classify = returnA &&& judge (choice "What kind of animal was this creature?")
+
+exhibit :: Agentic IO Creature Entry
+exhibit =
+  (returnA &&& draft @DinoPic "Draw an ascii picture of this dinosaur, 10 lines high"
+           &&& draft @TrumpCard "Make a trump card for this dinosaur")
+    `named` "exhibit"
+    >>> arr (\(c, (p, t)) -> Entry c p t)
+```
+
+The trump card's stats use a `Stat` contract that checks 1 to 10, so every card
+uses the same scale. And the poster is drafted from a named `Exhibit` record
+rather than a tuple, so Claude sees `dinosaurs` and `notDinosaurs` instead of
+`_1` and `_2`. The whole thing is in `examples/Dino.hs`.
+
+You could also write `exhibit` with `proc` notation, since `Agentic` is an
+`Arrow`:
+
+```haskell
+exhibit :: Agentic IO Creature Entry
+exhibit = proc creature -> do
+  pic   <- draft @DinoPic "Draw an ascii picture of this dinosaur, 10 lines high" -< creature
+  stats <- draft @TrumpCard "Make a trump card for this dinosaur" -< creature
+  returnA -< Entry creature pic stats
+```
+
+It reads nicely, but GHC turns `proc` into a chain of `first`s, never `&&&`. So
+the picture and the trump card run one after the other instead of side by side,
+and `describe` shows GHC's plumbing rather than the shape of the flow:
+
+```
+both halves
+├─ first → draft @DinoPic  "Draw an ascii picture of this dinosaur, 10 lines high"
+└─ second → pass
+both halves
+├─ first → draft @TrumpCard  "Make a trump card for this dinosaur"
+└─ second → pass
+```
+
+So for steps that don't depend on each other, stick with `&&&`.
+
+### Describe it before you run it
+
+```
+ghci> describe dinoProject
+draft @[Creature]  "Name 10 prehistoric creatures a grade 5 class might have heard of. Include a mix of kinds, not only dinosaurs."
+each
+└─ judge  choice of 7 "What kind of animal was this creature?"  (keeping its input)
+arr  split off the clear dinosaurs (≥ 0.8)
+both halves
+├─ first → each
+│  └─ exhibit  together  (keeping its input)
+│     ├─ draft @DinoPic  "Draw an ascii picture of this dinosaur, 10 lines high"
+│     └─ draft @TrumpCard  "Make a trump card for this dinosaur"
+└─ second → arr  note what the others were
+draft @Poster  "Create a poster of these dinosaurs for a grade 5 class. Add a corner about the creatures that weren't dinosaurs, and what they were."
+```
+
+`mermaid` and `dot` draw the same flow as a diagram of how the data moves
+(GitHub draws Mermaid inline, and Graphviz renders `dot` offline). Here's the
+dino project:
+
+```mermaid
+flowchart TD
+  input(["input"])
+  n0["draft @[Creature]<br/>#quot;Name 10 prehistoric creatures a grade 5 class might have heard of. Include a mix of kinds, not only dinosaurs.#quot;"]
+  subgraph n1["each"]
+    n2["judge<br/>choice of 7 #quot;What kind of animal was this creature?#quot;"]
+  end
+  n3["arr<br/>split off the clear dinosaurs (≥ 0.8)"]
+  subgraph n4["each"]
+    subgraph n5["exhibit"]
+      n6["draft @DinoPic<br/>#quot;Draw an ascii picture of this dinosaur, 10 lines high#quot;"]
+      n7["draft @TrumpCard<br/>#quot;Make a trump card for this dinosaur#quot;"]
+    end
+  end
+  n8["arr<br/>note what the others were"]
+  n9["draft @Poster<br/>#quot;Create a poster of these dinosaurs for a grade 5 class. Add a corner about the creatures that weren't dinosaurs, and what they were.#quot;"]
+  output(["output"])
+  input --> n0
+  n0 --> n2
+  n0 --> n3
+  n2 --> n3
+  n3 -->|first| n6
+  n3 -->|first| n7
+  n3 -->|second| n8
+  n3 -->|first| n9
+  n6 --> n9
+  n7 --> n9
+  n8 --> n9
+  n9 --> output
+```
+
+`describe` returns a plain `Description` you can walk yourself, and `toValue`
+turns it into JSON for UIs and other agents. The tree hides unnamed glue between
+steps, but never a branch.
+
+### Naming things
+
+An `arr` or an `act` is opaque to `describe`, so you name it. `named` binds as
+tightly as function application, so it names exactly the expression before it:
+
+```haskell
+    >>> arr (partition (clearly Dinosaur 0.8)) `named` "split off the clear dinosaurs (≥ 0.8)"
+```
+
+Without it, the one step that decides which creatures make the poster would be
+invisible. Bracket a bigger sub-flow to name all of it, and use `note` to add a
+description too. Names also tag every trace event, so they stay useful well
+after the flow is written. My rule of thumb: an instruction is written for the
+model, and a name is written for whoever's watching.
+
+## Example: Tic-tac-toe
+
+The model plays both sides. Given the game so far it plays the next move, and
+`repeatUntil` goes round again until the model says the game is over.
+
+```haskell
+data Square = Blank | X | O
+data Row = Row {left :: Square, centre :: Square, right :: Square}
+data Board = Board {top :: Row, middle :: Row, bottom :: Row}
+data State = Playing | Ended
+data Game = Game {board :: Board, state :: State}   -- all deriving (Generic, Show, Contract)
+
+nextMove :: Agentic IO Game Game
+nextMove = draft @Game "Play the next move!"
+
+game :: Agentic IO Game Game
+game = repeatUntil ((== Ended) . state) (nextMove >>> act printBoard `named` "print the board") `named` "play until the game ends"
+```
+
+Notice the instruction doesn't explain the rules. It doesn't need to! The types
+say there's a 3×3 board of `Blank`, `X` and `O`, and a game that's either
+`Playing` or `Ended` - the model knows the rest. `describe` shows the loop:
+
+```
+play until the game ends  repeatUntil
+├─ draft @Game  "Play the next move!"
+└─ act  print the board
+```
+
+`repeatUntil` is the only loop in the language, and it checks its condition
+before each round. `cabal run tictactoe` plays a game on OpenAI, where the dino
+project runs on Claude - same flow code either way.
+
+## Example: The kitchen sink
+
+Want to see everything at once? `examples/KitchenSink.hs` is a day of support
+email at an online bookshop. Claude writes the day's inbox. Jev drops the spam
+(`keep`) and triages each email with three questions in one request - a `choice`
+of topic, a `score` of urgency, and a `yesNo` "is the customer angry?" - and a
+named policy (`note`, `arr`) turns that into a ticket. Urgent and routine tickets
+are handled side by side (`***`). Refunds go to an agent with two tools, an
+order lookup (`act`) and a refund-policy check (`judge`), and everything else
+gets a plain reply (`|||`). Each reply is polished until Jev rates it polite
+(`repeatUntil`) while a log line is written alongside it (`&&&`), then sent
+(`act`), and urgent tickets also page the on-call team. Claude ends the day with
+a report.
+
+The runtime runs independent work at the same time (`concurrently`) and records
+every model call (`withStore`), so a second run replays the first. (Claude's
+first drafts are usually polite enough already, so the polish loop tends to
+hand them straight back.)
+
+## Actually running stuff
+
+A runtime has two roles to fill. System One answers `judge` steps - fast, typed
+judgements with probabilities. System Two answers `draft` steps - an LLM taking
+turns. You pick one provider for each:
+
+```haskell
+main :: IO ()
+main = do
+  rt <- pure runtime
+    >>= withSystemOne jev
+    >>= withSystemTwo (anthropic & model "claude-opus-5-5")
+    <&> concurrently . observing logEvent
+  poster <- interpret rt dinoProject ()
+  print poster
+```
+
+Each provider has a default config you tweak with setters, like
+`anthropic & model "claude-sonnet-5-5" & effort Low`. Keys come from the
+environment (`JEV_TOKEN`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`), or from a
+`.env` file via `loadDotEnv`.
+
+Jev only does System One, so `withSystemTwo jev` is a type error (nice!). The LLM
+providers can fill both roles - this runs everything on OpenAI, no Jev token
+needed:
+
+```haskell
+rt <- pure runtime
+  >>= withSystemOne (openai & model "gpt-6-astra")
+  >>= withSystemTwo (openai & model "gpt-6-astra")
+```
+
+Fair warning though: an LLM answering as System One gives you probabilities, but
+they aren't calibrated the way Jev's are, so a `gate 0.9` means a lot less.
+
+### What about prompts? And sessions?
+
+A system prompt for every `draft` is a setting
+(`anthropic & system "You write for primary school children."`).
+
+The library never tells the model how to format its reply - the providers'
+strict structured outputs take care of that. What the model gets is meaning: the
+instruction, the state, and your contracts' descriptions.
+
+And there are no sessions to manage. Anything a later step needs goes through
+the types. Memory across runs is yours to own - put it in the flow's types, or
+behind tools that read and write a store.
+
+### Record once, replay for free
+
+`withStore` records every model call to a file and replays it later:
+
+```haskell
+rt <- pure runtime
+  >>= withSystemOne jev
+  >>= withSystemTwo anthropic
+  >>= withStore ReplayOrRecord "dino.jsonl"
+```
+
+Each answer is keyed by its whole request, so it's replayed only when the model
+would be asked exactly the same thing. Three modes:
+
+- `Record` calls the models and writes a fresh file.
+- `Replay` answers only from the file (great for tests that are real but free).
+- `ReplayOrRecord` replays what it has and records the rest - perfect while
+  you're working on the end of a long flow.
+
+Only model calls are stored though - `act` steps and tool bodies run for real.
+
+For tests, swap in scripted providers from `Agentic.Scripted` - same flow, no
+network:
+
+```haskell
+testRuntime :: IO (Runtime IO)
+testRuntime = do
+  two <- scripted [respond joke]
+  pure runtime { systemOne = alwaysYes 0.95, systemTwo = two }
+```
+
+## History
+
+v0 - the Kleisli-arrow prototype that used Dhall as its output format (bless
+it) - is tagged `v0-prototype`.
+
+Anything I've missed, or something you'd build differently? Issues and PRs very
+welcome!
diff --git a/agentic.cabal b/agentic.cabal
--- a/agentic.cabal
+++ b/agentic.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               agentic
-version:            0.2.0.0
+version:            0.2.0.1
 synopsis:           Composable, inspectable agentic workflows mixing LLMs and Jev
 description:
   Typed agentic workflows built from Arrow combinators. A flow is a description: you can describe it as a tree, Mermaid or Graphviz before running anything, then interpret it against System One (Jev) and System Two (an LLM) providers. This is the core: flows, contracts, questions, the runtime and the interpreter. It depends only on base and text.
@@ -13,7 +13,9 @@
 homepage:           https://github.com/drshade/haskell-agentic
 bug-reports:        https://github.com/drshade/haskell-agentic/issues
 build-type:         Simple
-extra-doc-files:    CHANGELOG.md
+extra-doc-files:
+  CHANGELOG.md
+  README.md
 tested-with:        GHC ==9.6.7 || ==9.8.4 || ==9.10.3 || ==9.12.2 || ==9.14.1
 
 source-repository head
