Why this and not a CRUD app
Chopin names three gaps in how we plan with agents: plans are not interactive, decisions have no owner, and agents explain in text when a picture would do. A tool that fixes those is also, conveniently, the perfect specimen for this workshop. Decisions are events, so event sourcing is the obvious model rather than a flourish. A plan room with people and an agent in it is the multiplayer harness, so the harness demo and the product are the same code. The agent-built artifacts from the planning skills below have a home to live in. And you dogfood it on stage: the ticket for Greenhouse gets planned inside Greenhouse.
Scope, honestly. Chopin is months of work by two people and includes a real-time collaborative MDX editor, research blocks, and GitHub-repo organisation. Greenhouse is the smallest cut that still proves the point: block-level plans (no character-level co-editing; each edit is an event, last writer wins per block), comment threads, a decision log, an agent that participates through the same commands as a person, embedded artifacts rendered from Markdown, and an MCP server so any coding agent can read the plan and write decisions back. Deferred: CRDT text editing, presence cursors, research blocks, multi-repo. Chopin itself is open source (githubnext/chopin); the point here is not to clone it but to build the thin version on the stack below so every practice is visible.
The domain: a plan room
A plan belongs to a repo. It is made of blocks. Anyone in the room, human or agent, can edit a block, open a comment thread on it, or ask for an exploration. Threads end in decisions. Decisions stay attached to blocks. That is the whole model, and it needs a real vocabulary, an event log, auth, a file store for artifacts, and an agent in the loop.
// the ubiquitous language, as types the user would recognise
Plan — a document tied to a repo; a stream of events
Block — a unit of the plan: prose, artifact, or research
Thread — a comment thread anchored to one block
Decision — how a thread ended: who, when, what, which block
Exploration — an agent thread implementing one option, reporting back
Artifact — an agent-built interactive thing embedded in a block
Participant — a person or an agent; same commands, same rights
// commands anyone issues // events that get recorded
EditBlock ───────────▶ BlockEdited
OpenThread ───────────▶ ThreadOpened
Reply ───────────▶ ReplyPosted
ResolveThread ───────────▶ ThreadResolved ──▶ DecisionRecorded
RequestExploration ────────▶ ExplorationStarted … ExplorationReported
AttachArtifact ───────────▶ ArtifactAttached
ForkPlan ───────────▶ PlanForked // a branching entry point, not a dead end
Three speakers, one vocabulary: the person says "resolve this thread", the code says ResolveThread, and the agent, reading the repo, says the same thing back. The agent is a Participant, nothing more: it holds no command a person cannot issue, which is also how you keep it honest.
The stack: good enough, boring on purpose
| Layer | Choice | Why it is good enough | Practice it shows |
| runtime | Bun + TypeScript | One toolchain for scripts, server and tests. Fast enough that the feedback loop stays in milliseconds. | run anywhere |
| frontend | TanStack Start | Router, Query and Form from one family; file-based routes the agent can navigate without a map. | simple components |
| editor | Block-level Markdown, rendered with embedded artifacts | Each block is plain Markdown; an artifact block is a self-contained HTML file in an iframe. No MDX compiler, no CRDT: edits are events, which is simpler and already gives history and forks. | simple tech choices |
| realtime | Durable Object per plan, WebSocket fan-out | The plan's event stream lives in one DO; every open tab subscribes. Same mechanism the harness uses for sessions, so there is one way to do live. | event sourcing |
| ui | shadcn/ui + Tailwind | Components live in your repo as plain code, so taste is applied by editing, not by overriding a library. | taste |
| backend | Effect | Services, layers, typed errors, schemas. The type system carries the invariants and the agent cannot forget to handle a failure. | types as design language |
| schema | Effect Schema, shared | One definition of Decision used by the API, the UI, the MCP server and the tests. The vocabulary cannot drift between layers. | one vocabulary |
| db | Postgres + Drizzle | Drizzle schemas are readable TypeScript and migrations are plain SQL; both are legible to a reviewer and to the agent. Wired through @effect/sql. Local Postgres from Nix; in prod a hosted Postgres (Neon) reached through Hyperdrive. | make the data visible |
| events | Append-only events table | No event-store product. One table, one append, projections as Effect programs. Enough to replay and to test. | event sourcing |
| auth | Better Auth | Sessions, OAuth and passkeys in your own DB, no third-party dashboard to configure out of band. | simple tech choices |
| files | S3 API, MinIO locally | Artifacts and exploration reports go to a bucket. Locally that bucket is MinIO, in prod it is R2. Same code path, same S3 client. | run anywhere |
| agent | pi-durable harness, one per plan room | The room's agent is a durable pi session whose tools are the same commands people have, plus the planning skills. It runs explorations, builds artifacts, and resolves threads when asked. Mockable in tests by replaying its stream. | legibility · trust ladder |
| mcp | Greenhouse MCP server | Any coding agent (Claude Code, Codex, Amp) can read a plan, list open decisions, and write a decision back. Plan in Greenhouse, implement anywhere, review against the plan. | one vocabulary |
| local | Nix flake + process-compose | Postgres, MinIO, mail catcher and the app start from one just up. Works on a fresh laptop and inside an agent sandbox. | DX is agent DX |
| deploy | Alchemy on Cloudflare | Infrastructure as TypeScript in the same repo: Cloudflare.Website.TanStackStart for web and api on Workers, R2Bucket, Hyperdrive to Postgres, Durable Objects for sessions, secrets. The agent can read the deploy, not just the code. | read the docs |
| coding agents | pi + Claude Code, via the skills folder and the MCP server | Same skills consumed by both. pi for the harness walkthrough, Claude Code for the long runs; both read the plan through MCP rather than being pasted a wall of Markdown. | trust ladder |
Deploy footnote. Cloudflare is the easy path here: Alchemy's TanStack Start adapter is first-class on Workers, R2 speaks S3 so the MinIO code path is unchanged, and Durable Objects give pi-durable a per-session home for free. The one thing Cloudflare does not have natively is Postgres. Two honest options: keep Postgres (local from Nix, hosted Neon in prod, Hyperdrive in between) for full local/prod parity, or move to D1 and SQLite everywhere. The page assumes Postgres; D1 is a smaller, cheaper demo if the audience will not care about parity. Agent sandboxes run as Cloudflare Containers through the Sandbox SDK.
Repo layout
greenhouse/
├── flake.nix # postgres, minio, mailpit, bun — one env everywhere
├── process-compose.yaml # what `just up` starts
├── justfile # the only commands anyone (or any agent) needs
├── alchemy.run.ts # workers, r2, hyperdrive, durable objects, secrets
├── packages/
│ ├── domain/ # Plan, Block, Thread, Decision: commands, events, projections
│ │ └── __expect__/ # expect tests: command in, event stream out
│ ├── api/ # Effect HTTP, Better Auth, plan-room Durable Object
│ ├── agent/ # the room's pi-durable harness and its tools
│ ├── mcp/ # MCP server: read plan, list decisions, record decision
│ └── web/ # TanStack Start + shadcn/ui; block editor, threads, log
├── e2e/ # event-replay e2e, and a few Stagehand flows
├── scripts/ # scaffolding: new-command, new-event, new-projection, new-route
├── skills/ # the collection below, loaded by pi and Claude Code
├── docs/
│ ├── GLOSSARY.md # the ubiquitous language, kept next to the code
│ └── decisions/ # living wiki: why, not what
├── AGENTS.md # shared agent instructions
└── CLAUDE.md # → AGENTS.md
Scripts the demos run
A justfile is the contract between you, the agent and the sandbox. If a thing is not a recipe here, the agent will invent its own way to do it.
just up # nix develop + process-compose: db, minio, mail, api, web
just test # expect tests (ms) then event-replay e2e (s)
just test:ui # Stagehand flows against the local app (min)
just new command Name # scaffold command + schema + expect test stub
just new event Name # scaffold event + append to the registry
just new projection Name
just new route /path # TanStack route + loader wired to the api
just plan GH-12 # open a plan room for the ticket; agent drafts blocks
just explore GH-12 # one exploration per approach, reported back into the plan
just trace session-id # animated walk through what the agent touched
just replay stream-id # rebuild a projection from the log, print the story
just review # model-inversion review of the current branch
just deploy # bun alchemy.run.ts
How a coding harness works
Before the multiplayer version, the single-player loop, because most of what feels like magic is six lines of plumbing. A harness is a program that assembles context, asks a model, executes whatever tool call comes back, and feeds the result in again until the model stops asking.
// the whole loop, honestly
context = system prompt + AGENTS.md + skills + conversation + tool results
loop:
reply = model(context, tools)
if reply has no tool call: break
result = execute(reply.tool_call) // shell, read, write, browser…
context += reply + result // this is where context grows
you decide: which tools exist, what needs approval, what gets compacted,
what persists between sessions, who else can see the thread
Context assembly
Everything the agent "knows" was put there by the harness: instructions files, loaded skills, tool results. Skills are just files added to context on demand. This is why the living wiki and AGENTS.md matter so much.
Tools and permissions
The model only asks; the harness executes. The trust ladder is literally a permission table in the harness.
Durability
Two different things hide under this word, and the next block separates them: a transcript you can reopen, and an execution that survives being killed mid tool call.
Legibility
Every turn, tool call and result is a record. A harness that keeps them is a harness you can audit, replay and learn from. Same idea as the event log.
Context economics: tokens, caching, compaction
The model remembers nothing between requests, so every turn re-sends the whole conversation. Cost grows with the square of session length unless the harness is careful. Three mechanisms keep it survivable, and all three are decided by the harness, not the model.
// one request, in the order Claude Code sends it (stable first, volatile last)
[ system prompt + tool definitions ] changes when: the tool set changes, upgrade
[ CLAUDE.md · auto memory · rules ] changes when: session start, /clear, /compact
[ conversation · tool results ] changes when: every turn (appended at the end)
// the cache matches the PREFIX, exactly. change byte 1 and everything after it is recomputed.
turn n: [sys][ctx][msg1 … msg n-1] ← read from cache [msg n] ← processed
/model: [sys][ctx][msg1 … msg n ] ← all recomputed: each model has its own cache
Caching
Cached tokens bill at a fraction of the input rate; the rest is processed in full. Cache lifetime is 5 minutes of inactivity on an API key (1 hour for the main conversation on a subscription within plan usage; promptCacheTtl: "1h" sets it yourself). The first turn back after a break is slow because the prefix expired.
What breaks it
Switching model, changing effort (on most models), toggling fast mode, changing the loaded tool set (an MCP server connecting with tools loaded upfront, or denying a whole tool), compaction (by design), upgrading. Pick model and effort at the top of a session and leave them.
What keeps it
Editing files (reads are appended), invoking skills (appended as messages), changing permission mode, /rewind (truncates back to a prefix that is already cached), spawning a subagent (its own cache; the parent's is untouched). Editing CLAUDE.md mid-session keeps the cache because it does not apply: the new text loads on the next /clear, /compact or restart.
Compaction
Replaces history with a structured summary. Reloaded from disk afterwards: CLAUDE.md, memory, rules, the most recently modified files, and the body of each skill you invoked (capped at 5,000 tokens per skill). Not reloaded: skill descriptions, and anything the summary left out. Run /compact at a boundary between tasks rather than letting auto-compaction fire mid-task; after a long break it is the most expensive command you have, since the summarisation request reprocesses the whole history uncached.
Token diet
MCP tool schemas are deferred by default and loaded on demand, so connectors cost a name, not a schema. Research that would dump a library into context goes to DeepWiki or its own throwaway session, and only the answer comes back, as a paragraph you read first. Tool output is tokens: a just test that prints 400 lines on success is a tax on every later turn. Auto memory loads only its first 200 lines or 25 KB.
Where to look
/context shows what is in the window. /usage shows the session's cache hit ratio and names the likely cause of the last miss ("tool definitions changed"). A status line can show cache_read_input_tokens against cache_creation_input_tokens live: reads high and creation low means it is working.
Rules for Greenhouse sessions
Model and effort chosen before the first prompt. AGENTS.md short and stable; the detail lives in skills, which load on demand. just recipes quiet on success, verbose on --verbose. Expect snapshots kept short because they will be read back into context. just review runs the second model in its own session, so the cold start never lands in the authoring session's cache.
Rules for your own harness
Order the prompt stable-to-volatile and put the cache breakpoint after the stable part. Never rewrite history in place; append. Compact by summarising into a new entry, keep the old entries on disk (pi-durable's store already does this). Let sessions compound: what one session learned goes into a file the next one loads, not into a longer transcript.
Two kinds of durability
Vanilla pi and Claude Code both write the conversation to disk as it happens and can reopen it. Neither resumes a tool call that was running when the process died. pi-durable is built for exactly that gap: it checkpoints the loop itself, so a killed process picks up from the last committed step. The table is the slide.
| pi (vanilla) | Claude Code | pi-durable |
| transcript | Session file on disk, reopen later | JSONL at ~/.claude/projects/<project>/<id>.jsonl, saved continuously; --continue, --resume <name>, /branch, --fork-session | Committed entries in a store: SQLite (openNodeSqliteStorage), JSONL or in-memory |
| killed mid tool call | Tool result lost; you re-prompt | Call is marked cut off before its result was recorded; the model is told to check whether it took effect before running it again. Not rerun automatically | Resumes from the last durable checkpoint. Tools declared replay: 'safe' rerun; others are reported to the model as interrupted. Model requests can be resent (and billed again) |
| same prompt twice | Runs twice | Runs twice | A reused requestId returns the existing submission and its answer. Input dedup only; effectful tools still need their own idempotency |
| second device | No | Remote Control: a phone or browser attaches to a running session. Two terminals on one session interleave into a single transcript, no locking | root.viewState(context): a subscribable view of committed entries, running tool output and queued inputs, so a reconnecting client rebuilds the screen |
| message while busy | Queued | Queued as the next turn | Follow-up by default, or whenBusy: 'steer' to join at the next tool-round boundary |
| what does not survive | Anything in flight | Background subagents and background Bash end with the process; they show up as "did not finish" notes on resume | Nothing is exactly-once: checkpointed, not transactional across the world. One process must own each store; WAL with synchronous = NORMAL can lose the newest commit on power failure |
| status | Stable | Stable, documented | Experimental; pin the lockfile |
The honest line for the talk: Claude Code gives you a resumable conversation; pi-durable gives you a resumable run. For a coding session on your laptop the first is usually enough. For an agent that runs unattended in the cloud, triggered by a webhook, the second is the whole point.
pi-durable, picked apart
Three packages, two files, one store. The example below is the shape of the Flavio Copes walkthrough, trimmed to the parts that matter for the demo.
// tools.ts — a tool that takes 10s and declares it is safe to replay
import { defineTool } from "@earendil-works/pi-ai";
export const serviceStatus = defineTool({
name: "service_status",
replay: "safe", // rerun after a crash; no side effects
async execute(input, context) {
await delay(10_000, undefined, { signal: context.abortSignal });
return { checkout: "degraded" };
},
});
// agent.ts — open a store, get the root conversation, submit work
import { openNodeSqliteStorage } from "@earendil-works/pi-durable";
const storage = openNodeSqliteStorage("./agent.sqlite"); // one process owns this file
const harness = createHarness({ storage, tools: [serviceStatus], model });
const root = await harness.root(context, { conversationId: ROOT_CONVERSATION_ID });
harness.resume(); // pick up anything left mid-flight
await root.submit(context, {
requestId: "incident-106", // same id twice = same answer, no rerun
text: "What is the checkout status?",
whenBusy: "steer", // or default: queue as a follow-up
});
// ui.ts — any client, reconnecting at any time
for await (const view of root.viewState(context)) {
render(view.entries, view.runningTools, view.queuedInputs);
}
What is committed
Entries, in order: the submission, each model turn, each tool round and its result. A crash between tool start and tool result leaves the result uncommitted, so recovery restarts that tool (if safe) rather than guessing.
The kill demo
Start incident-106, wait for "Tool started", kill -9 the process, run the same command again. Same submission id, resumes from the checkpoint, tool reruns because its result never landed, answer arrives. Then run it a third time: the saved answer comes back with no tool call at all.
The two-client demo
A terminal and a web page both subscribed to viewState of the same root. Type in one, watch the other. Close the browser mid-run, reopen, it rebuilds from the snapshot. This is multiplayer with zero extra code.
Conversations, tasks, documents
pi-durable's own description: a "durable conversation, task, and document runtime". Child conversations are how subagents are modelled; documents are durable shared state an agent can update atomically. Both are what the Greenhouse review bot needs.
Prior art to borrow from
disler/durable-pi already builds the network: a per-device backend owning one agent.sqlite per session, a hub that holds device tokens, a web client and a TUI feeding the same frames into the same reducer, sync as hello → snapshot → deltas, fork at any past entry, change the tool set mid-run, jobs that pause for human approval. "The agent lives on the device, not in the client."
The Cloudflare step
pi-durable's storage is an interface (memory, JSONL, Node SQLite ship with it). A Durable Object already has SQLite and already enforces one owner per object, which is exactly the one-process-per-store rule. Writing a DO-backed storage adapter is the single piece of new code in the cloud demo; everything else is wiring.
Building the cloud multiplayer harness on pi-durable
The walkthrough: take the single-player loop above and make it survive and share. pi stays small and vanilla; pi-durable gives it a persistent, resumable session. In Greenhouse this is not a side demo: a plan room is a durable session with people in it, so the harness you build here is the product's agent layer.
What you build
A session is already an event stream in pi-durable: submissions, turns, tool rounds, results, as committed entries. The UI is viewState, a projection of that stream. Multiplayer falls out: two people subscribed to one root, either of them able to submit. One agent, one thread, many watchers. Your only decisions are where the store lives and who may submit.
What the agent gets
A sandbox per session (a Cloudflare Container via the Sandbox SDK), the Greenhouse repo checked out inside it, just recipes as its tools, and the skills folder in context. It verifies its own work by running the same tests you run.
Single-player, localVanilla pi against Greenhouse. Show the loop in the logs: context in, tool call out, result back in.trust ladder · rung 1
Add skills and the justfile as toolsWatch behaviour change with nothing but files added to context. This is the whole skills story in one diff.rung 2
Make it durableSwap the loop for pi-durable with a SQLite store. kill -9 mid tool call, rerun the same requestId, watch it resume from the checkpoint. Then a second client on viewState: a terminal and a browser showing the same run.durability
Put the sandbox in the cloudA Durable Object per session is the store (DO SQLite behind pi-durable's storage interface, one owner per object); a Cloudflare Container per session runs the repo and just. The loop runs there; viewState streams back to everyone watching over a WebSocket from the same object.rung 4
Trigger it from a webhookA PR opened starts a review session. Sandbox, agent, webhook: the simple code-review agent from the notebook, now legible end to end.review for understanding
The skills collection
All loaded from skills/ by both pi and Claude Code. Third-party ones are pinned by commit; project ones are ours. Each row says when it fires, because a skill nobody triggers is a document nobody reads.
| Skill | From | Guards | Fires when |
| impeccable | Paul Bakaus · pbakaus/impeccable | UI craft: hierarchy, spacing, type, the "AI-generated look" | any change under packages/web |
| review-animations, improve-animations, animation-vocabulary, pick-ui-library | Emil Kowalski · npx skills add emilkowalski/skills | Motion and component choices that feel right instead of merely present | anything with a transition, dialog or toast |
| anti-slop (TypeScript) | Matt Carey's rules, as ESLint config plus a skill | Defensive noise, needless abstraction, comments that restate code, any | every TypeScript edit; enforced by lint, explained by the skill |
| slop-guard (Markdown) | community skill, pinned | Prose slop in docs, decisions and PR descriptions | any edit under docs/ or a PR body |
| viz-explainer | ours, on manimgl | Renders a short animated explainer of a flow from the event stream or a diagram spec | just explain <stream-id>, and in the talk |
| claude-artifacts | Anthropic's artifact skills | Self-contained HTML pages for prototypes and show-me reviews | "show me", before any UI gets built for real |
| testing | ours | Which test to write: expect at the library level, event-replay e2e through the domain, Stagehand for browser flows. Never a unit test for its own sake | any new command, event or route |
| ddd-vocabulary | ours | New nouns must appear in GLOSSARY.md and as a schema before they appear in code | a new type, table or route |
| event-sourcing | ours | Commands validate, events are facts, projections are pure. Scaffolds with just new | anything under packages/domain |
| review-for-understanding | ours | Walkthrough first, stacked PRs, questions before comments, model inversion | just review, PR webhook |
| deploy | ours | Read Cloudflare and Alchemy docs before touching alchemy.run.ts; never guess a limit, a price or a default (Workers CPU time and D1 row limits bite quietly) | any infra change |
Planning as a boundary object
Maggie Appleton's "Planning with Agents" names the problem with how we plan today: the agent writes about 4,500 words a minute, you read about 240, so the plan arrives as a wall of Markdown you skim and approve. Her remedy is two ideas from Suchman and Star: plans are "a weak resource for ad hoc activity", not a contract, and the plan should be a boundary object, something that serves the human's way of seeing as well as the agent's. Her line for builders: "the agent can and should do 1000x more work to make the human more effective." Greenhouse takes that literally. The speaker has been doing this by hand in a product; the skills and scripts below are that practice, written down so the agent builds the interface instead of you.
| Skill / script | What the agent produces | Replaces | Appleton's pattern |
just plan <ticket> · planning skill | A short plan with every open decision as a link to a built artifact, not a question in prose. Decisions are answered in place and written back to the plan with a timestamp. | The multiple-choice interview nobody finishes | Boundary object; decisions logged |
just explore <ticket> · explore-threads skill | One thread per candidate approach, each a separate implementation opened in the portal. No branches, no worktrees. Each thread ends with the same structured report so they can be compared. | Deciding from a paragraph describing options nobody has tried | Pre-handoff exploration, "making contact with reality" |
decision-report skill | An interactive comparison across the explored options: call sites touched, mechanical vs judgement-call changes, silent behaviour changes, the invariant each option risks, and the expect snapshots side by side. Built with the claude-artifacts skill. | "Option A, B or C?" | Retry-logic decision report |
option-explorer skill | For anything perceptual (spacing, motion, copy, colour): a bespoke control rendered against the running app, with the chosen value saved back to the plan. Pairs with the animation skills and Agentation. | Three text options for something you have to see | Live-app manipulation; shadow and animation tuners |
state-explorer skill | For a command's lifecycle: the states and transitions of a stream rendered as a diagram you can click through, with a button that adds a missing transition as a new event type. | Reading a reducer to find the gap | State machine explorer |
just trace <session> · viz-explainer | What the agent touched and why, as a short animated walk through files and events from the pi-durable stream (manimgl under the hood). | Scrolling a 3,000-line transcript | Agent-run trace views (Mindwalk) |
just lookahead | While you answer one decision, the agent prepares the artifacts for the next two, so you never wait on it. | Idle agent, busy human | Parallel lookahead |
The GH-12 plan, as a boundary object
One real decision: is a decision a separate fact (ThreadResolved then DecisionRecorded, two events, so a thread can close without a decision) or one fact (ThreadResolved carrying the decision)? just explore runs both as explorations. The decision report, embedded in the GH-12 plan, shows the resulting event streams side by side, which projections each touches, and which one makes "a decision references exactly one thread" a type instead of a check. You click one. The plan records who clicked, and when.
Rules the skills enforce
Never ask a question in prose that could be a rendered choice. Never present an option that has not been tried. Every decision carries who made it and when. Plans orient; they do not bind: a thread that meets reality the plan did not predict reports back instead of pushing through. And the agent, not the human, pays the cost of being understood.
The social half of Appleton's argument (colleagues and agents debating a decision together) is what Chopin is for, and Greenhouse keeps the mechanism (a shared room, threads, an agent as participant) while leaving team process out of the talk: one person planning with an agent is the case shown; a second person joining the room is one click more, and that is the only team-level thing said all evening.
Connectors and tools the agent reaches for
Skills tell the agent how to think; these give it somewhere to look. All four are used in the runs.
| Tool | What it is | Used for | Practice |
| Mobbin MCP | Searchable library of real app screens and flows, exposed to the agent | Before designing the split dialog: "how do three shipped apps let a user split an amount?" Inspiration from products people use, not from the model's priors. | taste · show me |
| DeepWiki MCP | Devin's remote, no-auth MCP that answers questions about any public GitHub repo from a generated wiki | "How does Better Auth handle passkeys?" answered from the source, with citations, instead of from memory. The deploy and testing skills tell the agent to ask it first. | read the docs |
| Claude in Chrome | Browser extension that lets the agent drive your real Chrome, with your sign-ins | Dogfooding on the agent's side: open the deployed app, upload a receipt, split it, report what it saw. Also the quickest Stagehand-free smoke check. | dogfood · verify |
| Agentation | Agent-agnostic visual feedback: click an element, select text or drag an area on the page, add a note, copy structured markdown with CSS selectors and positions | Your show-me review loop. Point at the wrong padding, paste the note into the agent pane; it finds the selector in the codebase. Replaces screenshots and "the thing on the left". | show me · taste |
The testing skill, spelled out
Write these
- Expect tests in
__expect__/: a command and a starting stream in, the resulting events out, as a snapshot a human reads like a story. Milliseconds, deterministic, simulated clock.
- Event-replay e2e: a scripted sequence of commands through the real API against a real local Postgres, asserting on the stream and the projections. Seconds, no browser.
- Stagehand flows: the three or four journeys where the browser is the point (open a plan, comment on a block, resolve the thread, see the decision on the block and in the log). Natural-language steps, so the test reads like the user's intent and survives a redesign.
Do not write these
- Unit tests of a function the types already constrain
- Mocks of your own modules; mock the model and the network, nothing else
- Browser tests for anything the event stream can prove
- Tests whose expected value nobody could read and judge
The rule the agent follows: if a test's expected output is not something you could show a user and get a nod, it is the wrong test.
Demo script: each practice, shown live
Fresh clone to green tests, with nobody at the keyboardHand the agent a bare checkout. just up, just test. If it needs you, that is a DX bug, and we fix it on stage.run anywhere
Plan the ticket in the tooljust plan GH-12 opens a room. The agent drafts blocks, runs two explorations, embeds a decision report. You resolve the thread by clicking. The plan is now a boundary object with one owned decision in it.planning · boundary objects
Add a field, follow the trellisAdd rationale to Decision in the shared schema. Watch the compiler walk the agent through api, UI, MCP server and tests.types as design language
A new feature as command, event, projectionjust new command ResolveThread. The scaffold writes the expect stub; the agent fills it; the snapshot is reviewed as prose.event sourcing · expect tests
End to end without a browserReplay a full plan (edit, thread, replies, resolve, fork) through the API and assert on the decision log. Then the one Stagehand flow, to show what it costs by comparison.testing skill
Taste under supervisionAsk for the resolve action "to feel better". impeccable and the animation skills turn that into specific edits; you judge the result by using it.taste · dogfood
Correct code that is wrongHave one tool author a change that passes every test but breaks a glossary rule. Have a different model review it with just review.model inversion · review for understanding
Explain it backjust trace renders the event flow of a real session with viz-explainer and attaches it to the plan as an artifact block. The agent's account of its own work, as a picture, where the decision lives.augment your learning
Implement from the plan, anywhereOpen Claude Code with the Greenhouse MCP server. It reads the plan, sees the one decision, implements against it, and writes "implemented in PR #n" back as a decision. Plan here, build there, review against the plan.one vocabulary · legibility
Ship itjust deploy from the agent's sandbox, after it has read the Cloudflare docs via the deploy skill. Same repo, same vocabulary, now on Workers.read the docs · trust ladder