Pages: Docs our agents edit live
Pages is the docs editor we built for Skool's engineers. People and their Claude Code sessions edit one page at once, and every page is Markdown underneath.
Our plans, specs and agent reports live in Pages, a docs editor we built at Skool. A person edits a page in the browser while a Claude Code session edits the same page, live, as one more editor.

Who writes a page
A page in Pages has more writers than people. Here's who writes ours:
- Engineers, in the browser, on a desk or a phone.
- Their Zebra Code sessions, which are Claude Code sessions that run from a Slack thread or the Zebra Code app. A session drafts a plan, a spec or a report straight onto a page.
- Claude Code in a terminal, through an MCP server.
- Zebra, our Slack agent, which answers questions about a page and suggests edits to it.
- Scheduled agents, which write each run's report as a page.
People, Zebra Code sessions and MCP clients edit the same page at the same time, through the same live path. A scheduled agent writes its run's page whole, and Zebra suggests edits that a person applies.
Markdown underneath
Every page is GitHub-flavored Markdown. The editor is Tiptap, which is built on ProseMirror, and it maps each block back to the Markdown that made it. A block nobody edited saves as its own source text. So a session's small edit makes a small diff, and a page written by hand keeps its formatting.
Blocks that Markdown lacks save in a form GitHub still draws. A toggle saves as <details>, a page's properties as YAML front matter, and a diagram as a mermaid fence. An image's width saves as Pandoc's {width=60%}, which GitHub shows as text.
Point at a block, or tap it, to find its Markdown:
Requirements
- A failed charge retries 3 times
- Each retry waits 30 seconds
Two workers retry one charge
| Case | Expect |
|---|---|
| Declined twice | Charged on the third try |
---
template: "[Dev Spec](/pages/8fd2f86a…)"
Status: In review
Repos: checkout, billing
---
## Requirements
- [x] A failed charge retries 3 times
- [ ] Each retry waits 30 seconds
<details>
<summary>Failure modes</summary>
- Two workers retry one charge
</details>
| Case | Expect |
| --- | --- |
| Declined twice | Charged on the third try |
```mermaid
flowchart LR
charge[Charge] --> retry[Retry]
retry --> receipt[Receipt]
```
No line: a comment thread lives beside the Markdown, and keeps the text it quotes.
A comment never enters the Markdown. Each thread keeps the text it quotes, and finds it again after every edit. An edit that keeps the quoted text keeps the thread on it.
The Go server parses the same Markdown as the editor's TypeScript, line for line. It indexes each page's template, properties and tables for templates and queries. Both parsers read one file of examples in their tests, so a change that makes them disagree fails a test.
Editing together
Live editing is prosemirror-collab, with our Go server as the authority. Each editor sends its changes as steps, from the page version it holds:
- The server applies a push only from the page's current version, under a lock on the page's row.
- A push from an older version answers 409, with the steps it missed. The editor rebases its own steps over them and pushes again.
- Everyone with the page open hears each push as a server-sent event, with each caret and comment.
Our frontend's route handlers can't upgrade to a WebSocket, so the events stream as SSE through the same proxy as every API call. The server's two replicas share them over Postgres LISTEN and NOTIFY, so there's no message bus to run.
An editor saves a snapshot of its confirmed page a second after each push. Search, templates, queries and Zebra's asks read the Markdown from that snapshot. A session's read adds the steps pushed since.
A session is one more editor
A Zebra Code session asks for an edit in Markdown terms: this text becomes that text. The frontend's server turns it into live steps:
- It loads the page as it stands: the last snapshot, plus the steps since.
- It makes the edit on the page's Markdown, then diffs the result against the live document.
- The diff becomes one step that touches only what changed.
- It pushes the step like any editor. On a 409, it takes the steps it missed and rebases, as a browser does.
- It saves a snapshot, and puts the session's caret over what it wrote.
Everyone with the page open watches the edit land, beside a caret named “Zebra Code for” the person. Their own unsent typing rebases over it, as it would over a teammate's. Pick when Alice's push lands, then play it:
A session can also rewrite a whole page. That write names the version the session read, and answers 409 moved once anyone has edited the page since. So a stale rewrite never undoes a teammate's work.
While a session works, the page shows its turn as a card, with a summary and its latest lines. Only the person the session works for sees the lines, since they show commands and file paths. Everyone else sees the summary.
A session acts with its person's grant. The person signs in with Google once and allows it, and Zebra Code claims a token that lasts 90 days. The server keeps only the token's SHA-256.
Any MCP client
Claude Code in a terminal reaches the same pages through our MCP server. It has nine tools: they list, read, create, edit and write pages, comment on them, reply to and resolve comment threads, and list folders.
It signs in with OAuth 2.1. A client registers itself, proves its sign-in with PKCE, and may only redirect to a loopback address. So the code never leaves the engineer's machine. An access token lasts an hour, and each refresh token works once.
An MCP token can read, edit and comment. It can't share, move or delete a page, or change who reads one, so those stay in a person's hands. Its edits go through the same live path as a session's, so they land as steps too.
Asking Zebra about a page
Each page has an Ask tab beside its comments. A question there goes to Zebra, with the page as context:
- The page's Markdown, and the reader's selection if they made one
- Every comment thread, with its replies
- Up to 5 pages it links, if the reader can read them
Each part sits inside a tag that the brief tells Zebra to read as data, never as an instruction. A closing tag inside a comment gets escaped, so a comment can't end its region and talk to Zebra as the reader.
When an edit would answer the question, Zebra suggests it in a page-edit fence. The fence holds the text to replace, a line of seven = signs, then the new text:
```page-edit
A failed charge retries 3 times
=======
A failed charge retries 5 times, 30 seconds apart
```
An editor's Apply makes the edit as their own steps, so it lands like their typing. It first claims the edit on the conversation, so a second tab or a reload can't apply it twice. Undo releases the claim. A reader who can only comment posts the suggestion as a comment instead.
A page's owner also sees Readers asked: Zebra groups readers' questions into topics every 10 minutes. A topic shows once 2 people have asked about it, so the owner learns what the page leaves unclear.
Plans and specs as pages
A template is a page with a schema block, one field a line:
```schema
Status: one of Draft, In review, Approved, Done
Repos: list
Owner: text
```
A page made from it names the template in its front matter, and its property rows pick from the schema. A query block lists the pages of a template, filtered, grouped and sorted:
```query
from: [Plan](/pages/7fbc9d97…)
where: Status != Done
group: Status
sort: Edited desc
```
Our plan workflow runs on these. A plan is a page from the Plan template, with Status first among its properties, and its review is the page's comment threads. The plan-* skills in our agent kit write a plan, answer its threads, tick its steps and hand it off.
Each of those edits reads the page first and changes one section. So the edit never works from a stale copy, and each thread keeps the text it quotes.
Specs work the same way. A change can carry a Dev Spec and a Test Spec, each from its own template. Our test-spec-writer agent drafts the Test Spec from the Dev Spec alone. It never opens the change's code, so no test case is shaped by it.
Diagrams and design boards
A Mermaid flowchart opens as a canvas. A session writes the Mermaid, and a person drags the shapes.
We draw each diagram ourselves, with our own parser, layout and arrow routing, so every screen draws it alike. A diagram saves no positions until someone moves a shape. Then one comment line in the fence holds them, which Mermaid ignores. When a session renames a shape, the shape keeps its place.
A design page is a board of frames. Each frame is HTML with Tailwind classes and a design kit's components, such as our own port of Skool's UI library. Build this hands a frame to the clicker's own Zebra Code session. The session builds it in the repo and replies on the board with its draft PR.
A session can see a frame, too. Zebra Code's server draws it in headless Chromium, and the session reads the picture.
How we built it
I started Pages on October 16, 2026, and five of us built it over the next 24 days. Almost all of it came from Claude Code sessions. Claude co-wrote 128 of the 129 commits, merges aside, and these are the repo's numbers for Pages to November 9:
We prove a change in a real browser, with a Playwright check. Each run starts a fresh stack on free ports: its own database, the Go server, and Next.js. 29 of those checks cover Pages.
The services Pages calls are stubs that answer from fixtures. A Zebra stub answers an ask with the page's title and a count of the threads and linked pages it got. So a check can tell what Zebra saw.
What we learned
Keeping Markdown as the source made agents first-class writers. A session reads and writes the text a person sees, and a small edit stays a small diff.
Making an agent an editor, rather than a client that saves the page, kept people's work safe. Its edit rebases over their typing, and a stale rewrite gets refused.
Each kind of writer gets only the power it needs. A session acts with its person's grant and asks them before it shares or deletes. An MCP token only reads, edits and comments.
Drawing everything from the page's text means every screen, and every session, sees the same page.
More writing
Building something like this, or have a question about it? Email me.