{"slug": "i-don-t-write-codebase-documentation-anymore", "title": "I don't write codebase documentation anymore", "summary": "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.", "body_md": "Hello everyone 👋\n\nConfession time: in 10+ years of writing software, I have never kept documentation up to date. Not once.\n\nAnd I’ve tried! Confluence spaces, GitHub wikis, a `docs/` folder, READMEs that\nstart strong and stop being true three sprints later. It always goes the same\nway. Someone writes a nice page, the code moves, nobody touches the page, and\nsix months later a new person reads it, believes it, and loses an afternoon.\nUpdating docs always felt like a chore I owed someone, and I pay chores about\nas reliably as you’d expect.\n\nA while back I started reading some really cool wikis that a paid AI service\nhad generated for a few projects. Architecture overviews, flow diagrams, a page\nfor every subsystem, all built from the code. I loved them. What I didn’t love\nwas that they lived on someone else’s platform, and I couldn’t shape what they\nsaid or how they said it. So I thought: *I want my own*. In my repo, in plain\nmarkdown, maintained by an agent, updated on every push.\n\nSo I built it. One of my projects now has a 95-page wiki: about 134,000 words\nand 67 Mermaid diagrams. I didn’t write a single one of those pages, and it\nupdates itself every time something lands on `main`.\n\nThis 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).\n\nQuick caveat before we start, because the title is doing some heavy lifting:\nwhat I stopped writing is *codebase* documentation, the pages that describe\nwhat the code does. There’s still a small set of docs the wiki doesn’t touch,\nand I’ll get to those near the end.\n\n## What it looks like\n\nThe wiki lives in `docs/wiki/` inside the repo, and it’s two levels deep, never\nmore:\n\n```\ndocs/wiki/\n  README.md                  the index: every reader starts here\n  architecture-overview.md   root pages: stuff that spans more than one section\n  getting-started.md\n  api/\n    README.md                a section \"hub\": directory map, a diagram, a table of pages\n    app-and-routes.md        a \"leaf\": one seam of the code\n    auth-and-sessions.md\n    ...\n  web/\n    README.md\n    ...\n  workers/\n  operations/\n  .outline.json              which source files each page owns\n  .wiki-state.json           the commit the wiki was last generated from\n```\n\n(The real one has eight sections, but they’re very specific to the project, so I’m keeping this one generic.)\n\nThe project is a client one, so I can’t show you the real pages. Here’s the top of a leaf page, lightly trimmed:\n\n```\n> Auto-generated by the wiki skill from commit `20c2b08` on 2026-09-28. Do not\n> edit by hand; changes will be overwritten.\n\n# App Bootstrap, Routes and the Error Envelope\n\nThe FastAPI application is assembled in `backend/main.py`: `create_app` builds\nthe app with the `ClerkAuthMiddleware` and the aggregated router, and the\n`lifespan` function verifies Clerk settings, constructs every provider-backed\nservice that was not injected for tests, and fills an empty Clerk mirror before\nserving. [...]\n\n## Key files\n\n| Path                     | Role                                                    |\n|--------------------------+---------------------------------------------------------|\n| backend/main.py          | `create_app` and `lifespan`: middleware, router, ...     |\n| backend/routes/errors.py | Registers the three exception handlers that render ...   |\n| backend/exceptions.py    | `ErrorCode` vocabulary and the `ApiError` family ...     |\n```\n\nAfter that you get sections on how the app gets built, the route families, the\nerror format, and a Mermaid diagram of the startup sequence. At the bottom\nthere’s a `## Related` section linking back to the hub and to the sibling pages\nit depends on. Every page has the same shape, and that turned out to be a big\ndeal for the readers.\n\n## Who actually reads this?\n\nHumans and agents.\n\nFor humans, it’s been great for onboarding. When someone new joins the project\nI send them to the index and the “Reading order” list at the bottom of it, and\nthey get a tour of the codebase that matches what’s on `main` right now. Much\nbetter than whatever Confluence page someone last updated when they felt\nguilty.\n\nBut honestly, the agents are the heavy users. The coding agents working on\nthis repo read it *all the time*. Instead of grepping around for ten minutes\nto figure out how auth works, an agent reads the index, jumps to the right\nhub, and loads the one leaf page it needs. It’s a shortcut into the codebase,\nand a cheap one, since a leaf page is small enough to load whole.\n\nThe index even has a section written just for them:\n\n```\n## For agents\n\nThe lookup path is this index, then a section hub, then a leaf. Each hub's\ndirectory map names the page owning each part of its tree, and\n`docs/wiki/.outline.json` maps every source path to the page that describes it.\n```\n\n## How it works: two files\n\nThe whole thing is two files:\n\n| File | What it is | \n|---|---|\n| `.claude/skills/wiki/SKILL.md` | The instructions: layout, page templates, size rules, three modes, a self-check. About 230 lines of prose, zero code. | \n| `.github/workflows/wiki.yml` | The plumbing: triggers, model choice, the commit step, and a check for runs that got cut off. | \n\nI split them on purpose. The skill never touches git: it reads code and writes\nmarkdown, and that’s it. The workflow never decides what a page says: it runs\nthe skill and commits whatever changed. Because of that, you can drop the skill\ninto any repo, or run it by hand with `/wiki full` in Claude Code, and it\ndoesn’t care who’s calling it.\n\nOn every push to `main`, GitHub Actions starts a headless Claude Code session\nwith exactly one prompt: `/wiki incremental`. The session reads the diff since\nthe last wiki run, figures out which pages describe the files that changed,\nrewrites those pages from the current code, and exits. Then a plain shell step\ncommits `docs/wiki/` back to `main`.\n\n### Pages follow seams\n\nThe first big decision was how to cut a codebase into pages. The skill calls\nthe unit a *seam*: a boundary the code already has, like a package, a service,\na pipeline stage, or a bunch of modules that always change together. Pages\nfollow seams, and never file types or a fixed template like “one page for\nmodels, one for views”.\n\nThe skill also has to find the seams on its own, from `git ls-files` and the\ncode. There’s no hardcoded list of directory names anywhere. On my repo it came\nup with eight sections, including some splits I never asked for: it broke the\nbackend into the core service, identity, the answer pipeline, and the test\nsuites.\n\n### The two state files\n\nThese two files are what make incremental updates possible.\n\n`.outline.json` is the page map. Each page lists `covers` (the globs that make\nthe page suspect when they change) and `seeds` (one to five files to start\nreading from):\n\n```\n{\n  \"file\": \"api/app-and-routes.md\",\n  \"title\": \"App Bootstrap, Routes and the Error Envelope\",\n  \"covers\": [\"backend/main.py\", \"backend/routes/**\", \"backend/exceptions.py\", \"...\"],\n  \"seeds\": [\"backend/main.py\", \"backend/routes/__init__.py\"]\n}\n```\n\nThe 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.\n\n`.wiki-state.json` is one line:\n\n```\n{\"last_generated_sha\": \"a60e3827...\", \"generated_at\": \"2026-09-29T14:11:31Z\", \"mode\": \"incremental\"}\n```\n\nThe 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.\n\n### Three modes\n\n`incremental` runs on every push. It diffs `last_generated_sha..HEAD`, maps\neach changed file to its page, and rewrites each affected page *from scratch*\nusing the current code. The diff only tells it where to look, and the old page\nis just a checklist of topics to re-verify, so a page always reads like it was\nwritten today (no “this was changed to…” edit logs). Then it rewrites the\nhubs and index tables that changed.\n\n`audit` runs every Monday at 03:23 UTC. Ten incremental runs can each\ncorrectly decide “nothing to change here” and still, together, leave a page\nwrong. So once a week the audit re-derives the whole outline, fixes the\nstructure (splits, merges, new pages, deleted pages), and checks every page’s\nmain claims against the code, oldest page first. A page that passes keeps its\nbanner untouched, so a clean audit produces no diff at all.\n\n`full` runs when I ask for it, or automatically when the state files are\nmissing. It writes the outline first, then the leaves, the hubs, the root\npages, and the index last, so every level describes pages that actually exist.\n\nIncremental 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).\n\nOh, 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.\n\n### Keeping it honest\n\nThe 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:\n\n- Every claim has to be checked by reading the code in the checkout.\n- Every page starts with a banner with the commit and date it was generated from, so you always know how fresh it is.\n- Code is referenced by path and symbol, and never pasted in. Anything over ~10 lines is out; the reader has the repo.\n- 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.\n- A fact lives on exactly one page, and every other page links to it.\n- Secret values never show up, even ones someone committed by accident. The page names the config key and that’s it.\n- Pages have size limits: leaves are 300 to 1,500 words and *must* split past\n2,500, hubs stay under ~600, and the index under ~800.\n\nBefore 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.\n\nYes, I banned em dashes in my generated docs. I have strong feelings about em dashes.\n\n## The workflow\n\nHere’s the flow:\n\n```\n   push to main        Monday 03:23 UTC      manual dispatch\n        │                     │               (pick a mode)\n        ▼                     │                     │\n  ┌────────────┐              │                     │\n  │  dispatch  │  gh workflow run wiki.yml          │\n  │    job     │──────────────┐                     │\n  └────────────┘              ▼                     ▼\n                       ┌──────────────────────────────────┐\n                       │ wiki job                         │\n                       │  checkout (full history)         │\n                       │  claude-code-action: /wiki <mode>│\n                       └────────────────┬─────────────────┘\n                                        ▼\n                         did THIS run write the state file?\n                          │                            │\n                         yes                           no\n                          ▼                            ▼\n               commit docs/wiki, rebase,      commit partial pages,\n               push to main                   fail the job (red run)\n```\n\nSome of these details took way more effort than they look like they should.\n\n### Re-dispatching the push\n\n[claude-code-action](https://github.com/anthropics/claude-code-action) doesn’t accept `push` events. It throws\n`Unsupported event type: push` and that’s the end of that. So a tiny\n`dispatch` job (it runs for a few seconds) re-fires every push as a\n`workflow_dispatch`, which is one of only two event types the default\n`GITHUB_TOKEN` is allowed to trigger.\n\nThen there’s a second wall: the action refuses to run for bots by default, and\na run dispatched by `GITHUB_TOKEN` shows up as `github-actions[bot]`. So the\nworkflow sets `allowed_bots: github-actions` to let exactly that one bot\nthrough.\n\n### Avoiding the infinite loop\n\nThe wiki commits to `main`. That’s a push to `main`. Which would trigger the\nwiki… 🤔\n\nIt doesn’t, for two separate reasons. `paths-ignore: docs/wiki/**` means a push\nthat only touches the wiki doesn’t trigger the workflow, and the commit is\npushed with `GITHUB_TOKEN`, which GitHub never lets trigger push-based runs.\nEither one would be enough. I like having both.\n\n### The newest run wins\n\nA concurrency group cancels a run in progress when a newer push comes in, so\nthe wiki is always generated from the latest `main`. This is safe thanks to the\nstate-file-last rule: whatever the cancelled run didn’t finish, the new run\nredoes.\n\n### Different models for different modes\n\nIncremental runs happen on every push, so they should be cheap. Audits and full runs make the structural decisions, so they get the stronger setup. The models and the audit effort level are all repo variables, which means switching models is a settings change and not a commit.\n\nEverything goes through [Lazer Proxy](https://lazertechnologies.com/) (that’s the `ANTHROPIC_BASE_URL` line\nin the workflow). Right now both slots run GLM 5.3. Audits and full runs get\n`max` effort, and incremental runs use the model’s default effort (which, now\nthat I’m writing it down, I should probably bump to `high` lol).\n\n### The commit step\n\nThe commit step is plain shell, and it always runs, even when the Claude step\nfails or times out, so generated pages never get thrown away. It commits as\n`claude[bot]` with `[skip ci]`, then rebases onto the latest `main` and retries\nup to three times, because `main` has probably moved during a run that can take\nover an hour.\n\nIt also checks whether the run actually finished. It compares the state file’s SHA and timestamp with the time the run started, and if this run didn’t write the state file (and the diff wasn’t empty), it still commits the partial pages, since the next run redoes that diff anyway, but then it exits 1 so I get a red run.\n\nThat check exists because of the worst bug in this whole project.\n\n## What broke along the way\n\n### Three trigger designs in one day\n\nThe very first day was all about getting it to run on push at all. I started\nwith claude-code-action, hit the `push` rejection, and switched to calling the\nCLI directly (`claude -p \"/wiki incremental\"`) in one job with a normal push\ntrigger. That worked, and honestly it’s simpler! But it meant owning the CLI\ninstall and version pin myself, and losing the action’s run reports and GitHub\nintegration. So the same day, I went back to the action and built the\nre-dispatch job.\n\nThere’s a research doc in the repo comparing every option I looked at:\n`workflow_run` chaining, `repository_dispatch`, schedule-only, the raw CLI, and\ndispatch plus `allowed_bots`. The `workflow_run` option was sneaky. My CI\nworkflows only run when their own project’s files change, so a push that only\ntouched the README wouldn’t trigger any of them, and the wiki would silently\nskip that push. The workflow header links to that doc, so the next person who\nopens `wiki.yml` and thinks “why is this so weird?” gets an answer.\n\n### The first layout was flat\n\nThe first version of the skill wrote one flat level of pages. Later I\nrestructured it into index, hubs, and leaves, and added\n`\"outline_version\": 2` to the outline. An old outline without that field\nforces a full regeneration automatically, so the upgrade happened on the next\npush without me doing anything.\n\nThat regeneration was also the last time I touched the wiki by hand. Of the 111\ncommits to `docs/wiki/`, 105 are from the bot. My 6 are merge commits, one\nfeature commit that happened to touch the directory, one hand edit on day one,\nand that regeneration.\n\n### Eleven green runs that did nothing\n\nMy favorite bug, in the “I want to throw my laptop into the sea” sense.\n\nA big batch of changes landed at once, and the diff grew past ~100 files. At that size, the model decided (on its own, nobody asked it to) that the smart move was to hand the page rewrites to subagents. Which is very reasonable! In an interactive session that’s exactly what you’d want.\n\nBut in a headless run, subagents launch asynchronously, and the process exits when the main turn ends. So every run went like this: spawn a bunch of agents, then end the turn with some version of “the agents are still running, I’ll resume when they complete”. The process exited, the job reported success, the commit step committed whatever was written so far (between 1 and 12 files per run), and the state SHA never moved.\n\nThis happened eleven times in a row, and all eleven runs were green 🙃 The diff snowballed to about 245 files before one run happened to do everything inline and finish.\n\nThe fix was three changes:\n\n1. `--disallowedTools Agent` in the workflow, so the model can’t spawn\nsubagents at all.\n2. A line in the skill: do every step in your own turn and never hand page writing to subagents or background tasks, because a headless run ends when your turn ends.\n3. The “did this run write the state file?” check from above, so a run like that shows up red.\n\nThe third one is the one I care about most. The subagent thing was a bug, sure, but what really bothered me was eleven green checkmarks telling me everything was fine.\n\n### Nineteen refused commands\n\nThe next problem showed up on an audit that split a page in two and then couldn’t delete the old one.\n\nWithout an explicit allow rule, a headless Claude Code session refuses any\nshell command it can’t prove is safe. That includes `git -C`, anything with a\npipe, small Python helpers, and the `rm` that deletes a page dropped from the\noutline. That audit lost 19 commands this way. The skill has an\n`allowed-tools` list in its frontmatter, but those rules match on command\nprefixes, so `rm docs/wiki/foo.md` was allowed and the exact same delete with\nan absolute path wasn’t.\n\nTwo fixes here. The skill now tells the model to always run commands from the\nrepo root with relative paths, never with `cd` or `git -C` in front. And the\nworkflow allows Bash outright. That second one is a judgment call: it’s fine\nfor me because the prompt is a constant string, the repo is private, and the\njob token can only write repo contents, so there’s nothing untrusted for the\nsandbox to protect against. If your repo is public, think twice before you\ncopy that line.\n\n## The numbers\n\n| Measure | Value | \n|---|---|\n| Pages | 95: 1 index, 4 root pages, 8 section hubs, 82 leaves | \n| Words | ~134,000 | \n| Mermaid diagrams | 67 | \n| Commits to `docs/wiki/` | 111 (105 by the bot) | \n| Workflow runs | 136: 101 succeeded, 28 cancelled, 7 failed | \n\nMost of the cancellations are by design: a newer push cancelled an older run.\n\nHere’s what a normal incremental run looks like. The diff touched 24 of the repo’s 1,029 tracked files. The run took 23 minutes and 162 turns, cost $5.29, and changed 25 files in the wiki: 14 pages rewritten, 4 new pages (existing pages had grown past 2,500 words and had to split), plus the hubs and the index. At the end it left a summary in the CI log, including a list of pages that need the next audit to clean them up.\n\nOn models: an audit on Claude Opus got killed at the\n50-minute timeout I had back then without finishing. I switched to GLM 5.3 at\n`max` effort, and the first audit on that setup finished in 13 minutes for\nabout $4.30.\n\nWhat I don’t have is a monthly total, sadly. The wiki shares its API key with\nthe Claude workflows that review our PRs in CI, so the spend on that key is\nboth of them mixed together, and I can’t tell how much of it is the wiki. The\ncost also depends on how big each diff is and how often you push, so your\nmileage will vary a lot. If you want a rough number for your repo, multiply a\nfew dollars per run by your pushes to `main`, then add one audit per week. I’m\ngiving the wiki workflow its own API key so I can track exactly what it spends,\nand I’ll update the post when I have real numbers.\n\n## What it’s still bad at\n\nIt’s not perfect, so here’s where it falls short.\n\n### Walls of text\n\nThis is the big one, and it’s what I’m tuning next. The skill says paragraphs are at most ~120 words and hubs stay under ~600. My architecture overview has a paragraph that’s 524 words long 😅 Several hubs are between 870 and 1,050 words, and a dozen leaves are past the 1,500-word “you should split this” line.\n\nMy theory: the model follows the hard rules that the self-check measures (it\n*always* splits a page past 2,500 words) and treats the soft ones as\nsuggestions. Agents don’t mind a wall of text. Humans very much do. I think the\nfix is turning more of the soft rules into self-check failures, because\neverything the self-check measures gets fixed, and everything it doesn’t\nmeasure slowly drifts.\n\n### It’s sometimes wrong\n\nI’ve caught pages getting things wrong. I don’t lose sleep over it, because it fixes itself: the next time anyone touches those files, the page gets rewritten from the code, and even if nobody does, the Monday audit re-checks every page. When a human-written page is wrong, it stays wrong until a human notices. This one is on a timer.\n\nI also want to be upfront about what I *haven’t* done: nobody has fact-checked\nthe wiki sentence by sentence. The one independent check I ran confirmed the\nstructure was right: the outline matched what’s on disk, the links worked, and\nno page was behind the files it covers. That tells me the wiki is structurally\nsound, but it doesn’t tell me every sentence is true. For the content, I’m\ntrusting the verify-against-code rule, the self-check, and the weekly audit.\nSo far that’s been good enough for me, but it’s a bet, and you should know\nit’s a bet.\n\nAnd one that made me laugh: the run summary at the end of each CI log (the one thing the self-check doesn’t grep) is full of em dashes and bold-label bullets. Apparently the rules only count when someone is checking.\n\n## What the wiki doesn’t write\n\nBack to the caveat from the beginning.\n\nThe wiki describes what the code does *right now*. It can’t know intent: why we\npicked this design, what we decided in a meeting, how an operator should run a\ndata refresh. So the repo still has a small set of docs that live outside the\nwiki:\n\n- `docs/adr/` , the architecture decision records, with the “why” behind the\nbig calls.\n- `docs/handbook/` , with an operator guide, a runbook, and an architecture\nreference for maintainers.\n- `CONTEXT.md` , the domain glossary (including the words we*don’t* use).\n- `AGENTS.md` , the condensed project context for coding agents.\n\nI don’t write these by hand either, to be clear. I write them with AI. The difference is that I’m driving: the decision or the procedure comes from me (or the team), and the AI helps me turn it into a doc. The wiki doesn’t need me at all.\n\nThe handbook also has a rule, backed by an ADR: if you change an admin page or the data refresh workflow, you update the handbook page that describes it in the same pull request. That rule lives in the agent rules too, so the coding agents follow it.\n\nMy favorite detail: the wiki has a page *about* the handbook, explaining what\nit is, why it’s maintained outside the wiki, and which wiki pages describe the\ncode behind each handbook doc. The wiki documents the docs it isn’t allowed to\ntouch, which I find hilarious.\n\n## Stop writing the docs a machine can write\n\nOK, opinion time.\n\nDocs that describe code are basically a build artifact. We don’t hand-write compiled binaries or minified JS, we generate them from the source every time the source changes. For “how does this work” docs, the source is the code. And now we have something that can read the code and write decent pages about it for a few dollars a run. Asking a person to keep those pages in sync by hand is how you end up with a Confluence graveyard again.\n\nDocs about decisions, intent, and procedure are different. Those come from people, so a person should be in the loop, with AI helping. And that set turned out to be way smaller than I expected: a handful of files and two directories, versus 95 pages I never have to think about.\n\n## Show me the code\n\nBoth files are below, complete. To use them in your repo:\n\n1. Copy `SKILL.md` to`.claude/skills/wiki/SKILL.md` and`wiki.yml` to`.github/workflows/wiki.yml` . Make sure`docs/wiki/` isn’t git-ignored.\n2. Set up model access. My workflow uses a `LAZER_PROXY_API_KEY` secret and a`LAZER_PROXY_BASE_URL` variable because everything goes through our proxy.\nIf you’re calling Anthropic directly, put your API key in a secret, point`anthropic_api_key` at it, and delete the`ANTHROPIC_BASE_URL` line. If you\nuse another gateway, point that line at it instead. The model and effort\nvariables are optional. Without them, the workflow uses Sonnet for\nincremental runs and Opus for audits.\n3. If `main` is protected, let the workflow push (a GitHub App or a ruleset\nbypass), or change the commit step to open a PR.\n4. If your repo is public, rethink two choices I made for a private repo:\n`show_full_output: true` (it dumps the whole transcript into the public run\nlog) and allowing Bash outright.\n\nThen push something! The first run finds no state file and switches to `full`\nby itself. On a big repo that first run takes a while, so go get a coffee ☕\n\nYou can also skip CI entirely: open Claude Code in your repo and type\n`/wiki full`.\n\n### SKILL.md\n\n```\n---\nname: wiki\ndescription: Generate and maintain an agentic codebase wiki in docs/wiki/ (browsable markdown pages with Mermaid diagrams). Use this skill whenever the user asks to build, update, sync, audit, or regenerate the project wiki, codebase documentation, or architecture docs, or whenever it is invoked as /wiki. Also use it when asked \"document this codebase\" or \"keep the wiki up to date\".\nallowed-tools: Read, Grep, Glob, Write, Edit, Bash(git log:*), Bash(git diff:*), Bash(git show:*), Bash(git ls-files:*), Bash(git rev-parse:*), Bash(date:*), Bash(wc:*), Bash(ls:*), Bash(cat:*), Bash(grep:*), Bash(find:*), Bash(jq:*), Bash(python3:*), Bash(rm docs/wiki/:*)\n---\n\n# Codebase Wiki Generator\n\nMaintain a browsable markdown wiki describing this repository in `docs/wiki/`. The wiki is machine-owned: every run may rewrite any page, so treat existing pages as prior output, not as human work to preserve. Write files only. Never run `git add`, `git commit`, or `git push`; committing is the caller's job (CI or the human).\n\nAccuracy beats coverage. Every claim in a page must be something you verified by reading the code in this checkout. A wiki that confidently describes code that no longer exists is worse than no wiki, because readers (human and agent) trust it as context.\n\nReaders are humans browsing on GitHub and agents loading pages as context. Both navigate the same path: index, then section hub, then leaf. Every rule below exists to keep that path short and every page on it accurate.\n\n## Invocation and mode selection\n\nThe invocation is `/wiki [mode]` where mode is `incremental`, `audit`, or `full`. Rules:\n\n1. Run `full` regardless of the requested mode when any of these hold: `docs/wiki/.wiki-state.json` does not exist; `docs/wiki/.outline.json` is missing, unparseable, or lacks `\"outline_version\": 2`.\n2. If no mode is given, run `incremental`.\n3. In `incremental` mode, if `last_generated_sha` is missing or is not a commit in this repo (`git rev-parse --verify <sha>^{commit}` fails, for example after a history rewrite), there is no diff base: escalate to `audit` and say so in your summary. Audit verifies every page against the current code, which covers whatever the lost diff would have shown.\n4. In `incremental` mode, if the diff since the last run touches more than ~40% of tracked source files, escalate to `audit` and say so in your summary.\n\nRuns are time-bounded and the caller may cancel one. Write `.wiki-state.json` last, so a cut-off run leaves the previous SHA in place and the next run re-processes the same diff.\n\nDo every step in your own turn. Never hand page writing to subagents or background tasks: a headless run ends when your turn ends, so work still running elsewhere is lost while the run reports success. Run shell commands from the repository root with repo-relative paths, never prefixed with `cd` or `git -C`: tool allowlists match command prefixes, so `rm docs/wiki/<path>` is permitted where the same removal by absolute path is refused.\n\n## Layout\n\nThe wiki is two levels deep: root and sections.\n\n| Path | Role |\n|---|---|\n| `docs/wiki/README.md` | Index. The only page every reader starts from. |\n| `docs/wiki/<page>.md` | Root page. Spans more than one section. Always present: `architecture-overview.md`, `getting-started.md`. |\n| `docs/wiki/<section>/README.md` | Hub. The landing page for one seam. GitHub renders it when a reader browses into the directory. |\n| `docs/wiki/<section>/<page>.md` | Leaf. One seam within the section. |\n| `docs/wiki/.outline.json` | The page map: the contract that makes incremental updates possible. |\n| `docs/wiki/.wiki-state.json` | Run state. |\n\nA **seam** is a boundary the code already has: a package, a service, a subsystem, a pipeline stage, a bounded set of modules that change together. Pages follow seams, never file types or a generic template.\n\nSections are earned. A section exists when one seam yields two or more leaves. A repo whose whole outline is three to six leaves has no section directories: the index is the only hub and the leaves sit beside it at the root. A seam that fits on one page gets `<section>/README.md` alone, hub and leaf in one file. There is never a third level: if a section wants one, raise the abstraction of its leaves instead. Decide all of this from `git ls-files` and the code, never from a fixed list of directory names.\n\n## `.outline.json`\n\n``` json\n{\n  \"outline_version\": 2,\n  \"root_pages\": [\n    {\n      \"file\": \"architecture-overview.md\",\n      \"title\": \"Architecture Overview\",\n      \"covers\": [\"README.md\", \"AGENTS.md\"],\n      \"seeds\": [\"README.md\"]\n    }\n  ],\n  \"sections\": [\n    {\n      \"dir\": \"api\",\n      \"title\": \"API Service\",\n      \"covers\": [\"api/**\"],\n      \"pages\": [\n        {\n          \"file\": \"api/auth-and-sessions.md\",\n          \"title\": \"Auth and Sessions\",\n          \"covers\": [\"api/src/auth/**\", \"api/src/middleware/session.*\"],\n          \"seeds\": [\"api/src/auth/service.*\"]\n        }\n      ]\n    }\n  ]\n}\n```\n\n- `covers` on a leaf or root page is the set of paths/globs whose changes make that page suspect.\n- `covers` on a section is the fallback for the whole seam: any file in the section's tree that no leaf claims (configs, lockfiles, READMEs, one-file directories) belongs to the hub.\n- `seeds` are the 1 to 5 files to start reading from when writing the page.\n- A section with a single page has `pages: []`; its `README.md` is written from the section's own `covers` and `seeds` (add `seeds` on the section in that case).\n\n**Ownership rule:** every non-excluded tracked path resolves to exactly one page. Resolution is most-specific-glob-wins (the longest matching pattern). Leaf `covers` inside a section must not overlap each other; a path matching two leaves is a defect to fix in the outline, and the self-check reports it.\n\n**Order of writes:** a page is written to disk before its entry is added to `.outline.json`, and a page dropped from the outline is deleted from disk in the same step. A cut-off run must never leave an outline entry without its page, or a page without its entry.\n\nTwo shapes the outline takes. A small single-service repo:\n\n```\ndocs/wiki/\n  README.md\n  architecture-overview.md\n  getting-started.md\n  request-pipeline.md\n  persistence.md\n  background-jobs.md\n```\n\nA monorepo with two packages, one of which fits on a page:\n\n```\ndocs/wiki/\n  README.md\n  architecture-overview.md\n  getting-started.md\n  web/\n    README.md              hub: directory map, diagram, page table\n    routing-and-shell.md\n    auth.md\n    rendering.md\n    tooling-and-tests.md\n  worker/README.md         hub and leaf in one file\n  operations/\n    README.md\n    ci-workflows.md\n    agent-configuration.md\n```\n\n`.wiki-state.json`:\n\n``` json\n{\"last_generated_sha\": \"<full sha>\", \"generated_at\": \"<ISO 8601 UTC>\", \"mode\": \"incremental\"}\n```\n\nGet the SHA with `git rev-parse HEAD`. Rewrite this file at the end of every successful run; audit and full runs rewrite it even when no page changed. The caller reads its SHA and `generated_at` to tell a finished run from a cut-off one.\n\n## Page sizing and splitting\n\nA page is one seam, sized so a reader finishes it in one sitting and an agent can load it whole:\n\n- A leaf covers typically 3 to 15 project-authored files and runs 300 to 1,500 words. Over 1,500 words is a split candidate; over 2,500 words splits in the same run, whatever the mode, with the new leaf added to the outline and the hub.\n- A leaf has at most 7 H2 sections, and its H2s share one concern. Concerns are stack-neutral: bootstrap and configuration; auth and access control; request handling and routing; domain logic; data model and persistence; UI and rendering; external integrations; safety and compliance; tooling and tests. A page whose H2s straddle two concerns splits along that line. Read the concerns off the code; the list above is a vocabulary, not a template.\n- A paragraph is at most ~120 words. Inventories (test files, routes, config keys, commands, environment variables) are tables.\n- A section with more than ~8 leaves splits into two sections. A one-file directory is a row in the hub's directory map, never a page.\n- Hubs run under ~600 words. The index runs under ~800.\n- Vendored and generated directories get one row in the hub's directory map and one sentence on how they are produced, plus the local maintenance policy when the repo documents one (in its agent rules or README). Read that policy; never assume one.\n\n## Page templates\n\nEvery page starts with this banner, values filled in:\n\n``` markdown\n> Auto-generated by the wiki skill from commit `<short sha>` on <YYYY-MM-DD>. Do not edit by hand; changes will be overwritten.\n```\n\n**Index** (`docs/wiki/README.md`), in order:\n\n1. Banner, H1, overview: what the project is and how it runs, two paragraphs at most.\n2. One table per section (and one for root pages) with columns `Page | Summary | Key paths`. Summary is one sentence; key paths are the two or three directories the page is about.\n3. `## Reading order`: a numbered list of 4 to 6 pages for a newcomer.\n4. `## For agents`: two sentences stating that the lookup path is index, hub, leaf, and that `.outline.json` maps source paths to pages.\n\n**Hub** (`docs/wiki/<section>/README.md`), in order:\n\n1. Banner, H1, orientation: what the seam is and where it lives, one paragraph.\n2. `## Directory map`: a table `Path | What lives there | Page` with one row per top-level subdirectory and config file of the seam. Vendored and generated directories are rows too, marked as such. The Page column links the leaf that owns the row, or says \"this page\".\n3. One Mermaid diagram of the seam: module dependencies or the main flow through it.\n4. `## Pages`: a table `Page | Summary`.\n5. `## Cross-cutting`: links to the root pages and other sections this seam touches.\n6. A final line linking back to the index: `Back to [the index](../README.md).`\n\n**Leaf** (`docs/wiki/<section>/<page>.md`, and root pages), in order:\n\n1. Banner, H1, orientation: one paragraph on what this seam does and where its code lives.\n2. `## Key files`: a table `Path | Role` of the 3 to 10 files that matter most, each path a relative link into the repo (one `../` per directory level between the page and the repo root, so from a section directory it is `../../path/to/file`).\n3. H2 sections describing purpose, structure, key flows, and interactions. A Mermaid diagram wherever the page describes a flow, a state machine, or a handoff between components; a sentence wherever a sentence is enough.\n4. `## Related`: links to the hub, the sibling pages this page references, and the pages in other sections it depends on. Root pages link the index here instead of a hub.\n\n## Page conventions\n\n- Reference code by path and symbol (`src/auth/service.py`, `SessionStore.refresh()`), never with large pasted code blocks. Snippets over ~10 lines defeat the purpose; the reader has the repo.\n- Link pages with relative links (`[Auth](auth.md)`, `[Worker](../worker/README.md)`), anchors allowed (` auth.md#session-refresh`).\n- Describe what the code does today, in the present tense, as if the page were written fresh this run. Roadmaps, intent, and history belong in commits and tickets. If behavior looks like a bug, describe the behavior, not your guess about what was meant.\n- **Single source.** A fact lives on exactly one page; other pages link to it. When two pages both need a fact, the owner is the page whose `covers` includes the file that defines it.\n- Never copy values of secrets, tokens, API keys, connection strings, or `.env` contents into a page, even values found committed in the repo. Name the config key, never the value.\n\nWriting style: plain, specific, low ceremony. Concretely:\n\n- Punctuate with commas, colons, semicolons, periods, and parentheses. Em dashes are banned; the self-check greps for them.\n- Say what the thing does: \"X does Y\" over \"X serves as / is responsible for Y\". Show that something is simple rather than asserting it.\n- Plain vocabulary: delve, leverage, robust, seamless, streamline, comprehensive, and \"plays a crucial role\" are banned, as are bold-label bullets (`**Performance**: ...`) and negative parallelism (\"it's not X, it's Y\").\n\n## What to exclude\n\nSkip vendored dependencies, generated code, lockfiles, build output, fixtures/snapshots, minified bundles, and `docs/wiki/` itself. Use `git ls-files` as the source of truth for what is tracked, then apply judgment: if a directory is clearly machine-written (codegen output, migrations dumps, registry-pulled components), document that it exists and how it is produced, not its contents.\n\n## Full mode\n\n1. Inventory: `git ls-files`, apply exclusions, read the README and obvious entrypoints (main modules, app factories, CLI definitions, CI config) to understand what the project is.\n2. Write `.outline.json`: find the seams, decide which earn sections, assign `covers` so the ownership rule holds, pick `seeds`.\n3. Write each leaf and single-page section: start from its seeds, then explore with Read/Grep/Glob (follow imports, find callers, check config defaults) until you can describe the seam's purpose, structure, key flows, and interactions. Verify claims against code you actually read. Apply the sizing rules as you go; split before writing rather than after.\n4. Write each hub from its finished leaves, then the root pages, then the index last, so each reflects the pages that exist.\n5. Remove any `docs/wiki/**/*.md` not in the new outline (`rm docs/wiki/<path>`); an emptied directory may stay. Run the self-check. Write `.wiki-state.json`.\n\n## Incremental mode\n\n1. Read `.wiki-state.json` and `.outline.json`. Compute changes: `git diff --name-status <last_generated_sha>..HEAD -- . ':(exclude)docs/wiki'`. If the diff is empty, update nothing (you may still rewrite `.wiki-state.json`) and report a no-op. Commits after `last_generated_sha` that touch only `docs/wiki/` are not evidence that their diff was processed: a cut-off run commits partial pages without advancing the state file. The state file is the only record of what was processed; never narrow the diff by reasoning about those commits.\n2. Resolve each changed path to its owning page via the ownership rule. A path that resolves to no page means the outline has a gap: assign it to the best-fitting existing page (extend its `covers`) or, if it belongs to a genuinely new seam, add a page or section.\n3. For each affected page: read the diff for its files (`git diff <last_sha>..HEAD -- <paths>`) to learn where to look, then rewrite the whole page from the current code, using the old page only as a checklist of topics to re-verify. A page is a fresh description, never an edit log. Renames and deletions are reflected; a page whose entire subject was deleted is removed from disk and from the outline. Apply the sizing rules: a page that grew past its bounds splits now.\n4. Rewrite the hub of every section whose leaves changed (directory map and page table included). Rewrite the index tables whenever any page was written, added, removed, or retitled. Both are short; keeping them current on every run is what stops the index going stale.\n5. Run the self-check. Write `.wiki-state.json`.\n\n## Audit mode\n\nThe weekly safety net. Incremental runs can each correctly conclude \"no page change needed\" while ten of them together leave a page wrong. Audit exists to catch that drift plus structural rot.\n\n1. Re-derive an outline from the current tree as if running full mode, but don't write it yet. Compare with `.outline.json`: seams that grew enough to deserve their own page or section, pages whose subject shrank or vanished, tracked paths no page owns, leaves whose `covers` overlap, pages outside the sizing bounds. Apply the structural fixes (add/split/merge/remove pages; new or split pages are written as in full mode). Structural decisions are sticky: a split or merge made by an earlier run stands unless the code moved or a hard bound is exceeded, so audits do not oscillate on word counts alone.\n2. For every surviving page, oldest banner SHA first: confirm each path in `covers` still exists, then verify the page's main claims against the current code (entry points it names, flows it describes, config keys it references). Rewrite what drifted. A page that passes keeps its banner untouched, so a clean audit produces no churn; say which pages passed.\n3. Rewrite hubs and the index if the page set changed. Run the self-check. Write `.outline.json` and `.wiki-state.json`.\n\n## Self-check\n\nRun before writing `.wiki-state.json` in every mode. Fix what fails; anything you cannot fix goes in the summary.\n\n| Check | How |\n|---|---|\n| Links resolve | For every `](<path>.md` target, Glob the file relative to the linking page; for anchors, confirm a heading in the target produces that slug. |\n| Navigation is two-way | Every leaf is in its hub's page table and links the hub in `## Related`; every hub and root page is in the index; every hub links the index. |\n| Present tense | Grep pages for `—`, for `\\b(now|recently|no longer|previously|used to)\\b`, for ticket and PR references (`\\b[A-Z]{2,}-\\d+\\b`, `#\\d+\\b`), and for bold-label bullets (`^- \\*\\*[^*]+\\*\\*:`). Rewrite each hit. |\n| Sizes | `wc -w` on every page against the bounds in Page sizing. |\n| Ownership | Every tracked, non-excluded path resolves to exactly one page. Enumerate with `git ls-files <glob>` per pattern; list gaps and overlaps. |\n| Outline matches disk | Every page in `.outline.json` exists on disk, and every `docs/wiki/**/*.md` (index and hubs included) is in the outline. Delete strays with `rm docs/wiki/<path>`; never leave a redirect stub in place of a deletion. |\n| Banners | Every page written this run carries `git rev-parse --short HEAD` and today's date. |\n\n## Reporting\n\nEnd every run with a short summary: mode actually run (and why, if escalated); the page tree with word counts; pages created, updated, deleted, and verified unchanged; self-check results (gaps, overlaps, unresolvable links); whether `.wiki-state.json` was written; and anything that needs a human (huge uncovered directory, suspected bug, unparseable state). Keep it to a screen; it lands in a CI log.\n```\n\n### wiki.yml\n\n```\n# Generated codebase wiki (docs/wiki/), maintained by the /wiki skill.\n#\n# Runs anthropics/claude-code-action. The action doesn't accept push events,\n# so a push to main re-enters this workflow as a workflow_dispatch (one of\n# the two event types the default GITHUB_TOKEN may still trigger), and\n# allowed_bots lets that GITHUB_TOKEN-dispatched run pass the action's\n# human-actor check. Full rationale and the failure history:\n# docs/agents/research/claude-automation-on-push.md\n#\n# Triggers:\n#   - push to main, re-dispatched (incremental: diff since last wiki commit)\n#   - Mondays 03:23 UTC (audit: re-derive outline, catch drift)\n#   - manual dispatch with a mode override\n#\n# Loop safety (both hold, either alone is enough):\n#   - paths-ignore: pushes that only touch docs/wiki/ don't trigger this\n#   - the commit step pushes with the default GITHUB_TOKEN, and GitHub never\n#     creates push-triggered runs for events caused by that token\n#\n# Setup per repo:\n#   1. Copy .claude/skills/wiki/ and this file into the repo, and make sure\n#      docs/wiki/ is not git-ignored (this repo ignores docs/* with exceptions).\n#      The skill writes a two-level tree (index, section hubs, leaf pages) and\n#      escalates to a full run on its own when docs/wiki/.outline.json is\n#      missing or predates the current outline format.\n#   2. Set the LAZER_PROXY_API_KEY secret and the LAZER_PROXY_BASE_URL Actions\n#      variable (org-level recommended so repos don't each need a copy).\n#      Optional model overrides: LAZER_PROXY_WIKI_MODEL for incremental runs\n#      (cheap, every push) and LAZER_PROXY_WIKI_FULL_MODEL for full and audit\n#      runs (structure and verification decisions, worth a stronger model).\n#      LAZER_PROXY_WIKI_FULL_EFFORT (low, medium, high, xhigh, max) sets the\n#      effort level for full and audit runs; unset leaves the model default.\n#      Installing the Claude GitHub App is optional: without it the action\n#      falls back to the job's default GITHUB_TOKEN.\n#   3. If main is a protected branch, allow this workflow to push (GitHub App\n#      or Actions bypass in the ruleset) or switch the commit step to a PR.\n\nname: Wiki\n\non:\n  push:\n    branches: [main]\n    paths-ignore:\n      - \"docs/wiki/**\"\n  schedule:\n    - cron: \"23 3 * * 1\" # Mondays 03:23 UTC; odd minute to dodge the top-of-hour delay\n  workflow_dispatch:\n    inputs:\n      mode:\n        description: Generation mode\n        type: choice\n        options: [incremental, audit, full]\n        default: incremental\n\njobs:\n  # claude-code-action validates the event type and fails on push\n  # (\"Unsupported event type: push\"), so a push re-enters this workflow as a\n  # workflow_dispatch. Dispatching with the default GITHUB_TOKEN works:\n  # workflow_dispatch and repository_dispatch are the two events exempt from\n  # GitHub's no-retrigger rule for that token.\n  dispatch:\n    if: github.event_name == 'push'\n    runs-on: ubuntu-latest\n    permissions:\n      actions: write\n    steps:\n      - run: gh workflow run wiki.yml --ref main -f mode=incremental\n        env:\n          GH_TOKEN: ${{ github.token }}\n          GH_REPO: ${{ github.repository }}\n\n  wiki:\n    if: github.event_name != 'push'\n    runs-on: ubuntu-latest\n    # A newer run cancels an in-progress one, so the wiki is always generated\n    # from the latest main. Cancelling mid-run is safe: the skill writes\n    # .wiki-state.json last, so the replacement run re-processes the same\n    # diff. Job-level (not workflow-level) so the seconds-long dispatch job\n    # doesn't churn the group.\n    concurrency:\n      group: wiki-generate\n      cancel-in-progress: true\n    timeout-minutes: 85\n    permissions:\n      contents: write\n      id-token: write\n    env:\n      WIKI_MODE: ${{ inputs.mode || (github.event_name == 'schedule' && 'audit') || 'incremental' }}\n    steps:\n      - uses: actions/checkout@v7.0.1\n        with:\n          # Full history: incremental mode diffs against the SHA recorded\n          # in docs/wiki/.wiki-state.json, which can be arbitrarily old.\n          fetch-depth: 0\n\n      # The commit step compares this against the state file's generated_at\n      # to tell a state file this run wrote from one left over from before.\n      - id: start\n        run: echo \"at=$(date -u +%Y-%m-%dT%H:%M:%SZ)\" >> \"$GITHUB_OUTPUT\"\n\n      - uses: anthropics/claude-code-action@v1.0.235\n        # Below the job's 85-minute limit so the commit step (if: !cancelled())\n        # still has time to push whatever was generated when a run hits this\n        # ceiling. An audit of the current tree needs more than 50 minutes.\n        # Committing a truncated run is safe: the skill writes\n        # .wiki-state.json last, so a killed run leaves the previous SHA in\n        # place and the next run re-processes the same diff.\n        timeout-minutes: 75\n        with:\n          anthropic_api_key: ${{ secrets.LAZER_PROXY_API_KEY }}\n          # Dispatched runs are initiated by GITHUB_TOKEN, which the action's\n          # human-actor check sees as the github-actions bot.\n          allowed_bots: github-actions\n          # Stream Claude's full transcript into the run log. By default the\n          # action prints only the init and final-result messages, so a\n          # multi-minute generation looks stalled. The prompt is a constant\n          # string against our own repo, and the repo is private, so there is\n          # no untrusted output to hide.\n          show_full_output: true\n          prompt: \"/wiki ${{ env.WIKI_MODE }}\"\n          # Incremental runs happen on every push and only touch the pages a\n          # diff points at; full and audit runs decide the page structure and\n          # verify every page, so they get the stronger model.\n          #\n          # Agent is disallowed because subagents launch asynchronously and a\n          # headless run ends with the main turn: the model would hand page\n          # rewrites to subagents, end its turn to wait for them, and the run\n          # would exit \"success\" having committed almost nothing. Eleven runs\n          # did exactly that on 2026-09-15.\n          #\n          # Bash is allowed outright. Without an allow rule the action runs in\n          # default permission mode, where a headless session refuses any\n          # command it cannot statically clear: git diffs prefixed with cd or\n          # -C, python and node helpers, pipelines, and the rm that removes a\n          # page dropped from the outline (an audit on 2026-09-16 lost 19\n          # commands this way and could not delete a split page). The prompt\n          # is a constant, the repo is private, and the job token can only\n          # write repo contents, so there is nothing for the sandbox to guard.\n          claude_args: |\n            --model ${{ env.WIKI_MODE == 'incremental' && (vars.LAZER_PROXY_WIKI_MODEL || 'claude-sonnet-5') || (vars.LAZER_PROXY_WIKI_FULL_MODEL || 'claude-opus-5') }}\n            --allowedTools \"Read,Write,Edit,Glob,Grep,Bash\"\n            --disallowedTools Agent\n            ${{ env.WIKI_MODE != 'incremental' && vars.LAZER_PROXY_WIKI_FULL_EFFORT && format('--effort {0}', vars.LAZER_PROXY_WIKI_FULL_EFFORT) || '' }}\n        env:\n          # Routes all inference through Lazer Proxy (org-level Actions variable).\n          ANTHROPIC_BASE_URL: ${{ vars.LAZER_PROXY_BASE_URL }}\n\n      - name: Commit wiki updates\n        # Run even when the Claude step fails or times out, so generated\n        # changes aren't dropped; the job still reports the step failure.\n        if: ${{ !cancelled() }}\n        env:\n          RUN_STARTED_AT: ${{ steps.start.outputs.at }}\n        run: |\n          # The skill writes .wiki-state.json last, so a state file this run\n          # did not write means the run was cut off before it finished\n          # (timeout, API error, or the model ending its turn early). The\n          # only legitimate skip is an incremental run whose diff was empty.\n          # The partial pages are still committed below, because the next\n          # run re-processes the same diff, but the job fails so the gap is\n          # visible instead of buried in a green run.\n          head=\"$(git rev-parse HEAD)\"\n          previous=\"$(git show HEAD:docs/wiki/.wiki-state.json 2>/dev/null | jq -r '.last_generated_sha // empty')\"\n          recorded=\"$(jq -r '.last_generated_sha // empty' docs/wiki/.wiki-state.json 2>/dev/null || true)\"\n          generated=\"$(jq -r '.generated_at // empty' docs/wiki/.wiki-state.json 2>/dev/null || true)\"\n          state_written=true\n          if [ \"$recorded\" != \"$head\" ] || [ -z \"$generated\" ] || [[ \"$generated\" < \"$RUN_STARTED_AT\" ]]; then\n            state_written=false\n          fi\n          run_incomplete=false\n          if [ \"$state_written\" = false ]; then\n            if [ \"$WIKI_MODE\" != incremental ] || [ -z \"$previous\" ] \\\n               || ! git rev-parse --verify --quiet \"${previous}^{commit}\" >/dev/null \\\n               || ! git diff --quiet \"$previous\" HEAD -- . ':(exclude)docs/wiki'; then\n              run_incomplete=true\n              echo \"::error::docs/wiki/.wiki-state.json records '${recorded:-nothing}' at '${generated:-no time}' but this ${WIKI_MODE} run started at ${RUN_STARTED_AT} from ${head}; the wiki run did not finish.\" >&2\n            fi\n          fi\n          if [ -n \"$(git status --porcelain docs/wiki)\" ]; then\n            # Author only; the push still uses GITHUB_TOKEN, which is what\n            # the loop-safety rule above depends on.\n            git config user.name \"claude[bot]\"\n            git config user.email \"209825114+claude[bot]@users.noreply.github.com\"\n            git add docs/wiki\n            git commit -m \"docs(wiki): update generated wiki [skip ci]\"\n            # Main may have moved during the (up to 75-minute) Claude run.\n            # Rebase onto the latest main and retry; our commit only touches\n            # docs/wiki, so conflicts are only possible against another wiki\n            # commit, which the concurrency group already serializes.\n            pushed=false\n            for attempt in 1 2 3; do\n              if git pull --rebase origin main && git push origin main; then\n                pushed=true\n                break\n              fi\n              git rebase --abort 2>/dev/null || true\n              sleep 10\n            done\n            if [ \"$pushed\" != true ]; then\n              echo \"Failed to push wiki updates after 3 attempts.\" >&2\n              exit 1\n            fi\n          else\n            echo \"No wiki changes.\"\n          fi\n          if [ \"$run_incomplete\" = true ]; then\n            exit 1\n          fi\n```\n\n## Was it worth it?\n\nYes. This started as an itch: I liked someone else’s generated wikis and I wanted one that was mine. Now it’s the first thing I send to new people on the project, and the thing every agent in the repo reads before touching anything. It cost me one weird day of GitHub Actions trigger archaeology, eleven green runs that lied to me, and a few dollars per push.\n\nI still need to fix the walls of text. When I do, I’ll update the skill in this post.\n\nIf you set this up in your own repo, [let me know](https://rogs.me/contact) how it goes! I’m really\ncurious to see what seams it finds in codebases that aren’t mine.\n\nSee you in the next one!", "url": "https://wpnews.pro/news/i-don-t-write-codebase-documentation-anymore", "canonical_source": "https://rogs.me/2026/09/i-dont-write-codebase-documentation-anymore/", "published_at": "2026-09-30 16:28:32+00:00", "updated_at": "2026-09-30 16:49:59.295375+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "generative-ai", "developer-tools"], "entities": ["GitHub", "Confluence", "FastAPI", "ClerkAuthMiddleware", "Mermaid"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/i-don-t-write-codebase-documentation-anymore", "markdown": "https://wpnews.pro/news/i-don-t-write-codebase-documentation-anymore.md", "text": "https://wpnews.pro/news/i-don-t-write-codebase-documentation-anymore.txt", "jsonld": "https://wpnews.pro/news/i-don-t-write-codebase-documentation-anymore.jsonld"}}