whatibuilt
v0.1.6
Published
A wiki that writes itself while you vibe code. Explains your project in plain English, for people shipping code they can't read.
Maintainers
Readme
whatibuilt
A visual wiki that builds itself while you vibe code.
You keep working in the terminal. A Stop hook quietly reads each finished turn
and maintains a record of the project on three levels. A local Next.js canvas
renders it on your second monitor. The terminal never gets a wall of text.
Who this is for
Someone who has never written code professionally, building real software by directing an AI agent. It works — but they cannot read what the agent wrote, so they end up owning software they do not understand.
That audience is a hard constraint, not a nicety, and it decides how everything here is written:
- Every card and every decision leads with plain language. The technical detail is behind a toggle, so you can stop reading at the point you understand.
- Jargon gets defined on sight. "Hook", "schema", "endpoint" mean nothing until they do, and the glossary updates every single turn.
- "A programmer would find this obvious" is not a reason to skip something. That reader is not who this is for.
The claim worth testing: this captures rationale at the moment of the decision — the thing a README written afterwards quietly loses, that version history never had, and that the raw transcript buries under 40,000 lines of noise.
Install
Two pieces: a plugin that records, and a canvas that renders. Neither needs a checkout of this repo.
1. Install the plugin — once, in any Claude Code session:
/plugin marketplace add kasperzhang/whatibuilt
/plugin install whatibuilt@whatibuilt2. Arm it — in the project you want recorded:
/whatibuilt:learn3. Open the canvas — in that same project directory:
npx whatibuilt --detach # prints the URL and hands the prompt back
npx whatibuilt --stop # when you are doneUse --detach when you are starting it from inside a coding session, which is
almost always. In the foreground the server belongs to that turn: the shell
suspends or kills it when the turn ends, and all you see afterwards is a page
that will not load. Detached, it returns only once the canvas actually answers
for this project, and prints the port it really bound.
Drop the flag if you want it in the foreground with Ctrl+C to stop it.
The canvas serves the zone of the directory you start it in, so run it in your project, not in a whatibuilt checkout. Put it on your second monitor and go back to work.
The hook is installed globally but stays inert everywhere you have not run
/whatibuilt:learn — it exits immediately unless it finds .whatibuilt/enabled
in the project you are coding in. Installing it does not opt every repo in.
Commands
/whatibuilt:learn # arm the extractor for this project
/whatibuilt:learn status
/whatibuilt:learn phase "Billing" # force a phase boundary
/whatibuilt:learn backfill # build a zone from transcripts you already have
/whatibuilt:learn doctor # check the zone against the filesystem
/whatibuilt:learn map # plain-language map of the codebase
/whatibuilt:learn polish # rewrite text that lapsed into jargon
/whatibuilt:learn offCanvas options
npx whatibuilt --port 5000
npx whatibuilt --zone ~/code/other-app/.whatibuiltMore than one project at once
Run npx whatibuilt in a second project while the first canvas is still up and
it moves to the next free port and says so. Run it twice in the same project
and it points at the canvas already serving that zone rather than starting a
second one.
Both cases work by asking whoever holds the port who they are, via
/api/whoami. A canvas serving a different project answers on the same port
and is indistinguishable over plain HTTP, so "something responded on 4111" is
never treated as "the canvas for this project is up" — that check is what sent
an early tester to another project's wiki.
Occupancy is decided by connecting on every loopback address, not by binding
one of them. localhost resolves to ::1 before 127.0.0.1 on macOS, and a
server holding *:4111 does not stop anything else binding 127.0.0.1:4111 —
so a bind probe reports free for a port the browser cannot reach, and you end
up with two projects answering on one port number depending on which address
the client picked. The server then binds whatever localhost resolves to, so
the URL printed is the URL that works.
If you are ever unsure which project a canvas is showing:
curl -s localhost:4111/api/whoamiWorking on whatibuilt itself
Only needed if you are changing this tool, not using it:
git clone https://github.com/kasperzhang/whatibuilt
cd whatibuilt
npm install
npm run dev # http://localhost:4111, watching this repo's own zone
npm test
npm run build:dist # assemble the publishable canvas into dist/The repo records itself: .claude/settings.json wires the Stop hook to
hooks/extract.mjs directly, so a checkout dogfoods without the plugin
installed. The committed .whatibuilt/ zone is this project explaining its own
construction — clone it and the canvas has something real in it before you
write a line.
Backfill
If the project predates the hook, the zone starts empty while the history sits in Claude Code's transcripts. Replay it:
/whatibuilt:learn backfillOr drive the script directly, from the project whose zone you are filling:
node "${CLAUDE_PLUGIN_ROOT}/hooks/backfill.mjs" \
~/.claude/projects/<project>/<session>.jsonl --dry-runDrop --dry-run to execute. It feeds the extractor a growing prefix of the
transcript under one session id, so the cursor advances exactly as it would
have live — same phase decisions, made in order, without knowing what came
next. Oversized turns are subdivided, which is the one place backfill diverges
from the live hook: the digest cap exists to bound latency, and a one-time
batch has no reason to pay it.
Budget one or two Haiku calls per chunk.
The three layers
The layers differ by write mode, and that difference is the whole design.
| Layer | File | Mode | Reader |
| --- | --- | --- | --- |
| Atlas | atlas.json | rewritten | a stranger, first |
| Arc | arc.json | revised | anyone, for orientation |
| Cards | stream.json | appended | you, later |
Atlas — what the project is, right now. It opens with the premise: what was broken before this existed, who it is for, and the core idea in one repeatable sentence. Then a glossary, then the decisions with their reasons and rejected alternatives, then the parts.
The premise came last and should have come first. Without it a reader could finish the atlas knowing the entire architecture and still not know what the project was for — engineering rationale answers "why is it built this way", and only the premise answers "why does it exist". If your atlas reads as a list of technical choices, that is the failure mode. Rewritten on revision, so a reversed decision is replaced rather than left standing next to its replacement. An append-only log cannot do this, which is why it exists.
Arc — the phases the project moved through. Scaffolding → Auth flow →
The SSE reconnect bug. Five to fifteen entries for a whole project. Phase is
not just a display grouping: it steers the extractor, because scaffolding turns
produce architecture and bug hunts produce gotchas.
Cards — the individual things worth retaining. Filtered hard for novelty. Each leads with a plain-language line; the technical detail sits behind a toggle, so you can stop reading at the point you understand.
The glossary is maintained by the card call, not the atlas call. Jargon lands on this reader every turn, but the atlas rewrite is gated on structural change — so terms used to arrive only on the turns that happened to reshape the project. Folding them into a call that already runs costs nothing extra.
A fourth file, pulse.json, records one beat per armed turn whether or not it
produced anything. Nothing displays it, and it is deliberately kept off the
live feed so writing a beat cannot wake the browser. It survives because it is
the only thing that separates "the hook ran and correctly found nothing" from
"the hook never ran at all".
How it fits together
Claude Code turn ──Stop hook──▶ extract.mjs ──claude -p --model haiku──▶ phase + cards
│ │
│ structural? ─────┤
│ ▼
│ second call: atlas rewrite
▼ atomic writes
.whatibuilt/{atlas,arc,stream,pulse}.json
│
▼ fs.watch + 2s poll
/api/stream (SSE) ──▶ CanvasThe files are the entire protocol. Neither side knows the other exists, so either can be restarted, rewritten, or simply not running.
What is deliberately not here
This is not generative UI. The model does not emit React. It picks a type
from lib/schema.ts and fills props for a component that already exists. Cards
that fail validation are dropped, not rendered — and a malformed atlas rewrite
falls back to the previous one, because a bad atlas is worse than a stale one.
The extractor is not in your main loop. It runs as a separate claude -p
process on Haiku, so extraction never spends your context or your latency.
Most turns produce nothing. The extractor's real job is the novelty filter —
"is this new to this developer?" — not "is this a concept?". A routine rename
emits zero cards. If the canvas fills with things you already know, tighten
EXTRACT_SYSTEM in hooks/extract.mjs, not the UI.
The atlas is not novelty-filtered. Its reader knows nothing about the project, so it should state the obvious. Same data, opposite filters — that is why the two layers cannot be one layer.
The atlas call is gated. It carries the whole atlas as input and costs roughly twice the card extraction, so it runs only when a turn is flagged structural or a phase boundary opens. Most turns cost one Haiku call.
Layout
| Path | What it is |
| --- | --- |
| .claude-plugin/plugin.json | Plugin manifest. Namespaces the skill as /whatibuilt:learn. |
| .claude-plugin/marketplace.json | Makes this repo installable with /plugin marketplace add. |
| hooks/hooks.json | Wires the Stop hook when installed as a plugin. |
| bin/whatibuilt.mjs | The npx whatibuilt launcher. Resolves the zone from your cwd. |
| bin/build-dist.mjs | Folds static assets back into the standalone build. |
| lib/schema.ts | The contract. Add a card type here first. |
| hooks/extract.mjs | Stop hook. Sole writer of atlas.json and stream.json. |
| hooks/phase.mjs | Manual phase boundary, used by /whatibuilt:learn phase. |
| hooks/backfill.mjs | Replays an existing transcript through the extractor. |
| hooks/doctor.mjs | Checks the zone against the filesystem. |
| hooks/map.mjs | Parses the code's structure, describes each file. |
| hooks/polish.mjs | Rewrites plain-language fields that lapsed into jargon. |
| hooks/jargon.mjs | The shared word list, so check and repair agree. |
| skills/learn/SKILL.md | The /whatibuilt:learn command. |
| app/api/stream/route.ts | Watches the zone, pushes a full snapshot over SSE. |
| app/api/phase/route.ts | Phase rename, from the spine. |
| components/Canvas.tsx | Composes atlas → arc → cards, with scroll-spy. |
| components/Atlas.tsx | The macro layer. |
| components/Spine.tsx | The sidebar. Double-click a phase to rename it. |
| components/CodeMap.tsx | The code map page and its diagrams. |
| components/HowItWorks.tsx | Prose describing the extractor. |
| lib/graph.ts | Turns the parsed map into diagrams. No model involved. |
| tests/ | node:test suite. Excluded from the code map on purpose — see below. |
| components/cards/ | One renderer per card type. |
Adding a card type
- Define it in
lib/schema.tsand add it to theCardunion. - Write a renderer in
components/cards/and register it inCanvas.tsx. - Describe its JSON shape in
EXTRACT_SYSTEMinextract.mjs. - Add its required-field check to
validateCards()in the same file — every card needs aplainline, plus whatever its own kind cannot do without.
Skipping step 4 means malformed cards render as blank boxes instead of being dropped.
The code map
map.json is built in one pass over the source rather than turn by turn, and it
is the clearest example of the rule the whole project runs on:
Structure is parsed. Only prose is generated.
Everything about shape — which files exist, what imports what, which files read and write which saved files, what nothing imports — comes from reading the source, so it is true by construction. The model is never asked what connects to what. It is handed a file that definitely exists and asked what happens inside it, in language someone who cannot read code can follow.
node hooks/map.mjs --dry-run # what would be described
node hooks/map.mjs # build itTwo details worth knowing:
- Import edges are not enough. The four hooks are the heart of this project and nothing imports them — Claude Code starts them. Their real connection to the app runs through the saved files. So data-flow edges are parsed too, line by line, because a file that both reads and writes the zone must not be flattened into one direction.
- Prose is cached by file content hash. A first run pays for every chosen file; later runs pay only for files that actually changed, which is usually one or none.
Tests
npm test # everything, including a real server
npm run test:unit # just the fast ones74 tests, no framework beyond node:test. What they cover, and why each exists:
| File | Guards |
| --- | --- |
| tests/schema.test.mjs | A malformed card is dropped without taking the good ones with it. |
| tests/graph.test.mjs | Every diagram is well formed and no edge points at a node that was never declared. |
| tests/extract.test.mjs | Digest truncation keeps the newest end; card and atlas validation refuse what they should. |
| tests/map.test.mjs | Imports resolve, importedBy is the exact inverse of imports, and a saved file written through a variable is still found. |
| tests/doctor.test.mjs | Runs the real script against synthetic zones — fabricated paths, two open phases, duplicate subjects, jargon, README drift. |
| tests/hook.test.mjs | The guard rails: unarmed, recursing, locked, stale-locked, and model-unreachable all end without corrupting anything. |
| tests/api.test.mjs | Starts a real server: the zone and map arrive as separate events, a pulse write pushes nothing, a half-written file never reaches the browser. |
The suite exists because writing it found four real bugs, listed below.
Bugs the tests found
- Good cards were lost behind bad ones. Cards were capped at two before validation, so a turn returning two malformed cards and one good one kept nothing. Now capped after.
- Path containment was prefix matching.
abs.startsWith(root)accepts/tmp/proj-evil/x.tswhen the project is/tmp/proj, so a fabricated module path pointing at a sibling folder was treated as real. Fixed in both the extractor and the doctor. - Clearing a stale lock consumed the only attempt. A lock left by a killed process always blocked the very next run too, and was only cleared by the one after that.
- A card citing two files was reported as broken. The doctor tested the
whole
sourcestring as a single path, so real files were reported missing.
Keeping the zone honest
The atlas is the layer a stranger trusts without reading the code, which makes
a wrong entry in it worse than a missing one. Two things put wrong entries
there: the extractor confabulates plausible file paths that were never written
(a lib/novelty.ts for a filter that only ever lived in a prompt — never written), and real
files get renamed out from under an atlas with no way to notice.
Module paths are now checked against the filesystem at write time — a path that
does not resolve inside the repo never reaches atlas.json. For drift that
happens later:
node hooks/doctor.mjs # report
node hooks/doctor.mjs --fix # repair what is safely repairableIt also enforces the arc's structural invariants: one open phase, no cards
pointing at deleted phases, no duplicate slugs. A card's source is reported
but never deleted — that file may have been legitimately renamed since, and the
card's lesson survives its citation.
Deterministic, no model calls. Run it after any refactor.
When jargon creeps back
doctor can find jargon but not repair it — rewriting prose is not something a
deterministic pass can do. polish closes that loop without giving the doctor a
model:
node hooks/polish.mjs --dry-run # which fields lapsed
node hooks/polish.mjs # rewrite themBoth use the same word list, in hooks/jargon.mjs, so finding and
fixing can never disagree. Polish rewrites sentences only, and discards any
rewrite that did not actually remove jargon — the original at least came from a
pass a human reviewed.
Concurrency and debugging
Two Claude sessions in one repo, or a live hook firing during a backfill, would
otherwise interleave their read-modify-write of arc.json and silently lose a
phase. The extractor takes an exclusive lock covering both the arc and the
transcript cursor.
The two callers want opposite things from contention, so the wait is a knob:
| Caller | Behaviour | Why |
| --- | --- | --- |
| Live Stop hook | skips immediately | The next turn covers it. Burning the hook's timeout to skip anyway helps nobody. |
| backfill.mjs | waits up to ~75s | A skipped chunk is lost history, not deferred history. |
A blocked writer touches nothing — no card, no beat, and the cursor does not advance, so the turn it skipped is still pending for whoever runs next.
| Variable | Default | Effect |
| --- | --- | --- |
| WHATIBUILT_DEBUG | unset | Write extractor decisions to stderr. |
| WHATIBUILT_MODEL | haiku | Model used for both calls. |
| WHATIBUILT_MAX_DIGEST | 12000 | Transcript characters per turn. Backfill raises it to 40000. |
| WHATIBUILT_LOCK_WAIT | 1 | Lock acquisition attempts, 3s apart. |
| WHATIBUILT_ZONE | ./.whatibuilt | Zone directory, for the hook and the canvas alike. |
The hook swallows every error by design — a broken extractor must never break a
coding session — which also makes it invisible when it misbehaves. Set
WHATIBUILT_DEBUG=1 to get the reasoning back.
What it will not record
A turn that names files, all of them inside .whatibuilt/, is treated as work
on the tool rather than on your project, and is dropped before any model call:
no card, no phase, no turn counted.
The rule only fires on positive evidence. A turn that names no files at all is always kept — an agent can build an entire application through Bash heredocs and leave no file path anywhere in the transcript, and an earlier version of this check threw away exactly that: a fourteen-minute build of a whole app, recorded as "nothing extracted; the work was routine".
Arming the zone, chasing a canvas that will not load, killing a stuck server —
none of that is something to teach you about your own code, and recording it
means the first card a new user reads is about installing whatibuilt. It also
ages badly. The first such card written advised nohup, which was reasonable
at the time and wrong within a day of --detach shipping.
The check is deterministic and never asks the model, so a turn that touched real code is never dropped on a judgement call.
Known rough edges
- The KNOWN list is passed as slugs and grows unbounded. Past a few hundred cards, expect the novelty filter to start missing duplicates. The fix is embedding-based dedup, not a longer prompt.
- Phase detection is the most fragile layer. Its first real test produced a
one-phase arc across sixteen turns: the opening thesis was broad enough to
contain the whole project, and a continue-bias then made it an absorbing
state. The prompt now demands a narrow, completable thesis and applies
pressure past six turns — but if you see phases stop turning over, that is
the failure mode, and it looks like success until you count. Double-click any
phase in the spine to rename it; use
/whatibuilt:learn phaseto force a boundary. - The atlas can drift on a bad rewrite. Validation catches structural damage — a rewrite that drops the whole module map is rejected — but not a subtly wrong claim.
License
MIT — see LICENSE.
