The kolu Atlas
kolu's in-repo second brain — what it is, how it works, how it compares to Claude Code Artifacts, and the plan to scale it.
This is the Atlas’s note about itself — authored as MDX, living in the Atlas it describes (
docs/atlas/), rendered to a self-containeddocs/atlas/dist/meta.htmlyou read in the Code tab. No dev server. It covers the whole thing in one place: what the Atlas is and the rule for what goes in it, how it’s built, how it compares to Artifacts in Claude Code (cloud-hosted, governed pages generated from a session — not a substitute for this), and the plan to scale it without losing what makes it ours.
What the Atlas is
Premise: you already have a second brain — assign roles, don’t build a new system. kolu’s existing stores:
- GitHub Issues — lightweight, living nodes.
- In-repo docs (the Atlas) — substantial, structured artifacts.
- The blog — the public stream.
- The Code tab — renders + annotates both.
The one routing rule decides where a thing goes:
Substantial, structured artifact — or lightweight, transient node?
| Substantial → an Atlas note | Lightweight → a GitHub Issue |
|---|---|
| Proposals, designs, features, analyses, bug investigations, history | Quick bug tickets, tasks, roadmap items, questions |
- Maturity is a tag, not a divider —
seedling→budding→evergreen; never a location, never a routing axis. - Living ≠ frozen — a long-lived plan evolves for months and is still an Atlas note, never “frozen”.
- The boundary blurs by design — a concept can hold both a note and an issue.
- Extract, don’t sync — when an issue thread becomes the source of truth, lift its summary into a note.
The surfaces it sits between:
| Surface | Where | Role |
|---|---|---|
| Public | the blog (kolu.dev) + per-release changelog | outward-facing; one post per release |
| Atlas | docs/atlas/ → docs/atlas/dist/ |
the working brain; markdown/MDX notes, internal-first |
History isn’t a third place — it’s the Atlas over time: a settled note is just
evergreen, git is the history, the changelog is the release artifact.
How it works
docs/atlas/ is its own little Astro project — decoupled from the public
website/ (different audience + cadence), with its committed dist/ folded into
kolu.dev/atlas/ at the website’s build.
- Author markdown/MDX, get HTML — write
.md/.mdx+ frontmatter; Astro renders. - One layout + theme — no per-file CSS (the old HTML notes duplicated ~76 KB).
- Generated graph from frontmatter +
parents— categories aremocnotes, not a hand-curated map (see Navigation). - Self-contained committed output — each
dist/<slug>.htmlinlines its styles and cross-links with relative hrefs → previews in the Code tab, no server; the same bytes serve the public mirror. Those same committed pages now also unfurl as a social card on the public mirror — every page emits Open Graph + Twitter Card tags and a favicon, sharing one 1200×630og.pngbuilt around the Atlas logo (a confident “A” drawn as a tiny knowledge graph: the legs are edges, the terminals are nodes, the amber apex is the north-star hub) (docs/atlas/src/layouts/AtlasLayout.astro). draft: truehides a half-baked note from the index but keeps it on disk for agents.
Format — markdown prose + MDX components
- Prose in markdown — the default.
.mdxwhen a note needs more — import typed Astro/TS components, use them inline. No raw HTML, no per-note CSS.
| Why markdown for prose | Evidence |
|---|---|
| Far fewer tokens — paid on every agent read | Cloudflare: ~80% fewer tokens md vs html |
| The CLIs kolu runs prefer it | Claude Code & OpenCode send Accept: text/markdown |
| Renders where it matters | github.com renders .md; the Code tab renders it (#1093) |
Navigation — one graph, derived edges
- One entry point: a graph, not a tree. The Atlas index (
./index.html) is a force-directed graph of every note, laid out at build time and baked to a self-contained SVG. A category is just a note markedmoc: true(Bugs · Features · Analysis · Comparisons · Reference) — there is nokindenum and no synthetic nodes. Every note is filed under an index note through the one edge mechanism,parents, so the graph is one connected piece (no orphans) and you add a category by writing anothermocnote (Comparisons was added exactly that way). A title search filters the graph live — type a note’s title and the matching nodes stay lit while the rest dim (docs/atlas/src/components/ForceGraph.astro), so finding a note doesn’t yet need the full-text index that’s still future work. parents+ same-directory./slug.htmllinks are the only edges — a note lists one or moreparents(its index note and/or topical hubs) and references siblings in prose; both become graph edges and Maps of Content clusters. No adjacency is authored beyond the links a note already has.- Flat, ancestry-free slugs — the filename is a handle, not a path; a note’s connections are metadata (
parents) and prose links, not the filename. - Maps of Content = the index notes. Below the graph, each
mocnote gets a card listing every note filed under it; together they name every note, so the whole Atlas is reachable with no JS. (High-degree topical notes likeelectricityare emphasized in the graph itself, but only the index notes get cards.) Amaturitydot rides each row; drafts are hidden from the build. A nested ToC (<Toc>) is auto-inserted from each note’s headings. - Backlinks are derived, never authored — a build pass inverts every note’s
./slug.htmllinks +parentsinto a Referenced by list on each page (the inbound half of the same link graph), reusing the edges the Atlas already has rather than a hand-keptbacklinks:field. An internal./slug.htmlpointing at a missing note fails the build — a dead note link surfaces at build time, not as a 404 in the committed dist (#1426); general/external link checking still belongs to a generic linter.
Proposals — the contributor intake lane
- A proposal is an Atlas note filed under its real index (
parents: [feature],[bug], …) carryingstatus: proposed— it shows in that index’s card, flagged proposed. There’s no separate proposal category; the status badge is the queue. Contributors open an Atlas PR; the flow lives inCONTRIBUTING.md. - Accepting = a status flip. On acceptance a maintainer sets
status: accepted(thenimplemented); the index parent was right from the start, so nothing moves. The note stays living — git is the record, so there’s no frozen copy and no separate numbered log. proposed → accepted → implemented → supersededis a lifecycle (thestatusfield); supersession links viaparents+ status. Nodocs/proposals/and nodocs/decisions/dir needed.
The component kit
Because a note can be .mdx, every component below is a live import from
docs/atlas/src/components/ — rendered here, not screenshotted. Props are
typechecked at build.
Inline chips
Small typed references for the prose:
| Component | Live | Usage |
|---|---|---|
<PrLink> — GitHub PR, repo baked in |
#1095 | <PrLink pr={1095} /> |
<Issue> — GitHub issue |
#951 | <Issue n={951} /> |
<Commit> — short sha → commit |
7ec2566 | <Commit sha="7ec2566a" /> |
<Cite> — a linked file:line |
docs/atlas/astro.config.mjs:14-19 | <Cite file="…" lines="14-19" /> |
<Kbd> — a keyboard chord |
Ctrl +B | <Kbd keys="Ctrl+B" /> |
Block components
<Callout> — a typed box with markdown inside. kind: note · accent · good · warn · danger. Usage: <Callout kind="warn" title="…">body</Callout>.
<Terminal> — a faux transcript. Usage:
<Terminal title="…" lines={["$ cmd", "output"]} />; in lines, $ is a
prompt + command, # a comment, anything else is output.
<Svg> + <D2> — an architecture diagram, inlined as self-contained SVG:
hand-author the SVG (<Svg svg={…} />, the pipeline above) for full visual
control, or let <D2> lay one out from a graph DSL. <AtlasMockup> — a
one-off, self-contained HTML + inline-SVG prototype, for when a note needs
something markdown can’t draw:
<Toc> — a nested, auto-inserted table of contents (the Contents box at
the top), built from the note’s headings; notes never import it. <Roadmap> +
<Milestone> — a status-marked roadmap (done ✓ · now ▸ · next ○); see
The roadmap below for the live one.
Bring your own component
A note isn’t limited to the shared kit — but a note-local component lives in the
.mdx itself, never as a separate file (it keeps the note self-contained;
src/components/ is reserved for components reused across notes). Two ways:
- Inline
export constat the top of the.mdx. Defined here asSpark, used live: inline. - Raw inline markup — for a true one-off, drop JSX inline with no named component: raw inline JSX.
Promotion path: inline → shared kit, once a component proves reused (and earns
scoped styles + typed props as a real .astro).
Two jobs, not substitutes
With Artifacts in Claude Code shipped, the obvious question is whether it replaces the Atlas. It doesn’t — the clean tell is the unit each optimizes. The Atlas optimizes the corpus — a navigable, ever-living tree of interlinked notes an agent treats as durable memory, reviewed through the same PR/diff machinery as code. Artifacts optimize the single page — one session’s investigation handed as a governed, link-shareable snapshot to a teammate (often a non-engineer), deliberately not a searchable library.
| Axis | kolu Atlas | Claude Code Artifacts |
|---|---|---|
| Locality | Source and built dist/<slug>.html live in Git. No server, no account, no vendor to read a note. |
Cloud-only on Anthropic infra at a private claude.ai/code/artifact/<uuid> URL. No self-host (cannot be forked). |
| Format / agent cost | Authored as markdown/MDX; structure from a typed kit. The source is what agents re-read — markdown token rates, the format CLIs prefer. | Generated styled HTML; Anthropic’s own docs note it’s more token-intensive to re-ingest at scale. |
| Render surface | Two from one build: Kolu’s Code tab (local, offline) and kolu.dev/atlas/. | One: the claude.ai viewer behind org auth. No in-IDE embed, no public URL. |
| Organization | A graph over the notes + their parents; categories are moc notes, backlinks derived. No full-text search yet. |
Flat single-author gallery, opaque UUID keys; “a capture of work, not an application.” |
| Versioning | Git is the history — full blame/diff/PR review. | First-class in-product versions at one URL + restore; but no diff/blame, and a stray session silently forks. |
| Access control | None at note level — internal repo + a public mirror. | Compliance-grade: private-by-default, org-auth only, never public; audit + retention + Compliance API. |
| Discovery at scale | Deterministic tree at ~50 notes; no full-text search UI. | Weak by design — flat galleries, no search/folders. |
| Audience | Internal engineering + the agents themselves. | Cross-role org — explicitly legal, security, SRE, management. |
| Cost / gating | Free, ungated, vendor-independent. | Beta, Team/Enterprise only; interactive claude.ai login; extra tokens per generate. |
| Longevity | Durable, portable, offline, CI-checked. You own it forever. | Vendor-bound; a retention policy auto-deletes; export the HTML to survive. |
Where the Atlas genuinely wins: it renders locally in Kolu with zero network;
markdown/MDX is the cheapest thing an agent can re-read (~80% fewer tokens than
HTML — Cloudflare’s figure, see Format above; it’s the format CLIs request); Git
is the history; and it’s graph-able by construction, because note-to-note edges
already exist as plain ./slug.html links. Where Artifacts win, decisively:
enterprise governance, cross-role reach to people who will never open the repo,
zero-build cloud sharing, and in-product version restore for non-git users.
Scaling the Atlas
The worry is that the Atlas won’t survive going from one repo / ~50 notes to many projects. It’s real, but mostly contingent — and the cheap within-repo wins come first.
What breaks at multi-project scale
Ranked by real bite. The first cluster is critical only under a multi-team / separate-repo / confidential-project future the repo hasn’t actually adopted; the second bites even a single growing repo.
Contingent on going multi-repo / multi-team:
- Flat slug = filename = URL. Two projects’
release-workflow.mdxcollide. Within one repo it’s a prefix convention, not a wall. - No project axis (docs/atlas/src/content.config.ts) — project A’s and B’s bugs file under the same
bugindex and interleave. - Relative cross-links are load-bearing for both render targets (the Code-tab iframe and the
cp -r distin website/default.nix). A note in another repo isn’t a sibling file, so./other.html404s — the sharpest real constraint on any federation, and the reason not to build it speculatively. - No access control + unconditional public mirror. website/default.nix folds all of
dist/into/atlas/on merge;draft:trueonly hides from the index (it still builds and publishes). Public-by-merge is the intended contract today — and a breach the moment a confidential project lands.
Bites even a single growing repo:
- Committed
dist/churn — ~71 KB of minified HTML per note; every edit re-emits the whole file, andci::atlas-syncdoes a full build + a scrambled-TZ idempotency build, so CI cost scales with note count. This argues against inlining a heavy graph into every page. - No full-text search UI — the graph + hub cards are the browse at ~50 notes; at a few hundred there’s no escape hatch.
- One generated entry page, no pagination — the index cards (every note listed) become an unscrollable wall at 500+.
- The force graph turns into a hairball past a few hundred nodes (docs/atlas/src/lib/graphView.ts) — it stops being legible long before it stops building; it needs filtering / focus-by-hub / an orphan view (the graph’s own roadmap).
Fixing the tree/graph
The index notes are a lifecycle/type axis, not a topic — non-monotonic (a
feature becomes reference), so a note’s index parent changes on a lifecycle
flip while the stable thing (topic) isn’t an axis at all.
Right-sized redesign, by leverage-per-line:
- Derive backlinks from the existing link mechanism — shipped first (#1426). Edges already exist as
./slug.htmllinks; a build pass inverts them, no new wikilink syntax, and a link to a non-existent slug is a fail-fast build error. The note→note graph is sparse on its own (most edges cluster on a few hubs likeelectricityandodu) — so the graph view promotes the four categories to realmocnotes that every note files under viaparents, which both densifies the graph and removes every orphan. - Add optional
tags, not a requiredarea. A required topic axis forces a backward-incompatible migration of every note and imposes single-membership where the need is many-to-many. One optional field keeps “nothing unfiled” intact and adds the topic lens for free:// src/content.config.ts — add ONE optional field; no migration, no index inversion tags: z.array(z.string()).default([]), // many-to-many facets; derive a tag → notes map - Generalize typed relations with auto-inverses.
parents→ has-children,supersedes→ superseded-by, any link → referenced-by. Keep the vocabulary tiny.status: supersededis a dangling flag today;supersedes: <slug>makes the lineage a real bidirectional edge. - Graph view + Maps of Content — now shipped (#1434). The trap was inlining a JS graph +
graph.jsoninto every committed page (multiplying thedist/churn) and trusting the Code tab to run it. The resolution: compute the layout at build time withd3-forceand bake it to one self-contained SVG on the index page only, with a singleis:inlinescript for interaction (no bundle). The decomplected data model came with it — a category is no longer a hardcodedkindenum but a real note markedmoc: true, and every note files under one throughparents, the single edge mechanism.
Scaling to many projects
Chosen path: Option A now, Option C later — not the aggregator up front. There is no second repo, no confidential project, no cross-role audience today. Standing up an aggregator + search-catalog + resolver now is textbook Backstage-cargo-culting for a small team — a direct violation of the fail-fast / no-premature-abstraction philosophy.
- Now, if a second project lands inside the monorepo — Option A: one optional
projectfield (defaulted from the repo, not authored per-note) + a slug-prefix convention in the existing single Atlas. Zero new machinery; intra-repo siblings still cross-link relatively; one scope, one cadence. - Later, only when a separate repo exists — Option C: namespacing happens only at aggregation — a loader prefixes ids (
kolu/electricityvsinfra/electricity), the federated URL becomeskolu.dev/atlas/<project>/<slug>, search is Pagefind (Rust-built static index, byte-deterministic), and access control reuses Git permissions (who can clone = who can read) plus a per-repopublic:trueallowlist. Decide the aggregator’s owner and operating cost before building it — federated content doesn’t auto-rebuild, so it adds a webhook/cron and a stale-catalog window the single repo never had.
Is federated aggregation a @kolu/* package? No. It fails all three
electricity tests: it isn’t domain-agnostic (it aggregates
the Atlas domain itself), it hides no hard volatility (git clone +
getCollection() + pagefind is a bounded build-time pipeline; Astro’s Content
Layer is already the receptacle), and no foreign app graduates onto it. It’s a
leaf-tier Astro extension — and, by the same logic, so is the component kit:
prefer vendoring it per-repo over extracting @kolu/atlas-kit.
The roadmap
Where it’s been and where it’s going — smallest-valuable-first; each forward step ships independently and keeps the Atlas working at every stage.
- Shippedoriginal plans + MOC + house style + the
docs/**agent rule (#1095); thedocs-mocgate +plans::checkmodule (#1098). - Donethe Atlas as a self-contained
docs/atlas/project — generated index, render route, committed HTML,.apmrule +ci::atlas-syncgate, and the MDX component kit. First migrated note; the originalrelease-workflowplan re-created as the release-runbook note (#1208). - Donethe remaining
docs/plans/*.htmlmigrated to Atlas notes;docs/plans/retired; the/atlasskill shipped. - Donederived backlinks + the fail-fast dead-link gate (#1426) — render “Referenced by” from the existing
./slug.html+parentsedges. - Publication gateConfidentiality — a correctness bug, not a feature. Merge-to-master is public today (website/default.nix copies all of
dist/;draftonly hides from the index). Add a per-note opt-in allowlist (fail-fast: nothing public unless it declarespublic: true) and gate the Nix copy on it. Pure config + Nix; no schema rework. Unblocks a confidential note — or a second team — ever landing. - tags + in-repo searchAdd optional
tags: string[](no migration) and a Pagefind index shipped into the committeddist/so search works in both the Code tab and the public mirror. Trigger it when note count nears the few-hundred where index-as-search breaks — not at 50. - Graph view + MoC hubsShipped as the unified entry point (#1434) — a build-time
d3-forcelayout baked to one self-contained SVG. The data model is decomplected: a category is a real note markedmoc: true(nokindenum, no synthetic nodes), and every note files under one viaparents, so nothing is unfiled and the graph has no orphans. The index is the graph + Maps of Content; baking the layout settles the Code-tab JS question, leaving a singleis:inlinescript and no bundle. - Federation A → COption A (one
projectfield + slug prefix) the moment a second project shares the monorepo; the Option C aggregator only when a separate repo appears, and only after its owner + cost are decided. Never the stale-fallback resolver.
Define success up front so “won’t scale” becomes answerable: time-to-find a note, % reachable in ≤2 hops, agent token cost per corpus re-read (the headline advantage, quantified), CI time per PR as notes grow, and zero confidential leaks.
History: a 2026-06-02 adversarial research pass flipped HTML-all-the-way →
markdown-first; Astro was then chosen for rendering, the Atlas extracted into a
self-contained docs/atlas/ project, and notes moved to MDX with a typed
component kit. This note is the merge of the original second-brain design
rationale into the meta comparison + roadmap, so the Atlas’s note about itself
lives in one place.