lg is a filesystem-first notes indexer that was built in one day and has been discovering what it actually is ever since. It started as a search tool for Markdown files. It is now a publishing platform that serves a blog, a wiki, a cover art generator, a bi-directional sync engine, a git-backed version control system, and this encyclopedia entry about itself — all from a single Go binary that still thinks it’s a notes indexer.
This is Gall’s Law in its purest form: a complex system that works, found to have evolved from a simple system that worked. The simple system was six commits on a Monday. The complex system is 9,571 lines of Go, 25 subcommands, and the infrastructure behind The Lifelog and Yagnipedia. At no point did anyone design the complex system. At no point did anyone need to.
The system has evolved in waves: on March 14, lg replaced its HTTP sync layer with embedded NATS JetStream and deleted 4,254 lines. The binary got lighter by doing more. Then it kept growing — video generation, TTS narration, SFTP media upload, a notes browser — reaching 20,305 lines by March 17. It got heavier by being useful. The Lizard approves of both directions.
“THE PALACE WAS NEVER NEEDED
THE FILES NEEDED GLASSES”
— The Lizard, The Homecoming — The Three Days a Palace Was Built From Markdown and SQLite
The Homecoming
lg was born on March 3, 2026, in a single day that collapsed a two-milestone project plan into six commits:
cfaacfe Initial implementation: filesystem-first notes indexer
175a8a5 S-412 batch 1: fuzzy match, stable IDs, JSON output
b032a80 S-412 batches 2+3: fields, query, tasks, refs
7c0469d S-413/S-414: config system and cover art generation
19fecac Fix cover default resolution: "standard" -> "1k"
8d524ca Display cover images side by side, default count to 3
4,196 lines of Go. Thirty-three tests. A CLI that could search, query frontmatter, find backlinks, list tasks, generate covers, and display images inline in the terminal via the Kitty graphics protocol. The Squirrel had proposed Elasticsearch. The Squirrel was not given Elasticsearch. The Squirrel was given SQLite with FTS5, which indexes 253 notes in under 200 milliseconds, which is faster than the Squirrel can finish proposing the alternative.
“You’re telling me that eighty-three stories, three hundred links, seventy-nine covers, and the entire ‘borrowed palace’ narrative… was just because we didn’t have a search index on a folder of text files?”
“Yes.”
— The Caffeinated Squirrel and riclib, The Homecoming — The Three Days a Palace Was Built From Markdown and SQLite
The Architecture
lg’s architecture is two sentences:
Markdown files are the source of truth. Everything else is a lens.
~/Notes/*.md → fsnotify → parser → SQLite (FTS5) → CLI / HTTP
↓ ↓
git commit NATS publish
↓ ↓
GitHub Cloud (Hetzner)
The files live in ~/Notes/, tracked by git, synced between machines via embedded NATS JetStream. lg watches the directory with fsnotify, parses each Markdown file (frontmatter, wiki-links, tasks, headings), and stores the parsed data in SQLite at ~/.local/share/lg/index.db. The database is a lens. Git is the memory. NATS is the nervous system.
Delete the database. It rebuilds in 200 milliseconds. Delete the git history. The files are still there. Disconnect NATS. The events queue locally and catch up on reconnect. Every layer is disposable except the files. The files are the truth.
The Sync
lg embeds a NATS server directly in the binary. No external process. No Docker. No message broker to manage. The local instance runs as a leaf node; the cloud instance runs as a hub. They connect via mutual TLS — a self-signed CA, one certificate per machine, outbound connection from local (no port forwarding needed).
Three JetStream streams:
LG_LOCAL— the local instance publishes here (file changes, covers)LG_CLOUD— the cloud instance publishes here (enrichments, new notes)LG_EVENTS— the hub’s aggregated stream, sources from both
When the local machine is offline, events accumulate in LG_LOCAL. When it reconnects, JetStream catches up automatically. No polling. No retry loops. No hope-based engineering.
This replaced an HTTP outbox + SSE subscriber that was 2,057 lines of Go. The NATS replacement is 900 lines. The sync got more reliable by getting smaller.
The Memory
~/Notes/ is a git repository. Every file change triggers an automatic commit. Every five minutes, a push to a private GitHub repository (riclib/notes). The files have always been the truth. Now they also have history — every edit, every addition, every deletion, tracked and recoverable.
lg history "Go" shows the git log for the Go yagnipedia entry. lg drift shows uncommitted changes. lg push pushes immediately.
This inversion — files over databases, projection over persistence — is lg’s defining characteristic. Every other notes tool makes the database the source of truth and exports files as a convenience. lg makes the files the source of truth and treats the database as a convenience. The files existed before lg. The files will exist after lg. lg is the glasses, not the eyes.
The Evolution
lg was built as a notes indexer. It did not stay a notes indexer. Each new capability was discovered, not designed:
| Date | Capability | Trigger |
|---|---|---|
| March 3 | Notes indexer, search, backlinks | The Homecoming |
| March 3 | Cover art generation | “The covers need automation” |
| March 5 | Blog server (templ, HTMX) | The Front Door |
| March 5 | Wiki server (Yagnipedia) | The Silmarillion Problem |
| March 6 | RSS feeds | “Blogs need RSS” |
| March 7 | Content sync to remote | Deploy to Hetzner |
| March 8 | Gemini cover engine | The Labyrinth |
| March 9 | Note renaming with reference updates | “rename should update wiki-links” |
| March 12 | Live journal page (SSE, HTMX) | “The journal needs a page” |
| March 14 | NATS JetStream sync (replacing HTTP) | The Picnic That Almost Wasn’t |
| March 14 | GitStore (auto-commit, history, push) | “The files need memory” |
| March 14 | Deleted 4,254 lines (net) | “The sync is fragile” |
| March 15 | Journal chat agent (Claude subprocess) | The True Second Brain |
| March 15 | Two-leaf NATS sync (Mac + Linux → hub) | “will the code work with two leaf nodes?” |
| March 16 | Notes browser (/notes) | “I want to browse the folder” |
| March 17 | Video generation (Grok Aurora API) | “the blog should come alive” |
| March 17 | TTS narration (Grok TTS API) | “every article should have a voice” |
| March 17 | SFTP media sync (replacing NATS covers) | “rsync at generation time, not deploy” |
| March 17 | Removed NATS cover sync (net -576 lines) | “covers deploy via SFTP now” |
Each row is a subcommand (except the chat agent, which is a handler — Zawinski’s Law was narrowly avoided). Each subcommand was added because the previous capability revealed the next need. The notes indexer needed covers. The covers needed a blog. The blog needed a wiki. The wiki needed content sync. The content sync needed a remote server. At no point did anyone write a requirements document. At no point did the requirements document need to exist.
This is the Gall’s Law progression: simple system → works → reveals next need → simple addition → works → reveals next need. The complex system was never designed. The complex system emerged from a simple system that kept working.
The Subcommands
lg has 28 subcommands. For a “notes indexer”:
lg search Full-text search (FTS5)
lg backlinks Notes linking to this note
lg links Outgoing wiki-links
lg query Filter by frontmatter (key=value)
lg frontmatter Frontmatter as JSON
lg fields Frontmatter field discovery
lg refs Notes mentioning a ticket/string
lg tasks Task list
lg recent Recently modified notes
lg show Note details
lg stats Index statistics
lg cover Generate cover art (Gemini/Grok)
lg cover-migrate Rename covers to slug format
lg cover-thumbnails Generate missing OG thumbnails
lg animate Generate looping video from cover (Grok Aurora)
lg tts Generate TTS narration (Grok TTS)
lg rename Rename note + update all references
lg index Trigger full re-scan
lg serve Start daemon (scan + watch + blog + NATS)
lg history Git history for a note
lg drift Show uncommitted changes
lg push Push ~/Notes/ to remote git repo
lg nats-info Show NATS configuration
lg nats-certs Generate mTLS certificates
lg config Show configuration
lg completion Shell autocompletion
lg help Help
lg version Version
Twenty-eight subcommands in a “notes indexer” is either feature creep or Zawinski’s Law — every program attempts to expand until it can read mail. lg has not yet achieved mail. lg has achieved blog, wiki, cover art generation, video animation, text-to-speech narration, NATS-based sync, SFTP media deployment, git version control, and a notes browser, which is close enough that the Squirrel has proposed adding mail “for completeness.”
The proposal was declined. For now.
The Inevitable Future (Updated)
Zawinski’s Law states that every program attempts to expand until it can read mail. lg skipped mail entirely. It learned to talk instead.
On March 15, 2026, lg grew a chat bar. Not a subcommand — a handler. POST /journal/chat spawns a Claude Code subprocess, pipes the user’s message, and streams back rendered markdown through a frosted glass HUD. The journal page has no edit button. The only way to modify notes through the web interface is to talk to the second brain.
This suggests a new law, complementary to Zawinski’s:
The Chatbot Corollary: Every program in the age of LLMs attempts to expand until it can hold a conversation. Those programs which cannot so expand are replaced by ones which can.
Zawinski observed that programs converge on email — the universal communication tool of the 1990s. The Chatbot Corollary observes that programs now converge on conversation — the universal interface of the 2020s. The notes indexer didn’t need an email client. It needed a mouth.
The trajectory predicted in the original version of this section has been partially superseded:
An editor screen. Leapfrogged. lg skipped the editor entirely. The predicted path was contenteditable + WebSocket. The actual path was a chat bar and a Claude subprocess. The editor was the wrong metaphor. You don’t edit your second brain with a cursor. You talk to it. The conversation is the editor.
A task manager. Still valid — lg tasks parses tasks, and the chat agent can already check them off (“picked up the milk”) or create new ones. The task manager is emerging from the conversation, not from a dedicated UI.
An Obsidian companion. Reframed. lg is not replacing Obsidian — it’s the other half. Both watch the same ~/Notes/ folder. Edit a file in Obsidian; lg’s fsnotify watcher picks it up, re-indexes, publishes via NATS, commits via git. Ask the chat bar to create a note; Obsidian sees the new file instantly. They share the filesystem the way two lenses share a landscape — Obsidian is the editor, lg is the publisher and the voice. No integration needed. No plugin. No API. Just two programs watching the same folder of markdown files, which is the UNIX philosophy working exactly as intended for once.
A Claude Code orchestrator. Achieved in miniature. The journal chat agent is a Claude Code orchestrator — it spawns claude as a subprocess with access to lg, Read, Write, and Edit. It can search notes, create yagnipedia entries, check off tasks, and file episodes. The orchestrator isn’t a future feature. It’s the chat bar.
And then, finally, an email client. This prediction is hereby withdrawn. lg will never read mail. lg will talk about mail — “what did the dentist’s office email say?” — because the chat agent has access to whatever tools Claude has access to. The email client was the wrong destination. The chatbot is the destination. Zawinski was right about convergence but wrong about the attractor. The attractor is no longer mail. The attractor is conversation.
The Squirrel has proposed that lg should read mail anyway, “for completeness.” The proposal has been declined. The Lizard approves.
The Paradoxes
lg embodies several paradoxes documented elsewhere in this encyclopedia:
The Gall’s Law Paradox: lg is a complex system (9,571 lines, 25 subcommands) that works because it evolved from a simple system (4,196 lines, 6 commits) that worked. If anyone had designed the current lg on day one — blog server, wiki engine, cover generator, content sync, remote deployment — the project would have failed. Instead, each capability was added when it was needed, tested when it was built, and deployed when it worked. The Squirrel proposed the complete system on day one. The Lizard built the indexer. Everything else followed.
The Ship of Theseus Paradox: lg’s original 4,196 lines have been modified, extended, partially rewritten to 13,825 lines, and then shrunk to 9,571 lines by replacing HTTP sync with NATS. The peak was not the destination. Is it the same tool? The answer is yes, because the file main.go has the same go mod init and the same philosophy: files are truth, everything else is projection. The planks have changed. Some planks were removed. The ship is the same. The course is the same.
The Lens Paradox: lg adds no information. Every search result, every backlink, every frontmatter query — all of this existed in the files before lg indexed them. lg does not create knowledge. lg makes existing knowledge findable. This is the paradox of all indexers: they are essential and they contribute nothing. The lens is not the landscape. But without the lens, the landscape is invisible.
The Recursion Paradox: lg indexes ~/Notes/yagnipedia/lg.md — this file. This file describes lg. lg parses this file’s frontmatter, indexes its content for FTS5, extracts its wiki-links, and serves it as a Yagnipedia article at /wiki/lg. The tool serves a description of itself. The description is accurate. The description will be indexed the moment it is saved. The loop has no base case.
The Deployment
lg is deployed by deploy.sh — cross-compile, scp, systemctl restart. The binary goes from a Linux desktop in Riga to a Hetzner server in Helsinki in under a minute. No Docker. No Kubernetes. No CI/CD pipeline. No staging environment.
Content sync is no longer a deployment step. The local lg and the cloud lg are connected by NATS leaf node — a persistent, encrypted, bidirectional connection. Edit a file locally; it appears on the cloud server’s index within seconds. No sync command. The river flows.
Media — covers, videos, audio — take a different path. When lg cover, lg animate, or lg tts generates a file, it uploads immediately to the server via SFTP. The binary reads ~/.ssh/id_rsa, connects to the configured remote, and writes the file. No NATS for binary blobs (that was tried, and removed on March 17). No rsync at deploy time. Each generation is its own deployment.
The server costs €109/month and runs at 0.01 load average, which means 99.99% of the Ryzen 9 7950X3D is idle — idle while simultaneously hosting the lifelog, the Yagnipedia, an embedded NATS server accepting leaf node connections on port 7422 with mutual TLS, and the demo instance of the very product whose construction the lifelog documents and lg indexes and publishes with AI-generated covers. The server is bored, which is the highest compliment in the Boring Technology tradition.
The Trust
lg’s deployment reveals something deeper than architecture: trust.
The Hetzner server was set up by Claude. Not “Claude generated a script that riclib reviewed and executed.” Claude. Directly. riclib did one thing:
ssh-copy-id [email protected]
Then said: “Install lg and Solid. Harden the server.”
Claude did the rest. Not because Claude had documentation — because Claude had read the lifelog. Claude knew how riclib likes his servers installed the way a colleague knows how you take your coffee: not from a specification, but from observation. The firewall rules. The systemd units. The Cloudflare tunnel configuration. The directory structure. The log rotation. All of it inferred from 114 episodes of a developer documenting how he builds things, what he values, and what he refuses to tolerate.
This is the ultimate recursion: the lifelog documents how riclib builds. Claude reads the lifelog. Claude builds the way riclib builds. The documentation became the training data. The mythology became the specification. The stories about how the server should work became the instructions for how the server does work.
The Squirrel would call this “Mythology-Driven Infrastructure.” The Lizard would call it trust. Both are correct.
The Laws It Obeys
- Gall’s Law: evolved from simple to complex, never designed from scratch
- YAGNI: each feature added when needed, not before
- Boring Technology: Go, SQLite, NATS, git,
scp, systemd — no innovation tokens spent - Conway’s Law: one developer, one binary — the architecture mirrors the org chart
The Laws It Violates
- Zawinski’s Law: expanding toward reading mail (23 subcommands and counting)
- Single Responsibility Principle: a notes indexer that is also a blog, wiki, cover generator, message broker, and version control system
- The UNIX Philosophy: does many things, not one thing — but does them in one binary, which the UNIX Philosophy respects even as it disapproves
Measured Characteristics
- Lines of Go (day one): 4,196
- Lines of Go (peak): 13,825 (before NATS migration)
- Lines of Go (post-NATS): 9,571 (after deleting HTTP sync)
- Lines of Go (current): 20,305 (video, TTS, SFTP, notes browser, journal UI)
- Growth factor: 4.8x (from day one)
- The binary got lighter once (NATS migration), then kept growing (because it kept being useful)
- Subcommands: 28
- Subcommands in a “notes indexer”: 28 (no longer suspicious — confirmed feature creep)
- Time to build (day one): 1 day, 6 commits
- Time to evolve to current: 12 days
- Time to replace the entire sync layer: 1 Saturday morning
- Index time (full scan): ~200ms
- Notes indexed: 686
- Covers: 399
- Yagnipedia entries served: 281
- Lifelog episodes served: 119
- Deployment script: deploy.sh (cross-compile + scp + restart)
- Media sync: SFTP at generation time (pkg/sftp, SSH key auth)
- Content sync: NATS JetStream (automatic, real-time)
- Version control: git auto-commit + push to GitHub
- Docker containers: 0
- Kubernetes clusters: 0
- Kafka instances: 0 (the Squirrel proposed it; the Lizard said “EMBED”)
- Elasticsearch nodes: 0 (the Squirrel remembers)
- Database: SQLite (one file, disposable)
- Source of truth: Markdown files (not disposable)
- Memory: git (every edit, every deletion, recoverable)
- Embedded NATS servers: 2 (one per machine)
- NATS leaf nodes: 2 (Mac + Linux → cloud hub)
- mTLS certificates: 2 (one CA, shared)
- IP SANs learned about the hard way: 1
- Video generation engine: Grok Aurora (10s loops, $0.50/video)
- TTS engine: Grok TTS (MP3, reads frontmatter scripts)
- Cover images that now move: 3 and counting
- Articles that now speak: 3 and counting
- Chat bar: 1 (Claude subprocess, streaming markdown, frosted glass HUD)
- Edit buttons in the web UI: 0
- Ways to modify notes through the UI: 1 (talk to Claude)
- Thinking words in the HUD: 11 (Pondering, Noodling, Percolating, etc.)
- The files needed glasses: yes
- lg is the glasses: yes
- The glasses have memory: now
- The glasses can hear: now
- The glasses can speak: now
- The glasses can animate: now
- The palace was never needed: correct
- Zawinski’s Law status: superseded by the Chatbot Corollary
- Email client: will never be built (the conversation is the destination)
Addendum: The Pocket (March 27, 2026)
On March 27, 2026 — twenty-four days after a developer built a search tool for his notes folder — the notes indexer got an iPhone app.
Not a web wrapper. Not a PWA. A native Swift app with SSE streaming, optimistic task updates, and a chat interface that talks to a Claude subprocess running on a Mac Mini in Riga through a delegate-based URLSession because Apple’s bytes(for:) API buffers Server-Sent Events instead of streaming them, which is the kind of sentence that should not exist in the README of a notes indexer.
The iPhone app was needed because the task system was needed because the daily notes had tasks in them because the developer writes tasks in daily notes because the notes are the source of truth because the files are the truth because the lens is not the landscape. The entire mobile platform was derived from the premise that - [ ] Buy groceries should have a due date.
This is Zawinski’s Law entering its final form. The original law states that every program attempts to expand until it can read mail. The Chatbot Corollary (March 15) observed that programs now converge on conversation. The Pocket Amendment observes the terminal stage:
The Pocket Amendment to Zawinski’s Law: Every program that has expanded until it can hold a conversation will continue to expand until it fits in a pocket. At that point, the program has become an operating system. The developer will not notice.
lg’s trajectory:
- Notes indexer (March 3)
- Blog engine (March 5)
- Publishing platform (March 7)
- Sync engine (March 14)
- Second brain (March 15)
- Mobile app (March 27)
The article claimed 28 subcommands. There are now 32. The article claimed 20,305 lines of Go. There are now 44,284 lines of Go and — this is the part the Squirrel has been waiting to hear — 28,989 lines of Swift. The notes indexer is now 73,273 lines across two languages and two platforms. It has four task states, three date types per task, smart date bucketing (“Last Week”, “Now”, “Next Week”), an action dialog for rescheduling, and optimistic UI updates. For a tool that indexes a folder of text files.
The Squirrel has not proposed anything. The Squirrel is staring at the commit log with an expression that suggests vindication, awe, and a faint concern that the notes indexer will achieve sentience before anyone proposes Kubernetes.
Updated Measurements (March 27)
- Lines of Go: 44,284 (was 20,305 — a 2.2x increase in 10 days)
- Lines of Swift: 28,989 (was 0 — an ∞x increase)
- Total lines: 73,273 (the notes indexer is now larger than most of the programs it replaced)
- Subcommands: 32 (was 28 — the growth is linear, the concern is exponential)
- Notes indexed: 920 (was 686)
- Tasks tracked: 619
- Wiki-links: 8,098 (was 863)
- Platforms: 2 (macOS + iOS — was 1)
- Languages: 2 (Go + Swift — was 1)
- Git commits: 96
- iOS task tabs: 4 (Todo, Doing, Done, Notes)
- Task date types: 3 (created, due, snooze)
- Date bucket categories: 12+ (years, months, weeks, “Now”)
URLSession.bytes(for:)bugs discovered: 1 (it buffers SSE)SwiftDatamodel containers removed for blocking main thread: 1appendingPathComponentdouble-slash bugs: 2- Confirmation dialogs for task actions: 1
- The Squirrel’s expression: complicated
- Zawinski’s Law amendments: 2 (Chatbot Corollary + Pocket Amendment)
- Email client: still not built
- The files are still the truth: yes
- The truth now fits in a pocket: yes
See Also
- The Lifelog
- Yagnipedia
- Go
- SQLite
- NATS
- HTMX
- Hetzner
- Cloudflare
- Gall’s Law
- YAGNI
- Boring Technology
- The Homecoming — The Three Days a Palace Was Built From Markdown and SQLite
- The Front Door — The Night the Palace Finally Faced the Street
- Second Brain
- The Saturday the Lens Learned to Remember, or The Picnic That Almost Wasn’t
- The True Second Brain, or The Night the Lens Learned to Listen
