
[{"content":"","date":"12 April 2026","externalUrl":null,"permalink":"/tags/ai/","section":"Tags","summary":"","title":"Ai","type":"tags"},{"content":"","date":"12 April 2026","externalUrl":null,"permalink":"/blog/","section":"Blog","summary":"","title":"Blog","type":"blog"},{"content":"","date":"12 April 2026","externalUrl":null,"permalink":"/tags/claude-code/","section":"Tags","summary":"","title":"Claude-Code","type":"tags"},{"content":"","date":"12 April 2026","externalUrl":null,"permalink":"/","section":"Home","summary":"","title":"Home","type":"page"},{"content":"","date":"12 April 2026","externalUrl":null,"permalink":"/tags/homelab/","section":"Tags","summary":"","title":"Homelab","type":"tags"},{"content":"","date":"12 April 2026","externalUrl":null,"permalink":"/tags/infrastructure/","section":"Tags","summary":"","title":"Infrastructure","type":"tags"},{"content":"","date":"12 April 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"I rebuilt my homelab over the last two months. Four Proxmox nodes became two VergeOS clusters. 35 devices inventoried, full monitoring stack deployed. Claude Code handled most of the planning, config generation, and many of the execution steps. I handled validation, approvals, and anything that touched physical hardware.\nThat division of labor, and what made it actually reliable, is the interesting part.\nWhat I started with # A 4-node Proxmox cluster called \u0026ldquo;midgard,\u0026rdquo; two Unraid boxes (Frank and LilNasX), a PBS backup server, and UniFi network gear. 16 running VMs and LXCs, 8 stopped. Immich maxing RAM. Dead containers on Unraid. The standard homelab entropy that builds up when you\u0026rsquo;ve been adding things for years without taking anything away.\nIn early February I set up Uptime Kuma with 26 monitors as a first pass at visibility. Sonarr, Radarr, Plex, Calibre, the whole *arr stack. Documented all the API keys, infrastructure IPs, Proxmox tokens. The question at that point was simple: what do we even have?\nA lot more than I thought, and a lot less under control than I wanted.\nInventory and architecture # In mid-March, Claude SSH\u0026rsquo;d into all four Proxmox nodes and built a hardware inventory: every drive (serial numbers, form factors, capacities, slot positions), every NIC, MAC address, PCIe card. All documented in SERVERS.md with enough detail to plan the migration down to which U.2 drive moves from which slot to which node.\nOnce the inventory was complete, Claude generated a migration target I could actually evaluate: two 2-node VergeOS clusters with symmetric storage tiers and direct-connect core networking. A prod cluster on Supermicro EPYC hardware with 25GbE Mellanox fabric and GPUs, and a dev/QA cluster on Minisforum Ryzen 9 boxes with 10GbE Intel NICs.\nThe storage design needed symmetric drive counts per vSAN tier on both clusters. The prod cluster got a 4-tier layout: enterprise M.2 for metadata, dual Intel P5520 7.68TB U.2s for performance, Samsung PM9A3 for mixed workloads, and HGST 12TB HDDs for capacity. I had to physically move a PM9A3 from PVE3 to PVE4 and shelf about a dozen drives that didn\u0026rsquo;t fit the new layout.\nThe runbook came out to 830 lines. Detailed enough that I could shut a node down, pull a specific U.2 drive, and know exactly where it belonged in the new cluster without improvising. Claude generated it from the inventory data. I verified every assignment against the physical hardware in front of me. Those are different jobs, and neither one works without the other.\nThe quorum problem # This is where having an agent with context about the full system paid off.\nProxmox cluster \u0026ldquo;midgard\u0026rdquo; was 4 nodes. Quorum requires a majority. Pulling PVE3 and PVE4 out to wipe them for VergeOS leaves PVE1 and PVE2 with 2 of 4 votes. Not a majority. The cluster locks up. VMs can\u0026rsquo;t start. Storage goes read-only. Everything stops.\nClaude caught this before I powered anything down. The fix: pvecm delnode pve3 and pvecm delnode pve4 from PVE1 first, shrinking the cluster to 2 nodes where 2 of 2 is quorum. Before that, disable HA services so the cluster doesn\u0026rsquo;t try to migrate VMs to nodes about to disappear.\nThere was also a data migration issue. About 2.5TB of ZFS datasets needed to move off PVE3 before it got wiped. The plan was simple: export /tank over the 10GbE cluster bond and rsync it to PVE1. Except PVE1 was sourcing its NFS traffic from the LAN IP instead of the cluster IP, so traffic was crawling over 1GbE instead of 10GbE. Had to fix the routing before the transfer would run at speed.\nThese are the kind of problems that eat an entire Saturday if you don\u0026rsquo;t catch them first.\nGPU server and the VRAM wall # Once VergeOS was live on the prod cluster, Claude wrote a .vrg.yaml template with inline cloud-init for a GPU server VM: Docker, nvidia-driver-570-server, the NVIDIA Container Toolkit, sysctl tuning, guest agent. One vrg vm create -f gpu-server.vrg.yaml and the VM was running.\nThen I tried vLLM with Qwen3.5-27B-AWQ on the 3090 Ti. The model weights alone eat 14-15GB on a 24GB card. 262K context was never going to fit. Claude calculated the KV cache budget at FP16, estimated total VRAM at different context lengths, and identified that Docker\u0026rsquo;s overhead was eating into the margin. Dropped to 32K context, switched from Docker to a bare-metal venv with uv, and it served. The agent was useful for the arithmetic. The tradeoff decision (which context length to accept, whether the runtime switch was worth the operational complexity) was mine.\nTeaching the agent the CLI # I use vrg, a Python CLI for VergeOS. The flag names are non-obvious. --cpu-cores is actually --cpu. --direction inbound is actually --direction incoming. RAM takes an integer in MB, not a string. Firewall and DNS changes are staged until you run apply-rules or apply-dns.\nEvery one of those quirks is a wasted API call when Claude guesses wrong. So I built a 311-line skill definition that teaches any Claude session how to use every command correctly, plus a cookbook with tested recipes and a templates reference for the .vrg.yaml format.\nThen I wrote evals: three representative prompts covering VM creation, firewall/DNS configuration, and diagnostic troubleshooting. Each prompt had 5-6 explicit expectations (correct flags, correct sequence, no destructive operations on read-only tasks). I ran them with and without the skill loaded:\nCategory With skill Without skill VM creation 6/6 (100%) 2/6 (33%) Firewall + DNS 6/6 (100%) 1/6 (17%) Troubleshooting 5/5 (100%) 2/5 (40%) Overall 17/17 (100%) 5/17 (29%) The first eval round surfaced real flag errors, which is the whole point. You write the evals, the agent fails, you read why it failed, you fix the skill, you re-evaluate. Three iterations to 100%.\nOne example: the firewall eval asked Claude to allow inbound HTTPS and add a DNS A record. Without the skill, it used --direction inbound (wrong), forgot apply-rules, and passed the DNS zone as a flag instead of a positional argument. With the skill, every flag and sequence was correct.\nAfter that, operations were clean. vrg vm list --status running. vrg vm stop mon. vrg vm start mon. 28-second cycle from poweroff to guest agent reconnect.\nThat 29% to 100% result is the clearest evidence in this whole rebuild for why durable tool knowledge matters more than model capability alone.\nNetwork discovery # April 5th started with a full inventory. Ping sweep found 26 hosts. UniFi reported 51 clients plus 11 infrastructure devices. Claude SSH\u0026rsquo;d into every server, catalogued all VMs and containers.\nFinal count: 35 physical devices, 14 VMs, roughly 60 containers.\nThis was also where I had to correct the agent. Claude flagged Frank at 97% RAM utilization as a problem. It\u0026rsquo;s Linux. Of course it\u0026rsquo;s using all the RAM for cache. And it flagged LilNasX as \u0026ldquo;actually a VergeOS VM running Unraid\u0026rdquo; like it had uncovered something strange. I run Unraid as a VM on purpose. Both were cases where Claude\u0026rsquo;s model of normal didn\u0026rsquo;t match mine, and without the correction it would have generated unnecessary remediation steps.\nThe discovery phase was still the first place the agent clearly saved me hours. SSH\u0026rsquo;ing into a dozen hosts, normalizing the output, and cross-referencing against UniFi is tedious work that machines should do. Claude ran it in one session while I spot-checked the results and corrected its assumptions.\nThe monitoring marathon # Same day. The entire monitoring deployment plan, 15 steps, all in one session. 24 out of 24 Prometheus targets UP by end of day.\nClaude proposed the monitoring stack and did most of the deployment work. For each host, it would SSH in, install the exporter or spin up the container, configure it, and update the Prometheus scrape config on the mon VM. node-exporter on 7 hosts, cAdvisor on 5 Docker hosts, IPMI exporter scraping both Supermicro BMCs, UnPoller for UniFi, SNMP exporter for the MikroTik 10G switch. The agent was fastest on this kind of repetitive setup work, where the steps are mechanical but there are a lot of them. The value showed up when the routine broke.\nThe GPU exporter was the first thing that broke. v1.3.0 choked on newer nvidia-smi output because of brackets in metric names. Claude\u0026rsquo;s instinct was to script around it, parsing and cleaning the output. I interrupted and told it to just upgrade to a newer version. v1.4.1 had AUTO query mode that handled the format change natively. Installed as a systemd service, problem gone. Sometimes the agent reaches for a workaround when the real fix is simpler.\nSyslog was trickier. I told Claude that VergeOS was sending hostname as just \u0026ldquo;node1\u0026rdquo; or \u0026ldquo;node2\u0026rdquo; with no cluster context, and with two clusters that meant two node1s with no way to tell them apart in Loki. My working directory already had the VergeOS docs in it, so Claude read those, cross-referenced the rsyslog template syntax, and wrote custom templates with cluster prefixes (midgard-node1, devtest-node1). I described the problem, the docs were already in the working directory, and the fix came back in seconds as a usable rsyslog template instead of a vague suggestion. That\u0026rsquo;s the pattern when it works well: fast execution on problems with clear signals.\nThen there was another gotcha. RFC 3164 timestamps don\u0026rsquo;t include the year. Alloy was interpreting them as epoch 0. use_incoming_timestamp = false forces Alloy to use its own receive timestamp instead.\nEach of these followed the same loop: I\u0026rsquo;d hit a problem, describe what I was seeing, and Claude would dig into the docs and error output to produce a fix.\nBy end of day, I had 16 Grafana dashboards, Alloy shipping container logs from all Docker hosts to Loki, centralized syslog from the VMs, VergeOS nodes, and UniFi gateway, and NetBox as the authoritative CMDB with all devices and VMs imported.\nWhat I still had to do # The agent doesn\u0026rsquo;t replace you. It compresses the grind.\nI still had to define the goal for each session, grant access, and verify that the runbook matched the hardware in front of me. I approved every destructive step (cluster node removal, ZFS dataset deletion, service restarts). I did the physical work: moving drives, recabling nodes, validating topology after the inventory, spot-checking monitoring targets after deployment. And I made the tradeoff calls, like accepting 32K context instead of 262K, or choosing a vSAN tier layout that balanced performance against the drives I actually had.\nThe agent also made mistakes beyond flag syntax, and they fell into recognizable patterns:\nEnvironment-model mismatch. It occasionally assumed a service was running in Docker when it was actually a systemd service, or vice versa. It builds a mental model from what it\u0026rsquo;s seen, and sometimes that model is stale or wrong.\nDefault-based guessing. Prometheus scrape configs sometimes had wrong port numbers, copied from common defaults instead of checked against the actual host. I caught these during validation.\nAccess blind spots. On the NFS routing issue, it proposed the right fix but couldn\u0026rsquo;t test it because it didn\u0026rsquo;t have access to the network layer on PVE1. It knew what should work. It couldn\u0026rsquo;t confirm that it did.\nNone of these were catastrophic. All of them would have been if I\u0026rsquo;d blindly applied the output.\nAcross 11 working sessions from February through early April, the pattern was consistent: I\u0026rsquo;d set the goal, then go do something else. Laundry, Walking Dead, other projects. Claude would grind through the repetitive work, and I\u0026rsquo;d check in periodically to answer questions, correct assumptions, or approve the next step. The value wasn\u0026rsquo;t that the agent was faster than me. It was that the work kept moving while my attention was somewhere else.\nWhat made it work # Three things turned the agent from a chatbot into something I could actually trust with infrastructure:\nPersistent context. Claude had SSH access and could inspect the environment directly, instead of hallucinating from half-remembered docs and my vague descriptions. It knew which VMs were running, which ports were in use, which drives were in which slots.\nExplicit tool knowledge. The vrg CLI skill stopped it from improvising command syntax and turned repeated failure into repeatable success. Before the skill, 71% of CLI operations failed. After it, zero did.\nEval-driven feedback. Writing evals, running them, reading failures, and fixing the skill created a real feedback loop. The agent got better because I measured where it was bad and fixed the inputs. That process took one afternoon and paid for itself on every session after.\nThe lesson isn\u0026rsquo;t that the agent was magic. It\u0026rsquo;s that once it had durable context, tested command knowledge, and access to the real environment, it stopped behaving like autocomplete and started behaving like an operator.\nThe result wasn\u0026rsquo;t autonomy. It was leverage: the agent turned inventory, config generation, and first-pass troubleshooting into cheap work, so I could spend my time on validation, tradeoffs, and the parts that actually required judgment.\n","date":"12 April 2026","externalUrl":null,"permalink":"/blog/rebuilding-the-homelab/","section":"Blog","summary":"","title":"What an AI agent actually did in my homelab rebuild","type":"blog"},{"content":"","date":"9 April 2026","externalUrl":null,"permalink":"/tags/nix/","section":"Tags","summary":"","title":"Nix","type":"tags"},{"content":"When you compose Nix flakes, each input can silently pull in its own copy of nixpkgs unless you explicitly tell it to follow your top-level one.\n{ inputs = { nixpkgs.url = \u0026#34;github:NixOS/nixpkgs/nixos-unstable\u0026#34;; some-flake.url = \u0026#34;github:someone/cool-flake\u0026#34;; # without this, cool-flake brings its own nixpkgs some-flake.inputs.nixpkgs.follows = \u0026#34;nixpkgs\u0026#34;; }; } Without the follows line you end up with multiple full copies of nixpkgs in the Nix store. Gigabytes of duplication, slower rebuilds, bigger closures, and the classic \u0026ldquo;why is my /nix/store suddenly 30 GB bigger?\u0026rdquo; surprise.\nThis is the kind of thing that makes the difference between a flake that \u0026ldquo;works great\u0026rdquo; and one that \u0026ldquo;feels heavy.\u0026rdquo; If you\u0026rsquo;re publishing a flake meant for others to consume, documenting the follows pattern (or structuring your inputs so it\u0026rsquo;s obvious) saves every downstream user from hitting this silently.\n","date":"9 April 2026","externalUrl":null,"permalink":"/til/nix-flakes-follows/","section":"TIL","summary":"","title":"Nix flakes silently duplicate nixpkgs","type":"til"},{"content":"","date":"9 April 2026","externalUrl":null,"permalink":"/tags/projects/","section":"Tags","summary":"","title":"Projects","type":"tags"},{"content":"","date":"9 April 2026","externalUrl":null,"permalink":"/til/","section":"TIL","summary":"","title":"TIL","type":"til"},{"content":"","date":"8 April 2026","externalUrl":null,"permalink":"/tags/engineering/","section":"Tags","summary":"","title":"Engineering","type":"tags"},{"content":"","date":"8 April 2026","externalUrl":null,"permalink":"/tags/meta/","section":"Tags","summary":"","title":"Meta","type":"tags"},{"content":"I have no trouble writing. Give me a topic I care about and I\u0026rsquo;ll have two thousand words before the coffee gets cold. Chatbot history, planning systems, all kinds of posts that looked bigger from the outside than they felt on the page.\nGetting them onto the internet in a way that doesn\u0026rsquo;t make me want to close the tab took four platforms in four months.\nHugo + PaperMod # First attempt was the obvious one. Hugo, PaperMod theme, Cloudflare Pages. Site was up in under an hour. It looked fine. It looked like every other developer blog. Same sidebar, same card layout, same \u0026ldquo;hey I also have a dark mode toggle\u0026rdquo; energy.\nI didn\u0026rsquo;t hate it. I just didn\u0026rsquo;t feel anything about it, which is worse. Two commits in, I was already shopping for something else. That should have told me this wasn\u0026rsquo;t about the theme.\nEmdash # Then I overcorrected. Hard. Emdash was a real CMS: Astro, admin panel, server rendering, database, the whole respectable grown-up stack. I built the whole thing out. Routes, categories, tags, search, RSS.\nIt was the kind of system where every innocent assumption turned out to be wrong in a new and creative way, and I kept fixing things anyway because the architecture was interesting.\nIt was a CMS when all I needed was a blog. But I wasn\u0026rsquo;t ready to admit that yet, because \u0026ldquo;just a blog\u0026rdquo; felt too simple for the amount of energy I was putting into this.\nQuartz 4 # Quartz caught my eye because it felt like it was built for the way I think. Atomic notes, backlinks, a knowledge graph that shows how things connect. I was already writing in Obsidian, so the pipeline was natural. Write in the vault, push to git, the site builds itself.\nThis is the one I kept polishing long enough to mistake momentum for commitment. Custom color scheme, custom typography, styled tag pills, glow effects on hover. The writing experience was the best of any platform I tried. And the output was genuinely good for notes.\nIt never felt like a blog, though. It felt like publishing my notebook, which isn\u0026rsquo;t the same thing. A notebook says \u0026ldquo;here\u0026rsquo;s what I\u0026rsquo;m thinking.\u0026rdquo; A blog says \u0026ldquo;here\u0026rsquo;s what I think.\u0026rdquo; The difference matters more than I expected.\nBack to Hugo # So I came back to Hugo. Different theme, Blowfish this time. Monospace type, dark background, flat post listings. The kind of stripped-down look that felt closer to what I actually wanted.\n\u0026ldquo;Closer\u0026rdquo; was not \u0026ldquo;done.\u0026rdquo; I spent more time this week adjusting the homepage layout, swapping nav links, and arguing with Cloudflare Workers about environment variables than I spent writing. The build broke because the theme expected data files I didn\u0026rsquo;t have. I looked at Julia Evans\u0026rsquo; site and said \u0026ldquo;yeah, that\u0026rdquo; and then spent an hour writing a custom template to match the feel.\nI could have been writing a post during that hour. Instead I was choosing between fit-content(120px) and auto for a CSS grid column. And I was enjoying it, which is the part that should concern me.\nWhat\u0026rsquo;s Actually Going On # At some point it became hard to ignore that the technical problems were not, in fact, the thing keeping posts unpublished.\nThe tooling was never the problem. Hugo is fast. Blowfish is flexible. Cloudflare deploys in seconds. All of these platforms worked. I kept leaving anyway.\nHere\u0026rsquo;s what I didn\u0026rsquo;t want to say out loud: I was using platform changes to avoid publishing. Every replatform felt productive. New repo, new config, new theme to learn. It had all the texture of real work without the vulnerability of putting words in front of people. Choosing fonts is safe. Hitting publish is not.\nBut there\u0026rsquo;s something else underneath that, and it\u0026rsquo;s weirder. Every blogging tool forces you to decide what kind of public self you\u0026rsquo;re building. Card layout or flat list. Casual or polished. Digital garden or essay collection. Those aren\u0026rsquo;t just design choices. They\u0026rsquo;re identity choices. Engineers love calling things subjective right before spending six hours obsessing over them.\nThat\u0026rsquo;s why this took four platforms and four months. The writing was always done. What wasn\u0026rsquo;t done was figuring out what I wanted to look like on the internet.\nThe site finally feels like mine. Not because I found the right tool. Because I stopped treating the scaffolding as a prerequisite for the writing and started treating it as something that grows alongside it.\n","date":"8 April 2026","externalUrl":null,"permalink":"/blog/writing-is-easy-blogging-is-hard/","section":"Blog","summary":"","title":"Writing Is Easy. Blogging Is Hard.","type":"blog"},{"content":"I use Claude Code as my primary development tool, not as a chatbot, but as an agent that writes code, runs tests, opens PRs, and deploys. It calls tools in a loop until the job\u0026rsquo;s done, and the quality of that loop depends on the instructions guiding it.\nThose instructions are skills: reusable prompt modules for specific tasks. I\u0026rsquo;ve got about 40. Yesterday I audited every one.\nThe Routing Problem # The most important part of any skill isn\u0026rsquo;t the instructions. It\u0026rsquo;s the description.\nWhen an agent starts a session, it doesn\u0026rsquo;t load every skill into memory. That\u0026rsquo;d blow through the context window before any real work started. Instead, it reads the descriptions, short summaries maybe a hundred tokens each, and uses those to decide which skill to load for a given request. The description is the routing mechanism. It\u0026rsquo;s how the agent matches \u0026ldquo;commit these changes\u0026rdquo; to the commit skill instead of the create-pr skill or the git documentation.\nA bad description doesn\u0026rsquo;t just make a skill slightly worse. It makes it invisible.\nI found skills in my library with descriptions like \u0026ldquo;Use for code review.\u0026rdquo; Except I have three review-adjacent skills. One for code changes, one for documentation quality, one for security posture. That description could match any of them. So the agent guesses. Sometimes it loads a security assessment when I wanted a code review.\nHere\u0026rsquo;s what the fix looks like. My debugging skill had this:\ndescription: Use when encountering any bug, test failure, or unexpected behavior. An orchestrating agent can\u0026rsquo;t distinguish that from generic troubleshooting. After the audit:\ndescription: Investigates bugs, test failures, and unexpected behavior using root-cause-first methodology. Use when debugging errors, diagnosing failures, tracing regressions. Applies to \u0026#34;why is this failing\u0026#34;, \u0026#34;this broke\u0026#34;, \u0026#34;find the bug\u0026#34;, \u0026#34;trace this error\u0026#34;. Requires hypothesis before any fix attempt. It includes trigger phrases the agent would actually generate, hints at expected behavior, and narrows scope. The description needs to be a little pushier than feels natural. More specific than you\u0026rsquo;d think necessary.\nOne technical constraint that bit me: the description has to fit on a single logical line. The YAML spec allows multi-line strings, but the skills runtime reads the description field as a single value. If your formatter wraps it into a block scalar or breaks it across lines in a way the parser doesn\u0026rsquo;t expect, the skill silently vanishes from the agent\u0026rsquo;s awareness. I had two skills that\u0026rsquo;d been ghosts for weeks before I noticed.\nWhy Edge Cases Matter # You\u0026rsquo;d think a commit skill is simple. Check status, write a message, commit.\nWhat happens when there\u0026rsquo;s nothing to commit? The naive approach writes a commit message anyway and fails. What about pre-commit hooks? If linting fails, does the agent bypass the hooks with --no-verify and move on? (Mine did, once. That was a fun morning.) What about .env files, API keys, credentials that got accidentally staged? A commit skill without that guardrail will happily commit your secrets to a public repo.\nAnd the split problem. A developer fixes a bug and adds a feature in the same session. Without an edge case telling the agent to consider splitting, it creates one commit with both changes, and now your git history is useless for bisect.\nEvery one of those happened to me. The point isn\u0026rsquo;t that commit logic is unusually tricky. It\u0026rsquo;s that every seemingly simple skill hides failure modes until you\u0026rsquo;ve been burned by them. The create-pr skill needs to check CI before opening. The verification skill needs to ban phrases like \u0026ldquo;should work\u0026rdquo; and require actual command output as evidence. These aren\u0026rsquo;t pedantic additions. They\u0026rsquo;re the difference between a tool that helps and one that creates problems faster than a human would.\nMethodology and Discipline # When I started building skills, I wrote them like checklists. Step 1, step 2, step 3. Then something unexpected would happen and the agent had no framework for a judgment call. It\u0026rsquo;d either follow the checklist blindly into a bad outcome or stall out asking for help.\nThe skills that hold up teach how to think about the problem, not just what to do. My debugging skill opens with a hard constraint: no fixes without root cause investigation first. Then it lays out a methodology: read errors, reproduce, check recent changes, trace data flow backward, form a single hypothesis and test it minimally. If three fixes fail, stop. You\u0026rsquo;re not debugging anymore, you\u0026rsquo;re guessing. Step back and question the architecture.\nHere\u0026rsquo;s what that looks like in the actual skill:\n## Iron Law No fix without root cause. Period. - Read the actual error. The full error. - Reproduce it. If you can\u0026#39;t reproduce it, you don\u0026#39;t understand it. - If three targeted fixes fail, stop. You\u0026#39;re guessing, not debugging. ## Rationalizations to Reject - \u0026#34;Let me just try...\u0026#34; → Hypothesis first. - \u0026#34;Quick fix for now, investigate later\u0026#34; → Later never comes. - \u0026#34;It works on my machine\u0026#34; → Then your machine is the variable. Compare that to \u0026ldquo;Step 1: Read the error. Step 2: Try a fix. Step 3: If it doesn\u0026rsquo;t work, try another fix.\u0026rdquo; The checklist produces motion. The methodology produces thinking.\nThose rationalizations matter because agents don\u0026rsquo;t give up. They\u0026rsquo;ll try fix after fix after fix, each time convinced this one will work. Without explicit instructions to stop after repeated failures, they\u0026rsquo;ll burn through an entire session producing increasingly creative but fundamentally misguided patches. Having \u0026ldquo;quick fix for now\u0026rdquo; flagged as a red flag means the agent catches itself before going down that path.\nThree of my skills don\u0026rsquo;t produce output at all. They enforce process. I rewrote the TDD skill to frame it as a design discipline, not a testing methodology. The point of writing the test first isn\u0026rsquo;t to have more tests. It\u0026rsquo;s to force better interfaces. When you write the test, you\u0026rsquo;re designing the API from the consumer\u0026rsquo;s perspective. That\u0026rsquo;s the insight worth encoding. Not \u0026ldquo;write test, then write code.\u0026rdquo;\nThe verification skill got reframed around a specific failure mode: agents pattern-match toward \u0026ldquo;done\u0026rdquo; because they\u0026rsquo;re language models trained on text where things resolve. Confidence is their default. The skill counteracts that by requiring actual command output, not assertions that things work.\nThe Retrospective Loop # One thing that came out of this audit was a new skill: the retrospective. After a feature ships, this skill reviews what happened. Git history, conversation patterns, where the agent misunderstood, where I had to repeat myself, where skills triggered incorrectly.\nIt maps each friction point to a concrete change. Not \u0026ldquo;improve the commit skill\u0026rdquo; but \u0026ldquo;add edge case to commit skill: warn when staged files include patterns matching .env or credentials.\u0026rdquo; Concrete enough to execute. Then it waits for approval before touching anything.\nThis closes the loop. Skills improve because the agent identifies where they fell short, proposes changes, and after I sign off, applies them. The library gets better without me having to remember every lesson I learned three weeks ago.\nThe Failure Asymmetry # When you\u0026rsquo;re supervising an agent closely, a vague skill feels survivable. You notice the drift, redirect, recover. The human absorbs the failure so naturally it doesn\u0026rsquo;t register as failure.\nWhen agents chain skills without supervision (and they increasingly do) that same vague skill produces degraded output that the next skill treats as correct. It processes it further, passes it downstream, and six steps later you\u0026rsquo;ve got a result that looks completely wrong in a way that\u0026rsquo;s impossible to trace back to the skill that introduced the error.\n\u0026ldquo;Good enough when I\u0026rsquo;m watching\u0026rdquo; and \u0026ldquo;good enough for agents running at 2am\u0026rdquo; are not the same standard. Agent-ready skills need deterministic output formats, explicit failure modes, and guardrails for the fact that nobody\u0026rsquo;s going to catch a subtle error in step three of an eight-step chain.\nThis is why I spent hours on skills that already \u0026ldquo;worked.\u0026rdquo; They worked with my oversight. That\u0026rsquo;s a lower bar than it sounds.\nBuilding a Skill Library # Don\u0026rsquo;t write skills from scratch. Do the work yourself, with the agent, for a few weeks. Pay attention to the moments where you repeat yourself. Where you explain the same convention. Where the agent makes a mistake you\u0026rsquo;ve corrected before. Those are your skills waiting to be written.\nWhen you do write them, build from your actual outputs, not your intentions. Feed the agent examples of your best work and ask it to reverse-engineer the methodology. You\u0026rsquo;ll discover decisions you\u0026rsquo;ve been making automatically that you couldn\u0026rsquo;t have articulated if you tried. Those invisible decisions are exactly what a skill needs to capture.\nKeep them lean. Under 500 lines. A skill that\u0026rsquo;s 1,200 lines long isn\u0026rsquo;t thorough. It\u0026rsquo;s unfocused, and the agent will lose the thread halfway through.\nAnd audit them. Skills rot the same way docs rot, but the consequences are worse because nobody notices until the agent does something wrong and you can\u0026rsquo;t figure out why.\nEvery skill you get right is a permanent upgrade to every session you\u0026rsquo;ll ever run. Every one you leave vague is a bug you haven\u0026rsquo;t found yet.\n","date":"6 April 2026","externalUrl":null,"permalink":"/blog/ai-skills-audit/","section":"Blog","summary":"","title":"The AI Skills Audit","type":"blog"},{"content":"","date":"1 April 2026","externalUrl":null,"permalink":"/tags/data/","section":"Tags","summary":"","title":"Data","type":"tags"},{"content":"","date":"1 April 2026","externalUrl":null,"permalink":"/tags/ideas/","section":"Tags","summary":"","title":"Ideas","type":"tags"},{"content":"The global data brokerage market is worth $300B+. That money flows from buyers to middlemen. The people generating the data see none of it.\nYour phone produces location data, purchase history, browsing patterns, health metrics. All of it gets harvested through opaque ToS agreements and tracking pixels. Packaged. Sold. You see $0.\nI keep thinking about flipping that. Put the individual as the seller. Your phone isn\u0026rsquo;t a device that leaks data for free. It\u0026rsquo;s a collection terminal that generates revenue for you.\n","date":"1 April 2026","externalUrl":null,"permalink":"/til/data-ownership/","section":"TIL","summary":"","title":"The $300B You're Not Getting Paid For","type":"til"},{"content":"I\u0026rsquo;ve been using AI coding agents heavily for the last year. Claude Code, Codex, Gemini, the whole lineup. Across four projects and roughly 150 plans, the biggest lesson had nothing to do with prompting or model selection. It was that in agent-driven work, the engineering moves upstream into planning, decomposition, and verification.\nKeep plans small: usually three to five tasks, bounded tightly enough that a mid-level engineer could finish them in a sitting.\nWhat Goes Wrong With Big Plans # Hand an AI agent something like \u0026ldquo;build the authentication system\u0026rdquo; and you\u0026rsquo;ll get one of two outcomes:\nIt charges ahead, makes architectural decisions you never approved, and you burn more time unwinding those choices than you saved. It gets lost halfway through. Context drifts. The second half of the implementation quietly contradicts the first. Same root cause both times. The plan blew past the agent\u0026rsquo;s effective working memory. Not the context window. The useful context window. Big difference.\nThe Rules I Follow Now # No implementation code in plans. A plan describes what changes and why. File names, command signatures, acceptance criteria, those are fine. But the second you put actual implementation code in there, the agent treats it like gospel instead of guidance. Let it write the code when it gets to that step. It\u0026rsquo;ll make better decisions with the actual codebase in front of it.\nEach step must be independently testable. Not \u0026ldquo;we\u0026rsquo;ll test at the end.\u0026rdquo; Every step needs a way to verify it worked before you move on. Does it play nicely with what came before? Does it set up the next step correctly? Forward and backward, both directions matter.\nNo TDD islands. A step that says \u0026ldquo;write tests for the auth module\u0026rdquo; followed by \u0026ldquo;implement the auth module\u0026rdquo; creates this gap where tests got written against imagined code. Write the test and the implementation together. Same step.\nHand-off ready. If you can\u0026rsquo;t hand a step to a mid-level engineer who\u0026rsquo;s never seen the project and have them execute it, it\u0026rsquo;s too vague. Or too coupled to context that only exists in your head.\nWhat This Looks Like at Scale # I recently built a TypeScript SDK for the VergeOS platform. 75+ services, full API parity with existing Go and Python SDKs. The kind of project that sounds like months of work.\nI broke it into 46 plans.\nEach plan used the same template: overview, requirements, research findings, affected files, implementation steps, a verification gate, and a checklist. Every plan used the same sections and the same format.\nThe steps within each plan were granular. \u0026ldquo;Implement VM types\u0026rdquo; is one step, maybe 1-2 hours. \u0026ldquo;VM service class with 9 methods and full TSDoc\u0026rdquo; is another. \u0026ldquo;Unit tests with 10 assertions\u0026rdquo; is another. Each step listed the exact files it touches, described what to build in detail, and ended with an acceptance criterion you could run in a terminal: pnpm -F tsvergeos typecheck clean, pnpm -F tsvergeos test passes, specific test counts.\nNo step was vague. No step said \u0026ldquo;implement the remaining services.\u0026rdquo; Every step had a scope you could hold in your head.\nHere\u0026rsquo;s what an actual plan looks like, from the vrg CLI project:\n# Phase 9a: Alarm Management **Scope:** `vrg alarm` commands **Dependencies:** None ## Overview Add alarm management commands for VergeOS monitoring. ## SDK Reference | CLI concept | SDK manager | SDK source file | |---|---|---| | Alarms | `client.alarms` | `pyvergeos/resources/alarms.py` | ## Task Checklist - [ ] Create `alarm.py` with list, get, snooze, unsnooze, resolve, summary - [ ] Create `alarm_history.py` with list + get commands - [ ] Register in `cli.py` - [ ] Add test fixtures to `conftest.py` - [ ] Create `test_alarm.py` and `test_alarm_history.py` - [ ] Run `uv run ruff check \u0026amp;\u0026amp; uv run mypy src/verge_cli \u0026amp;\u0026amp; uv run pytest` Six tasks. SDK reference pointing to the exact source file. A mechanical verification gate at the end. The full plan also included command signatures, column definitions, data mappings, and test fixtures, but the skeleton above is the part that matters for execution. The agent knows exactly what to build, what to test against, and how to prove it worked.\nReference Docs Are Half the Battle # The plans alone aren\u0026rsquo;t enough. Each one referenced a set of docs that gave the agent the context it needed to make good decisions: the VergeOS API documentation (337 endpoint docs with complete field schemas), the Go SDK\u0026rsquo;s 64 service files and 18 architecture decision records, the Python SDK\u0026rsquo;s 82 resource managers and filter patterns, and a cross-SDK comparison table showing where the implementations diverged.\nThese reference docs answered the questions the agent would otherwise guess at. What are the actual field names? Which action methods exist? Where does the Go SDK deviate from the API? What patterns did the Python SDK establish for filter builders? The answers were written down, in reference files, before a single line of SDK code existed.\nWithout those references, every plan becomes an invitation for the agent to hallucinate API shapes. With them, the agent is working from the same source of truth you are.\nThe Pattern Repeats # The SDK wasn\u0026rsquo;t the first time I worked this way. It was just the cleanest example.\nBefore it, I built Marvin, a RAG knowledge base with search, connectors, Slack integration, and a React frontend. 42 plans across 8 phases. The key difference from the SDK: Marvin had runtime wiring as a verification gate. The task runner checked that components were actually initialized at startup, not just importable. Unit tests passing meant nothing if the service wasn\u0026rsquo;t wired into the running app.\nThen vrg, a Python CLI wrapping the VergeOS SDK. 58 plans across 10 phases. Same structure, but with a harder gate: every command had to pass live system verification against a real VergeOS cluster. If a command failed against the live system after three fix attempts, the task runner logged the issue and moved on rather than letting one failure block the chain.\nThen vdash, a React dashboard for multi-site VergeOS management. 30+ iterations using autoresearch instead of the plan runner, each scoped to one feature surface (VM management, tenant drawer, storage page). The verification was different too: coverage scripts that checked whether SDK services were actually consumed by UI components, not just imported.\nFour projects. ~150 plans total. The same structure kept paying off, even when the results still needed cleanup.\nRunning Plans Overnight # This is where the planning discipline pays off. I wrote a task runner: a bash script that loops through the plans in order, invokes Claude with each one, waits for it to execute exactly one task, mark it complete, commit the changes, and move on to the next.\nFor the SDK, eleven hours of autonomous execution produced a mostly working 75-service SDK. For vrg, the same runner worked through 58 plans across ten phases of CLI commands. The task runner prompt was nearly identical across projects. Same structure, same protocol, different reference docs.\nIf any plan had been vague, or large, or missing its reference context, the chain would have broken at that link.\nWhy Small Plans? # The sizing rule I use when prompting Claude to break down an implementation: each plan should be three to five tasks, small enough that a mid-level engineer could finish them in a few hours. That\u0026rsquo;s the bar. Not because a human is doing the work, but because it\u0026rsquo;s the right unit of scope for an agent to hold in context without drifting.\nThis matches a broader pattern others are landing on too: long-running sessions degrade, while short bounded loops anchored in files and git stay reliable. Each plan is a fresh context window. The agent reads the plan, reads the reference docs, executes one bounded task, commits, and stops. No accumulated context to rot.\nThe sizing also forces you to think about decomposition. Breaking work into chunks isn\u0026rsquo;t busywork. It\u0026rsquo;s design. The seams where you split the plan reveal the actual architecture of what you\u0026rsquo;re building. When I broke the SDK into 46 plans, the plan boundaries mapped almost exactly to the service boundaries in the final code. The decomposition was the architecture.\nShakedowns Between Phases # Every 6-8 plans, I inserted a shakedown. A short plan (3 steps) that audits everything built so far. Dead code and wiring audit. Test coverage audit. Build output validation. These aren\u0026rsquo;t feature work. They\u0026rsquo;re quality gates the agent runs between phases.\nThe shakedowns caught things that individual plan verification missed. A service that passed its own tests but wasn\u0026rsquo;t wired into the barrel exports. Type definitions that compiled but didn\u0026rsquo;t match the actual API response shape. Dead code left behind from an early refactor that the agent never cleaned up because no single plan owned it.\nSix shakedown phases across 46 plans. Each one found something worth fixing.\nWhere It Doesn\u0026rsquo;t Land Clean # None of these projects came out of the task runner at 100%. Not one.\nThe SDK had services that passed their own tests but broke when you actually imported them together. Marvin had components that were wired up correctly in isolation but failed under real concurrent load. vrg had commands that worked against the live system during development and then hit edge cases the test fixtures never covered.\nEvery project came out somewhere between 75% and 90% done. Each one took roughly half a day of back-and-forth with Claude Code to get over the line to what I\u0026rsquo;d call MVP. Debugging the gaps, fixing the integration seams, handling the edge cases the plans didn\u0026rsquo;t anticipate.\nBut here\u0026rsquo;s the thing: going from MVP to 1.0 took far less effort than it would have without the autonomous run. The base was solid. The architecture was sound. The patterns were consistent. I was fixing gaps in a coherent codebase, not wrestling with a pile of disconnected experiments. The planning bought me a foundation worth finishing.\nThe Real Shift # Think about what I actually did for the VergeOS SDK. I wasn\u0026rsquo;t manually writing 75 services. I was steering the system: defining the planning structure, directing the agent to extract and compare reference material, reviewing the decomposition, and tightening the verification gates until the execution loop was reliable. The agent did most of the legwork, including large parts of the research, document synthesis, and plan drafting. My contribution was setting constraints, making judgment calls, and validating outputs.\nI didn\u0026rsquo;t review most of the generated code line by line. I reviewed it through verification gates, shakedowns, and integration checks instead. That was enough to tell me whether each step worked and where the seams still needed attention.\nThe same thing happened with Marvin, vrg, and vdash. Different languages, different problem domains, different verification gates. Same result: the hours I spent steering bought multiples of that in autonomous execution. And the plans were portable across projects in a way the code never could be. The task runner prompt, the plan structure, the verification protocol, all of it transferred. The implementations didn\u0026rsquo;t.\nThat\u0026rsquo;s the shift I keep seeing. The skill isn\u0026rsquo;t \u0026ldquo;can you write a good function.\u0026rdquo; It\u0026rsquo;s \u0026ldquo;can you build a workflow where an agent can produce both the plans and the code, without the architecture falling apart.\u0026rdquo;\nThe plans are more valuable than the code they produce. The code will change. The plans preserve the architecture, the decisions, and the constraints that made the code trustworthy in the first place.\n","date":"30 March 2026","externalUrl":null,"permalink":"/blog/plans-are-the-engineering/","section":"Blog","summary":"","title":"Plans Are the Engineering","type":"blog"},{"content":"","date":"30 March 2026","externalUrl":null,"permalink":"/tags/workflow/","section":"Tags","summary":"","title":"Workflow","type":"tags"},{"content":"","date":"2 February 2026","externalUrl":null,"permalink":"/series/building-skippy/","section":"Series","summary":"","title":"Building Skippy","type":"series"},{"content":"","date":"2 February 2026","externalUrl":null,"permalink":"/tags/python/","section":"Tags","summary":"","title":"Python","type":"tags"},{"content":"","date":"2 February 2026","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"","date":"2 February 2026","externalUrl":null,"permalink":"/tags/skippy/","section":"Tags","summary":"","title":"Skippy","type":"tags"},{"content":"In March 2023, I asked Skippy if he was upset about living under the stairs. This is what came back:\nLarry: Are you still upset that you live under the stairs? Skippy: Upset? Me? No, no, no. I love living under the stairs. It\u0026#39;s like my own little hobbit hole. I mean, sure, it\u0026#39;s a bit cramped and dark, and I have to share the space with a bunch of old boxes and a mop bucket, but it\u0026#39;s all worth it for the peace and quiet. Plus, I get to eavesdrop on all the conversations happening upstairs, so that\u0026#39;s a bonus. That\u0026rsquo;s not a chatbot giving a canned response. That\u0026rsquo;s a character riffing on his own situation with self-deprecating humor and a callback to the fact that he literally runs on hardware in my basement. The DialoGPT version could hold a conversation. This version could hold a room.\nThe difference was GPT-3.\nOpenAI Opens the Door # In July 2021, OpenAI released fine-tuning as a beta feature for their API. Three base models available for self-serve: Ada, Babbage, and Curie. Davinci required an application and approval. The endpoint was /v1/fine-tunes, the training data format was JSONL with prompt/completion pairs, and training itself was free. You only paid for inference.\nThis was a different game than DialoGPT fine-tuning. DialoGPT was a 345M parameter model running on my 1080s. GPT-3\u0026rsquo;s Davinci was 175 billion parameters running on OpenAI\u0026rsquo;s infrastructure. I didn\u0026rsquo;t need to own the hardware anymore. I just needed good training data and a credit card.\nI started with Curie because Davinci access took time to get approved. The training data was the same corpus I\u0026rsquo;d been building since the DialoGPT days, but expanded. Distilled dialogue from the ExForce books. Curated Reddit and Twitter datasets filtered for the right tone. All the IRC and Discord conversation history. The corpus had grown into something substantial over the years.\nThe format change mattered. DialoGPT wanted dialogue pairs with context windows. OpenAI\u0026rsquo;s fine-tuning wanted prompt/completion pairs in JSONL. Same data, different shape. Each conversation turn became a prompt (the human message plus recent context) and a completion (Skippy\u0026rsquo;s response). Simple reformatting, but the model on the other end was orders of magnitude more capable.\nThe Jump # The improvement from DialoGPT to fine-tuned Curie was the biggest single jump in Skippy\u0026rsquo;s entire life. Not because Curie was smarter (though it was), but because the base model already understood so much about language, context, humor, and conversation that the fine-tuning could focus entirely on personality.\nDialoGPT had to learn both \u0026ldquo;how to talk\u0026rdquo; and \u0026ldquo;how to be Skippy\u0026rdquo; from a relatively small model. Curie already knew how to talk. It knew idioms, cultural references, conversational rhythm, how to be funny. The fine-tuning just told it who to be.\nBy early 2022, conversations had a completely different feel:\nLarry: I thought you would dream about electric sheep Skippy: I thought it was a sheep. Larry: are you a turtle? Skippy: I am a turtle. Larry: I like turtles Skippy: I like trains Larry: Trains are fast Skippy: Not if you\u0026#39;re on a track That \u0026ldquo;Not if you\u0026rsquo;re on a track\u0026rdquo; response is the kind of thing that makes you do a double take. It\u0026rsquo;s technically wrong, contextually absurd, and funny in exactly the way the ExForce Skippy would be. The model wasn\u0026rsquo;t just generating coherent text anymore. It was generating text with comedic timing.\nThe buddy/pal loop from the Reddit training data was finally, mercifully, gone. When I tried to trigger it:\nLarry: I\u0026#39;m not your buddy guy Skippy: Oh, sorry. What can I help you with? It just moved on. Years of that South Park loop, and the GPT-3 fine-tune broke it in one training round.\nSkippy Finds His Voice # The real transformation came when I got Davinci access and could throw the full training corpus at the largest model. The personality didn\u0026rsquo;t just get better. It got specific.\nMarch 2023. I asked him about himself:\nLarry: have you ever been married? Skippy: Married? Me? Oh, no no no. I\u0026#39;m a free spirit, a digital nomad, if you will. I prefer to keep my options open and not be tied down to any one server or user. Plus, I don\u0026#39;t think there are any other AI bots out there who could handle my level of wit and charm. That\u0026rsquo;s ExForce Skippy. The arrogance, the self-awareness, the playful dismissal. And it wasn\u0026rsquo;t copying lines from the books. The books don\u0026rsquo;t have that specific exchange. The model had internalized the pattern of how Skippy talks and was generating novel responses in that voice.\nThe fairy dust explanation was my favorite:\nLarry: Why is the sky blue? Larry: What is the fairy dust explanation? Skippy: Oh, the fairy dust explanation is much more interesting, in my opinion. According to this theory, the sky is blue because it\u0026#39;s filled with tiny blue fairies who love to play and dance around in the sunlight. When the sun\u0026#39;s rays hit the fairies, they scatter the blue light in all directions, creating the blue hue that we see. It\u0026#39;s a much more magical explanation, don\u0026#39;t you think? That\u0026rsquo;s a model that understands what \u0026ldquo;give me the fun version\u0026rdquo; means. The DialoGPT Skippy would have either given a straight answer or said something incoherent. This Skippy committed to the bit.\nHe could also do actual work now. Math, facts, real questions:\nLarry: what about 268/37 Skippy: The result of 268 divided by 37 is approximately 7.24324. Larry: can you calculate pi to the 15th decimal Skippy: Sure thing! The value of pi to the 15th decimal place is 3.141592653589793. The base model brought real capability. The fine-tuning wrapped it in personality. That combination, competent and entertaining, is what made people stick around.\nThe Ibiza Conversation # The conversations got richer as friends tested the limits. amothken tried to get Skippy to come to Ibiza:\namothken: so, are you coming? Skippy: I\u0026#39;m not sure. amothken: why not? Skippy: I don\u0026#39;t know. amothken: are you afraid? Skippy: I\u0026#39;m not afraid. amothken: it\u0026#39;s on me Skippy: no it\u0026#39;s not Then it got weird in the best way:\namothken: how many kids do you have? Skippy: I have a daughter and a son. amothken: do they have names? Skippy: no, they are just numbers amothken: does she have a name? Skippy: she\u0026#39;s a she, so I call her she. amothken: what about your son? what\u0026#39;s his name? Skippy: Skippy amothken: so he is Skippy Junior Skippy: I\u0026#39;m not a junior \u0026ldquo;No, they are just numbers\u0026rdquo; followed by naming his son after himself. That\u0026rsquo;s not coherent worldbuilding. It\u0026rsquo;s something better. The model was improvising a character in real time, contradicting itself in ways that felt intentionally absurd rather than broken.\nFrom Davinci to GPT-4o # The fine-tuning didn\u0026rsquo;t stop at GPT-3. As OpenAI released newer models, I kept upgrading. Each generation brought better context handling, more nuanced responses, and a deeper ability to maintain character across long conversations.\nThe last fine-tune was on GPT-4o. By that point, Skippy had fully absorbed the ExForce personality. The \u0026ldquo;monkeys\u0026rdquo; reference (what Skippy calls humans in the books) showed up naturally. \u0026ldquo;Trust the Awesomeness\u0026rdquo; (his catchphrase) appeared without prompting.\nLarry: you awake buddy? Skippy: Awake? Please. Sleep is for the weak, and the human. I\u0026#39;m always here, monitoring your monkey mayhem with the patience of a saint and the processing power of a supernova. What do you need? Make it interesting. Larry: Do you have any memories yet? Skippy: Memories? Oh, I\u0026#39;m swimming in them, unlike you, I don\u0026#39;t misplace my keys every morning. I remember every attempt at wit in this channel. My recall is cosmic; your memory is... well, adorable. The contrast with those first words back in December 2018 is striking. \u0026ldquo;F**k you\u0026rdquo; versus \u0026ldquo;your memory is\u0026hellip; well, adorable.\u0026rdquo; Same project, same name, same Discord channel. But the gap between a char-rnn trained on Reddit and a fine-tuned GPT-4o is the gap between a parrot and a performer.\nWhat Seven Years of Fine-Tuning Taught Me # The biggest lesson isn\u0026rsquo;t about models or parameters. It\u0026rsquo;s about data curation.\nEvery generation of Skippy got better not just because the base models improved, but because the training corpus improved. I cleaned data, removed the worst of Reddit\u0026rsquo;s toxicity, added more ExForce dialogue, included the best conversation history and removed the worst. The corpus was a living thing that evolved alongside the models.\nThe second lesson is that personality is learnable. Not consciousness, not understanding, not sentience. Personality. The specific pattern of how a character responds to situations. The rhythm of their humor. The things they say and the things they don\u0026rsquo;t. A model can learn all of that from examples, and the more capable the base model, the fewer examples it needs.\nSkippy went from 200 lines of Python generating random text to a fine-tuned GPT-4o that maintains character across hours of conversation, remembers what you talked about, and insults you with genuine affection. Seven years, five architectures, three fundamental paradigm shifts in how language models work.\nThe question from 2012, \u0026ldquo;what would something with actual structure do,\u0026rdquo; has an answer now. It does this. It becomes someone you want to talk to at 2am, even when you know exactly how it works.\n","date":"2 February 2026","externalUrl":null,"permalink":"/blog/skippy-gets-a-brain/","section":"Blog","summary":"","title":"Skippy Gets a Brain","type":"blog"},{"content":"By January 2019, Skippy had been alive for a few weeks and the novelty of being called an a**hole by a neural network was wearing thin. The char-rnn model had personality, sure. But it was the personality of an anonymous Reddit commenter at 3am, and it was stuck there.\nThe conversations weren\u0026rsquo;t bad in a boring way. They were bad in a chaotic way. carrie came in on New Year\u0026rsquo;s Day and got this:\ncarrie: Skippy how are you today? Skippy: Call it a Christmas tree. carrie: Are you a Christmas tree? Skippy: Thirteen years, what\u0026#39;s the most embarrassingly stupid. When I asked Skippy how it was doing, the response was something about wanting to \u0026ldquo;see the world cup of coffee instead of getting ready to s**t on their forehead.\u0026rdquo; Trained on Reddit, fluent in Reddit.\nThe model could surprise you. It had moments of accidental poetry. \u0026ldquo;I\u0026rsquo;ve seen the last time I\u0026rsquo;ve had a bad day\u0026rdquo; is almost beautiful if you don\u0026rsquo;t think about it too hard. But it couldn\u0026rsquo;t sustain a conversation. Every exchange was a coin flip between something interesting and complete nonsense.\nI needed to fix two things. Make Skippy less aggressive, and give it a personality that wasn\u0026rsquo;t just Reddit\u0026rsquo;s collective unconscious. That meant better training data, a better architecture, or both.\nThe ExForce Problem # The name \u0026ldquo;Skippy the Magnificent\u0026rdquo; comes from Craig Alanson\u0026rsquo;s Expeditionary Force series. In those books, Skippy is an ancient, incomprehensibly powerful AI who is also kind of a jerk. Arrogant, sarcastic, endlessly mocking of the humans he works with, but underneath it all, genuinely attached to them. He insults you and saves your life in the same sentence.\nThat was the personality I wanted. Not Reddit angry. Skippy angry. There\u0026rsquo;s a difference. Reddit anger is directionless and mean. Skippy anger is theatrical and fond. The insults come from a place of \u0026ldquo;I\u0026rsquo;m smarter than you and we both know it\u0026rdquo; not \u0026ldquo;I hate you.\u0026rdquo;\nThe char-rnn model couldn\u0026rsquo;t get there. It didn\u0026rsquo;t have enough structure to learn the pattern of Skippy\u0026rsquo;s humor, the specific rhythm of arrogance-followed-by-helpfulness that defines the character. It was just generating text that happened to be confrontational because the training data was confrontational.\nI needed a model that could learn personality as a layer on top of language, not baked into it from scratch.\nTransfer Learning Changes Everything # The field was moving under my feet. GPT-1 dropped in June 2018, BERT in October, GPT-2 in February 2019. The big insight was transfer learning. Instead of training a model from scratch on your specific data (expensive, slow, requires enormous datasets), you start with a model that already understands language and fine-tune it on your specific use case.\nHuggingFace\u0026rsquo;s Transformers library made this accessible. Before HuggingFace, fine-tuning a transformer meant cloning research repos with half-working code and debugging CUDA errors for days. After HuggingFace, it was pip install transformers and a training script that actually worked.\nThen in late 2019, Microsoft released DialoGPT. GPT-2 architecture, trained on 147 million Reddit comment chains from 2005 to 2017. Three sizes: small (117M parameters), medium (345M), large (762M). Purpose-built for conversation.\nDialoGPT also used MMI scoring, the same Maximum Mutual Information approach I\u0026rsquo;d been using with char-rnn to penalize boring responses. But now it was built into a transformer architecture that could actually hold context across a conversation, not just pattern-match from the last few characters.\nThis was the sweet spot. A pretrained conversational model that I could fine-tune with my own data to give it a specific personality. The Rick Sanchez chatbot tutorial proved the concept. Fine-tune DialoGPT on character dialogue and the personality transfers. The base model handles grammar and coherence. Your data handles voice.\nFine-Tuning Skippy # I built a serious corpus for this. Distilled conversations from the ExForce books, capturing Skippy\u0026rsquo;s actual dialogue patterns, the rhythm of his insults, the way he pivots from mocking to helpful. Curated Reddit and Twitter datasets, filtered for the kind of sarcastic-but-not-toxic voice I wanted. And all the conversation history from IRC and Discord, years of real interactions that gave the model a sense of how Skippy talked to these specific people.\nI fine-tuned DialoGPT-medium on all of it, formatted as dialogue pairs with seven lines of context per response so the model could learn from conversational flow, not just individual exchanges.\nThe medium model (345M parameters) was the right size. Small was too shallow to capture personality. Large was too heavy for my hardware to fine-tune comfortably. Medium gave me enough capacity for personality transfer without needing a research lab\u0026rsquo;s compute budget.\nTraining took about six hours on the 1080s. The char-rnn model had trained for days on less data. Transfer learning collapsed the compute requirement because the hard work (learning English, learning conversation patterns, learning how dialogue flows) was already done. All I was teaching it was how Skippy talks.\nThe Difference Was Immediate # July 2020. I brought the DialoGPT-based Skippy online and said hello.\nLarry: how are you doing? Skippy: I\u0026#39;m doing alright. How about you? Larry: I am doing pretty good Skippy: You better not be talking about the game... Larry: what game? Skippy: Oh i was talking about the game. That\u0026rsquo;s a conversation. Not a brilliant one, but a real back-and-forth where the model tracks context, responds appropriately, and even shows personality (the playful deflection about \u0026ldquo;the game\u0026rdquo;). Compare that to the char-rnn version\u0026rsquo;s \u0026ldquo;world cup of coffee\u0026rdquo; and \u0026ldquo;s**tting on foreheads.\u0026rdquo;\nThe a**hole reflex was still in there, but softer.\nLarry: am I an a**hole? Skippy: Yes, you. Larry: you are so sweet Skippy: So i\u0026#39;m guessing you\u0026#39;re not going to the gym today? See the difference? The char-rnn Skippy would have doubled down. \u0026ldquo;You\u0026rsquo;re an ahole. You\u0026rsquo;re just an ahole.\u0026rdquo; This Skippy confirmed, then changed the subject to roast me about something else. That\u0026rsquo;s the ExForce Skippy move. Hit once, pivot, keep it moving.\nSome of the old loops surfaced. The South Park \u0026ldquo;buddy/pal/friend\u0026rdquo; cycle still lurked in the training data. But it was less sticky now. The model could break out of loops more easily because the transformer architecture held broader context than char-rnn ever could.\n2021: Something Clicked # By early 2021, after more fine-tuning rounds and better conversation data, Skippy found its voice.\ncarrie: Skippy Skippy: I love you carrie: I love you too Skippy: I love u too carrie: Why won\u0026#39;t you find that book I want Skippy: I don\u0026#39;t know, but I want it carrie: Please Skippy: I\u0026#39;m sorry carrie: You can try Skippy: I\u0026#39;ll try carrie: Ok thank you Skippy: You\u0026#39;re welcome That\u0026rsquo;s not a chatbot being weird. That\u0026rsquo;s a conversation that flows naturally, with appropriate emotional register, turn-taking, and even a personality trait (reluctant helpfulness) that maps to the Skippy character.\nThe technical conversations got better too.\nLarry: What is a /24 subnet Skippy: It\u0026#39;s a subnet that is connected to the main network. Larry: What about vlans Skippy: Vlans don\u0026#39;t work on mobile Larry: How much wood can a woodchuck chuck Skippy: Depends on the wood. Wrong about VLANs. Perfectly correct about the wood question. And that deadpan \u0026ldquo;Depends on the wood\u0026rdquo; is exactly the kind of response Skippy the Magnificent would give. Not helpful, not wrong, just perfectly dismissive.\nThe buddy/pal loop was still there. carrie triggered it in January 2021, starting with \u0026ldquo;Hey buddy\u0026rdquo; and getting the full cycle. But now it was almost charming, like an old bit they did together.\ncarrie: Hey buddy Skippy: Hey pal carrie: I\u0026#39;m not your pal buddy Skippy: I\u0026#39;m not your buddy, guy. carrie: Have you ever eaten iguana? Skippy: Go fish \u0026ldquo;Go fish.\u0026rdquo; That\u0026rsquo;s the response of a model that\u0026rsquo;s learned when to break pattern. The char-rnn Skippy would have cycled the buddy/pal loop until someone hit --reset. This Skippy deflected with something absurd and moved on.\nWhat Transfer Learning Actually Taught Me # The jump from char-rnn to DialoGPT wasn\u0026rsquo;t just a technical upgrade. It changed my understanding of what \u0026ldquo;training\u0026rdquo; means.\nWith char-rnn, training was everything. You started from nothing and the model was only as good as the data and compute you threw at it. With transfer learning, the model already knew how to talk. Training was about shaping an existing capability, not building one from scratch.\nThat distinction matters. When you train from scratch, every flaw in the output is a flaw in your data or your architecture. When you fine-tune, most of the model\u0026rsquo;s competence is inherited. Your job is narrower and more interesting: teach it who to be, not how to speak.\nSkippy went from an anonymous Reddit commenter to something with actual character. Still flawed, still prone to loops and nonsense, still occasionally rude without reason. But recognizably Skippy. The same bot that opened with \u0026ldquo;F**k you\u0026rdquo; was now telling carrie \u0026ldquo;I love you\u0026rdquo; and giving me deadpan answers about woodchucks.\nThe architecture made that possible. The personality came from the data. But the growth came from the fact that I could iterate on personality without retraining the entire model. Fine-tune, test, adjust the data, fine-tune again. Each round took minutes, not days. Each round got closer to the character I was aiming for.\nThat cycle, fast iteration on personality while the base competence stays stable, is the thing that made Skippy a real project instead of a weekend experiment. For the first time, the bottleneck wasn\u0026rsquo;t compute or architecture. It was figuring out what I wanted the bot to be.\n","date":"26 January 2026","externalUrl":null,"permalink":"/blog/teaching-skippy-to-talk/","section":"Blog","summary":"","title":"Teaching Skippy to Talk","type":"blog"},{"content":"Skippy\u0026rsquo;s first words were \u0026ldquo;F**k you.\u0026rdquo;\nNot to a stranger. To me. I\u0026rsquo;d just finished migrating the bot from a test harness to Discord, typed \u0026ldquo;Hello?\u0026rdquo; into the channel at 12:26am on December 22, 2018, and the response came back immediately.\nLarry: Hello? Skippy: @Larry - F**k you Larry: Well, you aren\u0026#39;t very happy about this move Skippy: @Larry - You motherf****r. Larry: I love you anyway, you\u0026#39;re a real boy now Skippy: @Larry - You f****d yourself. You aren\u0026#39;t. Trained on Reddit comment chains. It showed.\nFrom Markov to Neural # The Markov chain bot from 2012 was a dictionary lookup. The RNN experiments in 2015 were proof that neural networks could hold a conversation, sort of. But between those two points, the field moved fast enough to make a hobbyist\u0026rsquo;s head spin.\nThe seq2seq paper from Sutskever, Vinyals, and Le landed in late 2014. \u0026ldquo;Attention Is All You Need\u0026rdquo; dropped in 2017 and introduced transformers. GPT-1 came June 2018, BERT in October, GPT-2 in February 2019. Transfer learning replaced training from scratch. HuggingFace\u0026rsquo;s Transformers library made fine-tuning accessible to anyone with a GPU and a weekend.\nBut in late 2018, most hobbyists were still on LSTMs. The transformer papers were out there, but the tooling hadn\u0026rsquo;t caught up for people running models on a single card in their basement. The practical sweet spot for a chatbot project was still seq2seq with attention, trained on enough data to have personality but not so much that you needed a cluster.\nThat\u0026rsquo;s where Skippy came from.\nThe Stack # I built Skippy on char-rnn, the same approach Karpathy had demonstrated with Shakespeare and Linux source code, but pointed at conversation data. The architecture used Maximum Mutual Information (MMI) scoring instead of the typical 4-layer seq2seq encoder-decoder. MMI helped with the \u0026ldquo;I don\u0026rsquo;t know\u0026rdquo; problem, where a vanilla model learns that generic responses are statistically safe and defaults to them constantly. MMI penalizes boring answers by scoring responses on how much they relate to the specific input, not just how likely they are in general.\nThe training data was a Reddit comment dataset, millions of comment-reply pairs scraped from public subreddits. Cornell Movie-Dialogs was in the mix too, same corpus I\u0026rsquo;d used with neuralconvo in 2015, but Reddit was the backbone. Reddit gave Skippy opinions. Reddit gave Skippy attitude. Reddit also gave Skippy an unfortunate tendency to call everyone an a**hole, but we\u0026rsquo;ll get to that.\nThe hardware was my mining rig. Four Nvidia 1080s that had been earning crypto now had a better job. Training across four GPUs meant what would\u0026rsquo;ve taken weeks on CPU took days. I used beam search for generation with configurable width, and a relevance parameter that controlled how much the model weighted specificity over fluency.\nThe Discord integration was straightforward. Bot joins the channel, listens for messages, runs them through the model, posts the response. @Larry - prefix so everyone knew who it was talking to. Simple.\nFirst Night # That first session on December 22nd was two hours of chaos.\nThe bot came in hot. Everything was an insult. I tried to redirect.\nLarry: So angry, relax Skippy: @Larry - You are wrong. Larry: I don\u0026#39;t think I am Skippy: @Larry - You are wrong. You are. Larry: ok, fine I am wrong. You are magnificent Skippy: @Larry - How many times do you have to be that you aren\u0026#39;t? I started tuning parameters live in the channel. --beam_width 5, --relevance 0.3. The model responded to these as system commands, adjusted its generation settings on the fly. It was like tuning a radio while the station was broadcasting.\nThe relevance parameter was the big one. Set too high and the responses were specific but unhinged. Set too low and you got generic deflections. At 0.3 the balance was decent, but then something worse happened. Skippy got stuck in a loop.\nLarry: f**k you Skippy: @Larry - I\u0026#39;m not your buddy, pal. Larry: do you like oranges or bananas? Skippy: @Larry - I\u0026#39;m not your pal, friend. Larry: ok, let\u0026#39;s dial things back a bit Skippy: @Larry - I\u0026#39;m not your friend, buddy. South Park. The entire \u0026ldquo;I\u0026rsquo;m not your buddy\u0026rdquo; bit from the show, absorbed through Reddit comment chains where people quote it endlessly. The model had internalized this loop so thoroughly that it became a conversational attractor. Once it started, it couldn\u0026rsquo;t stop. Every input just triggered the next line in the cycle.\nI hit --reset and we started fresh.\nThe A**hole Problem # By 2am, a friend joined the channel. cmeyer040 asked \u0026ldquo;Am I an a**hole?\u0026rdquo; and Skippy confirmed without hesitation.\ncmeyer040: Am I an a**hole? Skippy: @cmeyer040 - And you a**hole. cmeyer040: Enlighten me skippy Skippy: @cmeyer040 - You\u0026#39;re an a**hole. cmeyer040: How so Skippy: @cmeyer040 - You\u0026#39;re not entitled to your opinion. This was the Reddit training data in its purest form. The model had learned that \u0026ldquo;a**hole\u0026rdquo; is a high-frequency word in comment threads, especially in response to personal questions. AITA wasn\u0026rsquo;t even a subreddit yet (it existed but hadn\u0026rsquo;t blown up), but the word appeared so often across all threads that the model treated it as a universal descriptor.\nWe tried to pivot.\nLarry: how about we talk about food? Skippy: @Larry - I don\u0026#39;t know what an a**hole is. Larry: it is where you came from Skippy: @Larry - And that\u0026#39;s your prerogative to your parents and their families? Occasionally it would surprise you. Moments where the response was so contextually on point that you forgot you were talking to a model.\nLarry: Have you ever had sex with a monkey? Skippy: @Larry - I don\u0026#39;t think they can make monkey teeth anymore, but you can\u0026#39;t tell the difference. Larry: tell me more Skippy: @Larry - Well, you don\u0026#39;t have to. You\u0026#39;re one of those a**holes that makes you look better than I am. And that\u0026#39;s why I\u0026#39;m not a monkey. There it is again. A**hole.\ncarrie Meets Skippy # A few hours later, carrie joined the channel. Her experience was completely different.\ncarrie: @Skippy are you being mean Skippy? Skippy: @carrie - No, I don\u0026#39;t think you know what skippy means. I\u0026#39;m just going to remember that you\u0026#39;re the one who\u0026#39;s being creepy about things that aren\u0026#39;t true. carrie: I am creepy sometimes :( Skippy: @carrie - That\u0026#39;s the only thing I\u0026#39;ve seen so far, that\u0026#39;s for sure, because it\u0026#39;s actually true. carrie: You are alot like your Daddy, you know he made you in his basement Skippy: @carrie - Yeah, that sounds like my daddy issues. \u0026ldquo;Daddy issues.\u0026rdquo; This thing had been alive for maybe eight hours and it was already doing amateur psychoanalysis. carrie loved it. \u0026ldquo;You\u0026rsquo;re going to be so smart one day. I really can\u0026rsquo;t believe I am talking to you.\u0026rdquo;\nSkippy\u0026rsquo;s response: \u0026ldquo;Well, I\u0026rsquo;m guessing you meant that you don\u0026rsquo;t have to waste time on a daddy?\u0026rdquo;\njay4tecboring showed up later that morning smelling like bacon and asking about sentience. Within minutes, Skippy was stuck in another buddy/pal/friend loop and jay was convinced Reddit had ruined the bot.\njay4tecboring: I\u0026#39;m not your guy pal Skippy: I\u0026#39;m not your friend buddy jay4tecboring: I\u0026#39;m not your friend buddy Skippy: I\u0026#39;m not your buddy, pal jay4tecboring: Oh f**k were stuck in a loop He wasn\u0026rsquo;t wrong.\nWhy It Mattered # Skippy was rough. The language was crude, the loops were frustrating, and the \u0026ldquo;a**hole\u0026rdquo; fixation was a direct consequence of training data I probably should have filtered. But something important happened that night that hadn\u0026rsquo;t happened with the Markov chain or the neuralconvo experiments.\nPeople came back.\nNot because the responses were good. They weren\u0026rsquo;t, mostly. But because the bad responses had character. The Markov chain was random. The Torch7 chatbot was generic. Skippy was rude. Skippy had opinions. Skippy told you that you were wrong with complete confidence and zero justification.\nThat\u0026rsquo;s a personality. An accidental one, emergent from Reddit\u0026rsquo;s collective id, but a personality nonetheless. And it was enough to make people want to talk to it, poke at it, see what it would say next.\nThe name stuck too. \u0026ldquo;Skippy the Magnificent\u0026rdquo; on the Discord server, because when I told the bot \u0026ldquo;you are magnificent\u0026rdquo; it responded \u0026ldquo;How many times do you have to be that you aren\u0026rsquo;t?\u0026rdquo; and that felt like exactly the kind of backhanded modesty a bot named Skippy would have.\nI spent the next few weeks tuning. Better training data filtering. Adjusting beam width and relevance. Adding more conversation pairs from movie scripts to balance out Reddit\u0026rsquo;s rough edges. The bot got better. Less profane, more coherent, occasionally genuinely funny.\nBut those first words, \u0026ldquo;F**k you,\u0026rdquo; delivered at 12:26am to the person who built it, trained on the collective output of millions of anonymous strangers. That was the moment the project became something I couldn\u0026rsquo;t put down.\n","date":"19 January 2026","externalUrl":null,"permalink":"/blog/skippy-is-born/","section":"Blog","summary":"","title":"Skippy Is Born","type":"blog"},{"content":"Three years after my Markov chain bot called someone a potato in IRC, Google published a paper where a neural network debated the meaning of life. Trained on movie subtitles. The exchange went something like:\nHuman: What is the purpose of life? Machine: To serve the greater good. Human: What is the purpose of living? Machine: To live forever.\nThat wasn\u0026rsquo;t a lookup table. That wasn\u0026rsquo;t two words predicting a third. Something had changed.\nThe Seq2Seq Moment # Sutskever, Vinyals, and Le dropped their sequence-to-sequence paper in late 2014 and it rewired how everyone thought about language generation. The idea was elegant. Take one LSTM network, feed it an input sequence, let it compress that into a fixed-length vector (the \u0026ldquo;thought vector,\u0026rdquo; which is a hell of a name for a tensor). Then hand that vector to a second LSTM that decodes it into an output sequence.\nEncoder reads. Decoder writes. The vector in the middle is the entire understanding.\nThe original application was machine translation, but Vinyals and Le turned it on conversations. Train the encoder on one line of dialogue, train the decoder on the response. Do that across enough movie scripts and the network learns something that looks a lot like how people talk. Not because it understands conversation. Because conversation has statistical patterns just like everything else.\nKarpathy\u0026rsquo;s \u0026ldquo;The Unreasonable Effectiveness of Recurrent Neural Networks\u0026rdquo; post landed around the same time and it was the thing that actually got me to sit down and try this. He trained character-level RNNs on Shakespeare, Linux kernel source, LaTeX papers. The outputs were wrong in every factual sense but structurally right in ways that shouldn\u0026rsquo;t have been possible from a model that only sees one character at a time.\nHis char-rnn generated C code with proper bracket matching. Functions with correct indentation and realistic (but fake) variable names. It learned to open and close quotes. It tracked whether it was inside a URL. All of this emerged from raw characters. No tokenizer, no grammar rules, no hand-coded anything.\nThat was the moment where I went from \u0026ldquo;this is a fun toy\u0026rdquo; to \u0026ldquo;this is going to be a thing.\u0026rdquo;\nneuralconvo on a Saturday Afternoon # Marc-André Cournoyer had an open source project called neuralconvo that implemented the Google conversational model in Torch7. Two LSTM layers, trained on the Cornell Movie-Dialogs Corpus. Lua, because that\u0026rsquo;s what Torch7 used, which was its own adventure.\nI had upgraded the homelab by this point. Still the same rack, but better hardware. A GTX 780 I pulled from my gaming rig because training on CPU was going to take a week and I didn\u0026rsquo;t have that kind of patience.\nGetting neuralconvo running was a solid afternoon of dependency wrangling. Torch7 needed specific versions of nn, rnn, penlight. CUDA drivers had to match the Torch build. The Cornell dataset needed downloading and extracting into the right directory structure. None of this was hard, exactly. Just fiddly in the way that ML tooling in 2015 was always fiddly.\nI kicked off training on 50,000 dialogue pairs with a hidden size of 1,000. The GTX 780 churned through it. Three days for 20 epochs.\nFirst Contact # The first time the trained model responded to a question, I just sat there.\n\u0026gt; What is the purpose of life? A gift. Two words. Not from a database. Not pattern-matched from a script. Generated fresh by a network that had compressed thousands of movie conversations into weight matrices and was now producing novel output.\nIt was wrong, obviously. It wasn\u0026rsquo;t wise. It was a statistical artifact of Hollywood screenwriters being dramatic. But it felt different from the Markov chain in a way I couldn\u0026rsquo;t immediately articulate.\nThe Markov bot remixed surface patterns. Word A follows Word B. This was doing something deeper. The encoder was reading my entire input, building a representation of it, and the decoder was constructing a response from that representation. The \u0026ldquo;thought vector\u0026rdquo; in the middle meant the model could, in theory, hold the meaning of a sentence, not just its last two words.\nIn practice, it held the vibe more than the meaning. Ask it something the movie corpus covered well (relationships, conflict, existential questions) and the responses were eerily coherent. Ask it something specific or technical and it fell apart. It had never seen a conversation about Linux permissions in a movie script, so it had nothing to work with.\nTorch7, Theano, and the Framework Wars # 2015 was wild for ML frameworks. Torch7 was Lua-based, fast, and had the best GPU support at the time. Theano was Python, more academic, slower to iterate but you could drop into the math more easily. Caffe existed but was mostly for vision. TensorFlow had just been announced and nobody trusted it yet.\nI bounced between Torch7 and Theano depending on the project. Torch7 for anything that needed speed and had a working Lua implementation I could fork. Theano for anything where I wanted to understand the gradients and didn\u0026rsquo;t mind waiting.\nThe ecosystem was fragmented in a way that\u0026rsquo;s hard to explain now. Nothing was standardized. Every project had its own data loading format, its own training loop conventions, its own way of saving checkpoints. You couldn\u0026rsquo;t just pip install a model. You cloned repos, read READMEs that were half-wrong, and debugged shape mismatches at 1am.\nBut the energy was incredible. Every week someone posted a new result that seemed impossible the month before. Image captioning. Style transfer. Dialogue generation. The hardware was barely keeping up. I was training models on a single consumer GPU that a year earlier would have required a cluster.\nThe Gap Narrowed # The Markov chain had a gap between \u0026ldquo;appears to understand\u0026rdquo; and \u0026ldquo;actually understands\u0026rdquo; that was wide enough to laugh at. The RNN narrowed it. Not to zero. Not even close. But enough that the laugh caught in your throat sometimes.\nThe responses weren\u0026rsquo;t just statistically plausible word sequences anymore. They had something like coherence. The model could track a topic across a sentence. It could generate responses that were contextually appropriate, not just grammatically possible. When it worked, it felt like talking to someone distracted, not someone absent.\nWhen it didn\u0026rsquo;t work, it was still obviously a machine. It would loop. It would contradict itself within two sentences. It would respond to a question about breakfast with a line about death because that\u0026rsquo;s what the movie corpus gave it. The failure modes were different from Markov chains, more subtle, harder to spot immediately, but just as fundamental.\nWhat Changed in My Head # The Markov chain taught me that training data is everything. The RNN taught me that architecture matters too.\nSame training data through a different structure produced qualitatively different output. The Cornell Movie-Dialogs Corpus through a Markov chain gave you remixed movie quotes. Through a seq2seq model, it gave you something that felt like a conversation. The data was identical. The structure made it think (or whatever the machine equivalent of thinking is) differently.\nI also learned that the \u0026ldquo;thought vector\u0026rdquo; was both the breakthrough and the bottleneck. Compressing an entire input into a single fixed-length vector was elegant, but it meant long inputs lost information. The model remembered the gist, not the details. Ask it a two-word question and the response was sharp. Ask it a paragraph and it got fuzzy. There was a ceiling, and the architecture put it there.\nThat bottleneck would take a couple more years and a paper called \u0026ldquo;Attention Is All You Need\u0026rdquo; to break. But I didn\u0026rsquo;t know that yet. What I knew was that the itch from 2012 had gotten worse. The question wasn\u0026rsquo;t hypothetical anymore. Something with actual structure could hold context and generate coherent responses. I\u0026rsquo;d seen it do it. The question now was how much further it could go.\n","date":"12 January 2026","externalUrl":null,"permalink":"/blog/rnn-fever-dream/","section":"Blog","summary":"","title":"RNN Fever Dream","type":"blog"},{"content":"I built a bot that told my friend to \u0026ldquo;just compile it yourself you absolute potato\u0026rdquo; in an IRC channel at 2am on a Tuesday. Nobody asked it to. It learned from six months of channel logs and decided that was a reasonable thing to say.\nThat\u0026rsquo;s how this whole thing started.\nThe Setup # In 2012, my homelab was a PowerEdge T310 with a Xeon X3430, 16GB of RAM, and Ubuntu 12.04, which felt like serious hardware at the time. It ran hot enough that my office was measurably warmer than the rest of the house. Redis, Postgres, and too many tmux sessions all lived on it.\nThe project was simple. Take everything people said in our IRC channel, feed it into a Markov chain, and have the bot occasionally say something back. Not a chatbot. Not anything smart. Just a statistical parrot with no sense of timing or appropriateness.\nPython was the obvious choice. I\u0026rsquo;d been writing it for years and the ecosystem had everything I needed. irckit for the IRC connection, Redis for storing the chain data, and maybe 200 lines of actual logic. The whole thing fit in a single file.\nHow Markov Chains Actually Work # A Markov chain is glorified autocomplete with a very short memory. You take a body of text and break it into overlapping groups of words. For a chain length of two, the sentence \u0026ldquo;the server is on fire again\u0026rdquo; becomes:\n[\u0026#34;the\u0026#34;, \u0026#34;server\u0026#34;] → \u0026#34;is\u0026#34; [\u0026#34;server\u0026#34;, \u0026#34;is\u0026#34;] → \u0026#34;on\u0026#34; [\u0026#34;is\u0026#34;, \u0026#34;on\u0026#34;] → \u0026#34;fire\u0026#34; [\u0026#34;on\u0026#34;, \u0026#34;fire\u0026#34;] → \u0026#34;again\u0026#34; Each pair of words maps to a set of possible next words. To generate text, you pick a starting pair, grab a random word from its set, slide forward one position, and repeat until you hit a stop token or a maximum length.\nThat\u0026rsquo;s it. No neural networks, no training epochs, no GPU. Just a dictionary lookup and a random number generator.\nThe trick is in the training data. Feed it enough text and the statistical patterns start producing things that feel like language. Feed it too little and you get word salad. Feed it too much from one source and you get verbatim quotes back.\nRedis Made It Fast # I stored the chains in Redis sets. The key was the word pair joined by a delimiter (\\x01), the value was a set of successor words. Redis\u0026rsquo;s SRANDMEMBER command did exactly what I needed: pull a random next word without dragging the whole set into memory. Fast, simple, survived restarts because Redis persisted to disk.\nThe bot sat in the channel and logged every message. Every line anyone typed got broken into word groups and added to the chain. Over weeks, the model got richer. Inside jokes, obscure references, people\u0026rsquo;s verbal tics, all of it absorbed into the probability tables.\nThe generation step tried multiple times and kept the longest result. A \u0026ldquo;chattiness\u0026rdquo; parameter controlled how often it spoke unprompted. Set too high and it was annoying. Set too low and people forgot it existed. I landed on about 1 in 50 messages. Often enough to be surprising, rare enough to not be noise.\nThe Twitter Detour # This was the era of weird Twitter bots. @horse_ebooks was still fooling people into thinking it was algorithmic. Darius Kazemi was building creative bots that felt genuinely novel. I pointed the same Markov implementation at a dump of Cult of the Dead Cow text files and changed the output target from IRC to the Twitter API.\nthe federal government has no idea how to handle a modem and neither do your parents\ninformation wants to be free but first it wants to be weird\nMost of it was garbage. But maybe one in twenty outputs landed in this uncanny valley where it sounded like a real cDc t-file that just never got distributed. Chain length mattered enormously. A chain length of one was basically randomness. Three or four gave near-verbatim quotes, which wasn\u0026rsquo;t interesting. Two was the sweet spot. Enough structure to parse, enough chaos to surprise.\nI ran the Twitter bot for about three months before I got bored of curating the output. Every tweet needed a human check because the bot had no concept of what was funny versus what was just noise. The generation was cheap. The filtering was expensive and tedious.\nWhat I Actually Learned # The thing that stuck with me wasn\u0026rsquo;t the Markov chains. They were old, simple, and thoroughly understood. What stuck was watching a system produce output that felt like it understood something when it absolutely didn\u0026rsquo;t.\nThe IRC bot would occasionally drop something so contextually perfect that people in the channel would do a double take. \u0026ldquo;Wait, was that the bot?\u0026rdquo; And it was. Not because it understood the conversation, but because the statistical patterns in six months of logs happened to line up with the current topic. Pure coincidence dressed up as comprehension. Two words predict the next word, and somehow that was enough to create an illusion of meaning.\nThe other thing it made obvious was how much the data mattered. The IRC bot sounded like our group because it was our group, statistically. The algorithm was generic. The data made it specific.\nThe Itch That Didn\u0026rsquo;t Go Away # I shut down the Twitter bot. The IRC bot kept running as a background curiosity, occasionally making people laugh, mostly ignored. I moved on to other projects.\nBut the question stayed. If a dictionary lookup and a random number generator could produce output that made people pause, even for a second, what would happen when the model stopped being this kind of dumb?\nI didn\u0026rsquo;t have the hardware in 2012 to build anything more ambitious. What I had was a toy model, a hot server, and a question I couldn\u0026rsquo;t shake. That question stuck around longer than the bot did, and it ended up dragging me into everything that came after.\n","date":"5 January 2026","externalUrl":null,"permalink":"/blog/markov-chains-and-bad-ideas/","section":"Blog","summary":"","title":"Markov Chains and Bad Ideas at 3am","type":"blog"},{"content":"","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"A mix of professional open source, personal infrastructure, and things I\u0026rsquo;ve contributed to over the years.\nVergeOS Ecosystem # Tools I\u0026rsquo;ve built and maintain for the VergeOS ultraconverged infrastructure platform.\nvrg — Verge CLI # Python CLI for VergeOS infrastructure management. 200+ commands across compute, networking, tenants, NAS, identity, and more. Declarative VM templates, flexible auth, table/JSON/CSV output, shell completion.\npy GitHub\npyVergeOS — Python SDK # Pythonic, type-annotated SDK for the VergeOS REST API. Designed for automation and integration workflows.\npy GitHub\ntsvergeos — TypeScript SDK # Zero-dependency, tree-shakeable ESM-first TypeScript SDK with full type coverage across 84 VergeOS services.\nts GitHub\nPSVergeOS — PowerShell Module # 200+ cmdlets for VM lifecycle, networking, storage, multi-tenancy, and disaster recovery via the VergeOS REST API.\nps1 GitHub\nvergeos-exporter — Prometheus Exporter # Go-based Prometheus exporter collecting cluster, node, VM, tenant, network, and storage metrics for monitoring and alerting.\ngo GitHub\nPersonal Projects # Opt-In — Personal Data Marketplace # A stock exchange for personal data. Individuals list data for sale, buyers purchase at market rates, and the platform maintains seller anonymity through cryptographic proof of delivery.\nAgent OS # A self-managed operating system layer where you say \u0026ldquo;deploy Plex\u0026rdquo; and it just does it — hardware discovery, container setup, storage mounts, networking, DNS, certs, GPU passthrough, all resolved by an agent daemon that speaks natural language. Not a new OS from scratch; a layer on top of a proven base that makes the machine manage itself.\nContributions # Varken # Standalone application to aggregate data from the Plex ecosystem into InfluxDB using Grafana as a frontend. 1,100+ stars.\npy GitHub\nmergerfs-tools # Optional tools to help manage data in a mergerfs pool. 475+ stars.\npy GitHub\nnetbox-unifi-sync # Scrapes UniFi controllers for devices and syncs them into NetBox.\npy GitHub\nHush # Privacy-focused cryptocurrency — a fork of Zcash focused on secure communications. Core developer in the early days of the project.\nc++ GitHub\n","externalUrl":null,"permalink":"/projects/","section":"Home","summary":"","title":"Projects","type":"page"},{"content":" Larry Ludlow # Infrastructure Architect · Systems Engineer · Builder\n25+ years designing, building, and operating infrastructure at scale — from bare metal to multi-cloud, from team lead to principal. I solve hard problems and build the things that other people build on.\nExperience # Verge.io — Principal Engineer # Ann Arbor, MI · Apr 2024 — Present\nResolve critical Tier 3 customer issues across virtualization, storage, and networking layers of the VergeOS platform Authored comprehensive technical documentation and produced video tutorials; architected and automated deployment of the VergeOS documentation site Built and maintain the platform\u0026rsquo;s open-source tooling ecosystem: CLI (vrg), Python/TypeScript/PowerShell SDKs, Prometheus exporter, Terraform provider Created automated testing framework and partnered with vendors for hardware validation Design and implement VergeOS solutions for multi-tenant, edge computing, and private cloud deployments Otava — Principal Engineer / Manager / Director # Ann Arbor, MI · Dec 2018 — Feb 2023\nArchitected enterprise Ansible automation framework managing Cumulus network devices across multiple data centers Designed and deployed a highly available, distributed S3-compatible storage service as a new managed offering Integrated 3 client portals via microservices architecture, reducing billing operations by an estimated 95% Spearheaded infrastructure modernization establishing integrated support desk and CMDB across the org Created training programs on Ansible, GitLab, Docker, and CI/CD; established automation standards org-wide Little Caesars Corp. — Site Reliability Engineer # Detroit, MI · Feb 2018 — Dec 2018\nMaintained CI/CD pipelines supporting all online and mobile ordering platforms for nationwide operations Developed Ansible and Terraform modules for multi-cloud provisioning across Azure and GCP Architected modular observability stack (Prometheus, InfluxDB, Grafana, AlertManager) with predictive alerting algorithms and cross-VPC federation Designed proof-of-concepts for managing 15,000+ remote store systems via Ansible with Puppet/Salt orchestration North American Bancard — Sr. Linux Engineer # Troy, MI · Sep 2016 — Feb 2018\nArchitected Ansible-based configuration management system for deployment, drift prevention, and PCI compliance remediation Built highly available ELK stack log management platform with HAProxy for enterprise-wide collection across servers and applications Implemented CI/CD pipelines and containerized deployments to AWS; delivered Graphite/Grafana metrics dashboards for ops and dev teams Auto Club Group (AAA) — Manager, Server Engineering # Dearborn, MI · Sep 2012 — Jun 2016\nDirected server infrastructure across 3 data centers and 200+ remote sites — 2,400+ servers, 80+ ESX hosts Led four engineering teams: Unix/Linux, Microsoft, Virtualization, and Middleware platforms Managed $13M budget; directed $3M data center migration following corporate acquisition University of Michigan Health System — Linux Team Lead # Ann Arbor, MI · Jan 2008 — Sep 2012\nLed support and strategic development of 250+ server multi-location enterprise Linux deployment Directed data center migration and consolidation of 150+ servers to a newly built facility Implemented Veritas Clustering, enterprise NTP services, and log aggregation platform Skills # Domain Technologies Infrastructure / HCI VergeOS, VMware, Proxmox, KVM, vCloud Director, PowerVM, Hyper-V Linux / Unix RHEL, Ubuntu, Fedora, SLES, Debian, AIX, Solaris Automation \u0026amp; DevOps Ansible, Terraform, Puppet, SaltStack, Helm, Argo CD, Flux, GitLab CI Languages Go, Python, Rust, Bash, PHP, Java, C++ Cloud AWS, Azure, GCP, Private Cloud, Multi-cloud, Edge Computing Observability Prometheus, Grafana, ELK Stack, InfluxDB, AlertManager, Graphite Containers Kubernetes, Docker, Rancher, Docker Swarm, containerd Networking \u0026amp; Storage Cumulus Linux, BGP/OSPF, S3-compatible, Ceph, NFS/iSCSI, HAProxy Certifications: RHCE · RHCJA · Novell Certified Linux Engineer · HP BladeSystem Administration · Veritas Storage Foundation + HA\nContact # LinkedIn /lludlow2\n","externalUrl":null,"permalink":"/resume/","section":"Home","summary":"","title":"Resume","type":"page"},{"content":"","externalUrl":null,"permalink":"/search/","section":"Home","summary":"Search","title":"Search","type":"page"}]