I don't write codebase documentation anymore A developer reports building an agent-maintained codebase wiki that generated a 95-page, roughly 134,000-word documentation set with 67 Mermaid diagrams for one project, updating automatically on every push to main. The wiki lives in docs/wiki/ inside the repository in plain markdown, with an .outline.json file mapping which source files each page owns and a .wiki-state.json file recording the commit it was last generated from, and the author says the two configuration files needed to replicate it are included at the end of the post. Hello everyone 👋 Confession time: in 10+ years of writing software, I have never kept documentation up to date. Not once. And I’ve tried Confluence spaces, GitHub wikis, a docs/ folder, READMEs that start strong and stop being true three sprints later. It always goes the same way. Someone writes a nice page, the code moves, nobody touches the page, and six months later a new person reads it, believes it, and loses an afternoon. Updating docs always felt like a chore I owed someone, and I pay chores about as reliably as you’d expect. A while back I started reading some really cool wikis that a paid AI service had generated for a few projects. Architecture overviews, flow diagrams, a page for every subsystem, all built from the code. I loved them. What I didn’t love was that they lived on someone else’s platform, and I couldn’t shape what they said or how they said it. So I thought: I want my own . In my repo, in plain markdown, maintained by an agent, updated on every push. So I built it. One of my projects now has a 95-page wiki: about 134,000 words and 67 Mermaid diagrams. I didn’t write a single one of those pages, and it updates itself every time something lands on main . This post is the full shebang: what the wiki looks like, how it works, every part that broke along the way, what it’s still bad at, and the two files you need to copy to get it in your own repo both are at the end of the post, complete . Quick caveat before we start, because the title is doing some heavy lifting: what I stopped writing is codebase documentation, the pages that describe what the code does. There’s still a small set of docs the wiki doesn’t touch, and I’ll get to those near the end. What it looks like The wiki lives in docs/wiki/ inside the repo, and it’s two levels deep, never more: docs/wiki/ README.md the index: every reader starts here architecture-overview.md root pages: stuff that spans more than one section getting-started.md api/ README.md a section "hub": directory map, a diagram, a table of pages app-and-routes.md a "leaf": one seam of the code auth-and-sessions.md ... web/ README.md ... workers/ operations/ .outline.json which source files each page owns .wiki-state.json the commit the wiki was last generated from The real one has eight sections, but they’re very specific to the project, so I’m keeping this one generic. The project is a client one, so I can’t show you the real pages. Here’s the top of a leaf page, lightly trimmed: Auto-generated by the wiki skill from commit 20c2b08 on 2026-09-28. Do not edit by hand; changes will be overwritten. App Bootstrap, Routes and the Error Envelope The FastAPI application is assembled in backend/main.py : create app builds the app with the ClerkAuthMiddleware and the aggregated router, and the lifespan function verifies Clerk settings, constructs every provider-backed service that was not injected for tests, and fills an empty Clerk mirror before serving. ... Key files | Path | Role | |--------------------------+---------------------------------------------------------| | backend/main.py | create app and lifespan : middleware, router, ... | | backend/routes/errors.py | Registers the three exception handlers that render ... | | backend/exceptions.py | ErrorCode vocabulary and the ApiError family ... | After that you get sections on how the app gets built, the route families, the error format, and a Mermaid diagram of the startup sequence. At the bottom there’s a Related section linking back to the hub and to the sibling pages it depends on. Every page has the same shape, and that turned out to be a big deal for the readers. Who actually reads this? Humans and agents. For humans, it’s been great for onboarding. When someone new joins the project I send them to the index and the “Reading order” list at the bottom of it, and they get a tour of the codebase that matches what’s on main right now. Much better than whatever Confluence page someone last updated when they felt guilty. But honestly, the agents are the heavy users. The coding agents working on this repo read it all the time . Instead of grepping around for ten minutes to figure out how auth works, an agent reads the index, jumps to the right hub, and loads the one leaf page it needs. It’s a shortcut into the codebase, and a cheap one, since a leaf page is small enough to load whole. The index even has a section written just for them: For agents The lookup path is this index, then a section hub, then a leaf. Each hub's directory map names the page owning each part of its tree, and docs/wiki/.outline.json maps every source path to the page that describes it. How it works: two files The whole thing is two files: | File | What it is | |---|---| | .claude/skills/wiki/SKILL.md | The instructions: layout, page templates, size rules, three modes, a self-check. About 230 lines of prose, zero code. | | .github/workflows/wiki.yml | The plumbing: triggers, model choice, the commit step, and a check for runs that got cut off. | I split them on purpose. The skill never touches git: it reads code and writes markdown, and that’s it. The workflow never decides what a page says: it runs the skill and commits whatever changed. Because of that, you can drop the skill into any repo, or run it by hand with /wiki full in Claude Code, and it doesn’t care who’s calling it. On every push to main , GitHub Actions starts a headless Claude Code session with exactly one prompt: /wiki incremental . The session reads the diff since the last wiki run, figures out which pages describe the files that changed, rewrites those pages from the current code, and exits. Then a plain shell step commits docs/wiki/ back to main . Pages follow seams The first big decision was how to cut a codebase into pages. The skill calls the unit a seam : a boundary the code already has, like a package, a service, a pipeline stage, or a bunch of modules that always change together. Pages follow seams, and never file types or a fixed template like “one page for models, one for views”. The skill also has to find the seams on its own, from git ls-files and the code. There’s no hardcoded list of directory names anywhere. On my repo it came up with eight sections, including some splits I never asked for: it broke the backend into the core service, identity, the answer pipeline, and the test suites. The two state files These two files are what make incremental updates possible. .outline.json is the page map. Each page lists covers the globs that make the page suspect when they change and seeds one to five files to start reading from : { "file": "api/app-and-routes.md", "title": "App Bootstrap, Routes and the Error Envelope", "covers": "backend/main.py", "backend/routes/ ", "backend/exceptions.py", "..." , "seeds": "backend/main.py", "backend/routes/ init .py" } The rule is that every tracked file belongs to exactly one page, and the most specific glob wins. So when a file changes, there’s always exactly one page to re-check. If a file doesn’t match any page, that’s a hole in the outline, and the run has to fix it by extending a page or adding a new one. .wiki-state.json is one line: {"last generated sha": "a60e3827...", "generated at": "2026-09-29T14:11:31Z", "mode": "incremental"} The skill always writes this file last, and that one rule is the whole crash-safety story. If a run dies halfway timeout, API error, whatever , the old SHA is still there, so the next run computes the same diff and does the work again. The work gets done a bit later, but it gets done. Three modes incremental runs on every push. It diffs last generated sha..HEAD , maps each changed file to its page, and rewrites each affected page from scratch using the current code. The diff only tells it where to look, and the old page is just a checklist of topics to re-verify, so a page always reads like it was written today no “this was changed to…” edit logs . Then it rewrites the hubs and index tables that changed. audit runs every Monday at 03:23 UTC. Ten incremental runs can each correctly decide “nothing to change here” and still, together, leave a page wrong. So once a week the audit re-derives the whole outline, fixes the structure splits, merges, new pages, deleted pages , and checks every page’s main claims against the code, oldest page first. A page that passes keeps its banner untouched, so a clean audit produces no diff at all. full runs when I ask for it, or automatically when the state files are missing. It writes the outline first, then the leaves, the hubs, the root pages, and the index last, so every level describes pages that actually exist. Incremental also bumps itself up to an audit when the diff touches more than ~40% of the tracked files, or when the recorded SHA doesn’t exist anymore someone rewrote history . Oh, and the 03:23 is on purpose. GitHub’s scheduler gets hammered at the top of the hour, so an odd minute dodges the delay. Keeping it honest The skill’s top priority is literally written as “accuracy beats coverage”. A wiki that confidently describes code that doesn’t exist anymore is worse than no wiki, because humans and agents both trust it. So a big chunk of the skill is rules about that: - Every claim has to be checked by reading the code in the checkout. - Every page starts with a banner with the commit and date it was generated from, so you always know how fresh it is. - Code is referenced by path and symbol, and never pasted in. Anything over ~10 lines is out; the reader has the repo. - Present tense only. No roadmaps, no history, no ticket numbers. If the code looks buggy, the page describes what it does, and doesn’t guess what the author meant. - A fact lives on exactly one page, and every other page links to it. - Secret values never show up, even ones someone committed by accident. The page names the config key and that’s it. - Pages have size limits: leaves are 300 to 1,500 words and must split past 2,500, hubs stay under ~600, and the index under ~800. Before it writes the state file, every run does a self-check: links and anchors resolve, navigation works both ways leaf to hub, hub to index , every file has exactly one owner, the outline matches what’s on disk, and a few greps make sure there are no em dashes, no “now / recently / no longer”, no ticket references, and no bold-label bullets. Yes, I banned em dashes in my generated docs. I have strong feelings about em dashes. The workflow Here’s the flow: push to main Monday 03:23 UTC manual dispatch │ │ pick a mode ▼ │ │ ┌────────────┐ │ │ │ dispatch │ gh workflow run wiki.yml │ │ job │──────────────┐ │ └────────────┘ ▼ ▼ ┌──────────────────────────────────┐ │ wiki job │ │ checkout full history │ │ claude-code-action: /wiki