{
	"version": "https://jsonfeed.org/version/1",
	"title": "enumerator.dev",
	"icon": "<no value>",
	"home_page_url": "https://enumerator.dev/",
	"feed_url": "https://enumerator.dev/feed.json",
	"items": [
			{
				"id": "https://enumerator.dev/brain-burger/",
				"title": "Brain Burger",
				"content_html": "<p>I have tried to describe my agentic coding workflows as they&rsquo;ve evolved and Emily captures it perfectly. I use a Brain Burger (or Brain Sandwich):</p>\n<ol>\n<li>My brain, <em>and then</em></li>\n<li>AI, <em>and then</em></li>\n<li>My brain</li>\n</ol>\n<p>via <a href=\"https://terriblesoftware.org/2026/10/02/the-brain-sandwich/\">The Brain Sandwich</a></p>\n",
				"date_published": "2026-09-29T00:00:00+00:00",
				"url": "https://enumerator.dev/brain-burger/",
				"tags": ["ai"]
			},
			{
				"id": "https://enumerator.dev/differently-difficult/",
				"title": "Differently Difficult",
				"content_html": "<p>In <a href=\"https://cacm.acm.org/opinion/ai-didnt-make-programming-easier-it-just-made-it-differently-difficult/\">AI Didn’t Make Programming Easier. It Just Made It Differently Difficult</a>, Jeremy Osborn writes:</p>\n<blockquote>\n<p>In other words, the hard part moves from recall (“How do I write this?”) to judgment (“Does this actually make sense?”). This shift from recall-based to judgment-based programming represents the fundamental cognitive transformation at the heart of AI-assisted development. Where traditional programming demanded that developers maintain vast internal libraries of syntax, patterns, and idioms, AI-enabled programming demands instead they maintain robust evaluative frameworks for assessing correctness, coherence, and appropriateness. The cognitive burden has not disappeared—it has relocated from retrieval to reasoning.</p>\n</blockquote>\n",
				"date_published": "2026-09-29T00:00:00+00:00",
				"url": "https://enumerator.dev/differently-difficult/",
				"tags": ["ai","career-growth"]
			},
			{
				"id": "https://enumerator.dev/how-i-use-ai-when-writing/",
				"title": "How I use AI When Writing",
				"content_html": "<p>I use AI when I write for work.</p>\n<p>And like you, I hate slop.</p>\n<p>Despite LLMs writing like someone <a href=\"https://martinfowler.com/articles/2026-dont-like-llms.html\">I would never want to spend time with</a>, I still find some of their output useful.</p>\n<p>They speed up research, even if they are confidently wrong.</p>\n<p>And they are mediocre grammar checkers and good enough document structure editors, even if they are obtuse.</p>\n<h2 id=\"my-writing-standards\">My Writing Standards</h2>\n<h3 id=\"when-my-writing-is-personal\">When My Writing is Personal</h3>\n<p>I don&rsquo;t write personal things with AI. This includes text messages, emails to friends and family, and Slack messages, comments on documents.</p>\n<h3 id=\"when-i-write-for-this-blog\">When I Write for this Blog</h3>\n<p>Most of my writing on this blog is free form with limited edits. I hit publish and then find a mistake when I read it on my site and I&rsquo;m quite okay with that.</p>\n<p>However, I use LLMs to assist with research and validation. Every now and then, I&rsquo;ll do a double check on grammar and structure with a writing tool like LanguageTool or Grammarly, and I&rsquo;ll occasionally accept phrasing that an LLM suggests if it suits my needs.</p>\n<h3 id=\"when-my-writing-is-an-artifact-for-work\">When My Writing is An Artifact for Work</h3>\n<p>At work I write documentation and proposals that are artifacts the company can discuss and work with.</p>\n<p>I use today&rsquo;s AI tools to research ideas, validate my assumptions, fact-check my work, and edit the document&rsquo;s structure. More on this later.</p>\n<h3 id=\"when-writing-generates-code\">When Writing Generates Code</h3>\n<p>I write to AI agents in the form of chat interfaces, documents, and structured prompts to generate code. I use all the tools available to me:</p>\n<ul>\n<li>Type out my chat prompts by hand.</li>\n<li>Dictate through a voice-to-text model.</li>\n<li>Dictate through a voice-to-text model that is passed through an LLM to generate a structured output.</li>\n<li>Have an agent generate a document, then have a second agent read and validate that document.</li>\n</ul>\n<p>Essentially, there are no rules when I write in order to generate code. I don&rsquo;t even consider most of this &ldquo;writing.&rdquo; But it does result in generated text that has my name attached to it.</p>\n<h2 id=\"how-i-use-ai-to-write-an-artifact\">How I use AI to Write an Artifact</h2>\n<ol>\n<li><strong>Use a number of agents to do research.</strong> This often involves getting a coding agent to research and test ideas in a code repository, sending an agent with computer use off to do web research and produce a document, or chatting with an LLM to validate ideas.</li>\n<li><strong>Save agent-generated artifacts.</strong> Between chat sessions, I get the agents to save artifacts other agents can find. Coding agents write docs, desktop agents find and read those docs and generate text files or docs through an MCP connector, and chat agents generate text I copy-paste elsewhere.</li>\n<li><strong>Write a doc in my own words.</strong> After iterating with agents to build an understanding of what I&rsquo;m writing, I start writing in my own words. The result is a document that is well-researched and thought-through. And very human. It has my tone, thought process, and mistakes.</li>\n<li><strong>Edit with an LLM.</strong> I have two prompts that I use to edit both my work and docs the agent generates. For my own writing, I prefer to make the edits myself. Occasionally, I agree with the LLM&rsquo;s feedback but am stumped on how to make a change, so I&rsquo;ll ask for ideas.</li>\n<li><strong>DON&rsquo;T TRUST THE LLM.</strong> I use my judgement when the LLM gives me feedback. Its generated response often has good points that slightly miss the mark. I take those into account and use my own words.</li>\n<li><strong>Use LLM-generated text when it helps.</strong> It feels like I&rsquo;ve admitted to a mortal sin here. I am writing for business, and LLMs are trained on business applications. I have no problem copy-pasting a sentence from an LLM at this point because I&rsquo;ve done the legwork and the LLM is helping with polish.</li>\n<li><strong>DON&rsquo;T ARGUE WITH THE LLM</strong>. It is going to generate text that makes no sense. Ignore this. When I have tried to correct things the LLM gets wrong, it begins to misunderstand the task overall. I often tell it what was good and then say something like &ldquo;You can do better than this, I know you can. Give it a second, more thoughtful pass.&rdquo; If is WEIRD to say things like that to a computer, but it works for me.</li>\n<li><strong>Share my work.</strong> <del>I share a document with two sections. The first is my writing. The second is a summary of the LLM&rsquo;s research, generated by the LLM. I flag this as AI-generated content in a BIG banner at the top.</del> [EDIT: Oct 3, 2026] I no longer find value in the AI generated section. I share my writing with a notice at the top explaining the document was written by me with AI assisted research.</li>\n</ol>\n<h2 id=\"ai-agent-prompts\">AI Agent Prompts</h2>\n<h3 id=\"research-prompts\">Research Prompts</h3>\n<p>In my research prompts, I tell the agents my <strong>objectives, opinions, and assumptions.</strong> This context helps the agent focus on the outcome I am asking for. It also forces me to think about my opinions and <a href=\"/assumptions/\">assumptions</a>. I expect the agent to confirm my assumptions and push back on my opinions. If it doesn&rsquo;t do this, I ask it to.</p>\n<p>Here is a template of a prompt I use:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-fallback\" data-lang=\"fallback\"><span class=\"line\"><span class=\"cl\">I am researching &lt;feature&gt;. I need you to research &lt;technology&gt; in this\n</span></span><span class=\"line\"><span class=\"cl\">repository and write a summary that explains the current state.\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">Focus on &lt;area of code&gt;.\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">Ignore &lt;similar but irrelevant area of code&gt;.\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">My objective is to &lt;write your objective&gt;.\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">My opinion is that &lt;what I think the research will reveal and what decision\n</span></span><span class=\"line\"><span class=\"cl\">I lean towards already&gt;.\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">My assumptions are &lt;how the repo works, how the technology works with the\n</span></span><span class=\"line\"><span class=\"cl\">repo, etc.&gt; \n</span></span></code></pre></div><h3 id=\"prompts-for-editing\">Prompts for Editing</h3>\n<p>I have been working on a skill I call &ldquo;<a href=\"/writing-for-understanding.txt\">Writing for Understanding</a>&rdquo; that is based on a number of accessible writing standards from government websites and based on my own experience. I pass all of the writing through this with the following prompt:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-fallback\" data-lang=\"fallback\"><span class=\"line\"><span class=\"cl\">/writing-for-understanding read @doc and give me feedback one bullet\n</span></span><span class=\"line\"><span class=\"cl\">point at a time. Don&#39;t edit the doc. I will make the edits myself.\n</span></span></code></pre></div><p>I also use Anil Dash&rsquo;s <a href=\"https://www.anildash.com/2024/03/10/make-better-documents/\">Better Documents</a> with a similar prompt. This is where I often get stuck! The LLM makes good recommendations based on Anil&rsquo;s skill, but I sometimes don&rsquo;t have an idea at my fingertips. In this case, I ask the LLM for ideas for restructuring the doc.</p>\n<p>I prefer to make these edits myself, even if it means copying some AI-generated text into my final output.</p>\n",
				"date_published": "2026-09-21T00:00:00+00:00",
				"url": "https://enumerator.dev/how-i-use-ai-when-writing/",
				"tags": ["ai","writing"]
			},
			{
				"id": "https://enumerator.dev/self-host-something/",
				"title": "If You Make Software, You Should Self-Host Something",
				"content_html": "<p>If you write software for your job, you probably write software that runs &ldquo;in the cloud&rdquo; somewhere that users access it over the internet.</p>\n<p>Do you know how all of this works?</p>\n<p>Probably not.</p>\n<p>I don&rsquo;t doubt your skills and knowledge as a developer. Software deployments are so complex, even small companies have teams dedicated to managing deployment and uptime.</p>\n<p>Self-hosting something&ndash;anything&ndash;can teach you a lot about how software runs on the web. Knowledge that is essential to being an <a href=\"/excellent-engineers/\">excellent engineer</a>.</p>\n<p>While your company might deploy with Kubernetes triggered by your git workflow, the fundamentals of how software runs on a computer aren&rsquo;t that different between a small self-hosted VPS and a k8s deployment.</p>\n<h2 id=\"what-should-you-self-host\">What Should You Self Host?</h2>\n<p>There are plenty of open source projects you can self-host these days. You can deploy any of these just to try them out and then tear down your server. Here are a few ideas:</p>\n<ul>\n<li>Host your blog with a <a href=\"https://caddyserver.com\">Caddy</a> or NGINX server in front of it.</li>\n<li>Run a <a href=\"https://kan.bn\">kan</a> instance for your own todo list.</li>\n<li>Deploy a WordPress site on a VPS.</li>\n<li>Deploy a <a href=\"https://miniflux.app\">MiniFlux RSS reader</a> (you do use RSS, right?)</li>\n<li>Run your own <a href=\"https://apps.yunohost.org/app/vaultwarden\">password manager with Vaultwarden</a>.</li>\n<li>Run <a href=\"https://trivabble.org\">Trivabble</a>, a Scrabble-like game with no rules.</li>\n</ul>\n<h2 id=\"where-should-you-host\">Where Should You Host?</h2>\n<ol>\n<li>Your own computer.\n<ol>\n<li>Start here. You already have this computer! Edit your local <code>/etc/hosts</code> file to give yourself a fun domain that you can access from your local system. But you write software for the web, right? So you should deploy to a virtual private server.</li>\n</ol>\n</li>\n<li>A Virtual Private Server\n<ol>\n<li>Find a provider of your choosing and deploy a small VPS. Get this connected to the internet and deploy an app. Find a free subdomain provider and connect it to your VPS.</li>\n</ol>\n</li>\n<li>A Computer at Home\n<ol>\n<li>If you have a spare computer you can keep running, set up a web server on it. This will require more tinkering and probably require a reverse proxy to expose it to the public internet.</li>\n</ol>\n</li>\n</ol>\n<h2 id=\"how-to-host\">How To Host?</h2>\n<ol>\n<li>Manual! You&rsquo;ll learn the most this way. Follow the install instructions on the app you chose to get it up and running. Once you can log in, celebrate!</li>\n<li>Automated(ish). <a href=\"https://yunohost.org\">YunoHost</a> is a software for managing your server. Once you&rsquo;ve done it manually, you&rsquo;ll appreciate all the work that&rsquo;s gone int to YunoHost. The docs help you understand how user roles work on the operating system and how applications are protected from reading one another&rsquo;s data.</li>\n</ol>\n<h2 id=\"self-host-for-fun\">Self Host for Fun</h2>\n<p>You really don&rsquo;t need to keep apps up and running. Remember that the point is to get something out here, break it, and learn to fix it. Along the way you&rsquo;ll learn about unix systems, application processes, resource management, backups, downtime and application resiliance.</p>\n<p>Try self-host something and have some fun.</p>\n",
				"date_published": "2026-09-21T00:00:00+00:00",
				"url": "https://enumerator.dev/self-host-something/",
				"tags": ["self-hosting","career-growth"]
			},
			{
				"id": "https://enumerator.dev/what-is-mcp/",
				"title": "What is MCP: A No Bull Explainer",
				"content_html": "<p>Model Context Protocol (MCP) is a way for AI Agents to connect to an external application (usually on the internet) to gather information and perform tasks.</p>\n<p>That&rsquo;s it. No magic.</p>\n<h2 id=\"breaking-down-the-jargon\">Breaking Down the Jargon</h2>\n<p>I have read so many descriptions of AI tools that are riddled with jargon. &ldquo;MCP is a HTTP-stream-based protocol for agentic interfaces.&rdquo; &ldquo;MCP does not use SSE because it now favours streamable HTTP.&rdquo; &ldquo;MCP is a stdio-based server to connect to an LLM.&rdquo;</p>\n<p>Blah blah blah.</p>\n<p>Model context protocol is a standard that uses existing technologies. The standard is new. The technologies are not.</p>\n<p>HTTP is a transport protocol that defines how two applications connect and communicate over the internet.</p>\n<p>MCP is a semantics protocol that defines how LLM-based software can perform actions in another application. MCP connections are often transmitted over HTTP using POST requests.</p>\n<h2 id=\"the-simplest-mcp-connector\">The Simplest MCP Connector</h2>\n<ol>\n<li>An HTTP endpoint that responds to POST with JSON-RPC in the content type, and GET that returns 405 Method Not Allowed. The GET endpoint can be used for an event stream, but we&rsquo;re focusing on simplicity here.</li>\n<li>The POST body is a JSON-RPC request object and the server replies synchronously with a single JSON-RPC response object.</li>\n<li>The client sends <code>id</code>, <code>method: &quot;initialize&quot;</code>, <code>protocolVersion</code>, <code>capabilities</code>, and <code>clientInfo</code>.</li>\n<li>The server responds with <code>protocolVersion</code>, <code>capabilities</code>, <code>serverInfo</code>.</li>\n<li>The client then POSTs <code>notifications/initialized</code>and the server replies <code>202 Accepted</code> with empty body.</li>\n<li>Tada! We have a connection!</li>\n<li>The AI Agent POSTs <code>tools/list</code> to fetch tools or <code>tools/call</code> with an <code>id</code>.</li>\n<li>The server always responds with an <code>id</code> that matches request <code>id</code>.</li>\n<li>The AI Agent uses the <code>id</code> to keep track of which response belongs to which request. We&rsquo;re doing HTTP here, so the ID is superfluous, it is essential if you add an HTTP stream that can respond to multiple calls.</li>\n<li>Errors: JSON-RPC error object (<code>code</code>/<code>message</code>) in place of <code>result</code>, still plain POST/JSON, no special HTTP status needed beyond 200.</li>\n</ol>\n<h3 id=\"new\">NEW!</h3>\n<p>MCP sessions are now stateless. Are you managing <code>Mcp-Session-Id</code>? No need!</p>\n<pre><code class=\"language-mermaid\">sequenceDiagram\n    participant C as AI Agent\n    participant S as Server\n    C-&gt;&gt;S: POST / {method: initialize, id: 1}\n    S--&gt;&gt;C: 200 result: protocolVersion, capabilities, serverInfo, id: 1\n    C-&gt;&gt;S: POST / {method: notifications/initialized}\n    S--&gt;&gt;C: 202 Accepted (empty body)\n    C-&gt;&gt;S: POST / {method: tools/list, id: 2}\n    S--&gt;&gt;C: 200 result: tools, id: 2\n    C-&gt;&gt;S: POST / {method: tools/call, id: 3}\n    S--&gt;&gt;C: 200 result or error, id: 3</code></pre>\n<h2 id=\"misconceptions-about-mcp\">Misconceptions About MCP</h2>\n<h3 id=\"mcp-no-longer-recommends-server-sent-events\">MCP No Longer Recommends Server Sent Events</h3>\n<p>This confusion comes from the semantics of &ldquo;Streamable HTTP&rdquo; vs SSE (Server Sent Events). The MCP spec used to require SSE for remote connections and std IO when the MCP was on the same host as the agent. The original specification in 2024 required an SSE endpoint and a separate <code>/messages</code> endpoint.</p>\n<p>In the current spec, MCP requires one endpoint with an optional upgrade to SSE. The wording is confusing here because the spec references <a href=\"https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http\">Streamable HTTP</a> under &ldquo;Transports&rdquo; and states that this is a replacement for HTTP+SSE. These statements are accurate, but once you read the spec, you realize that the only way to implement Streamable HTTP is by using server-sent events.</p>\n<h3 id=\"mcp-is-just-an-api\">MCP Is Just an API</h3>\n<p>If you want to split hairs and say that since API stands for &ldquo;Application Programming Interface,&rdquo; then, yes, it is an interface of sorts for programming an application.</p>\n<p>But your hair splitting would be out of touch with the way people talk. API these days usually means a set of HTTP endpoints that responds with JSON accessed over REST or GraphQL.</p>\n<p>JSON APIs have been around for a long time now! And many of them don&rsquo;t work well with AI agents. A large API specification is hard for an agent to reason about, which makes it difficult to accomplish tasks with a 1-1 mapping between an API and an MCP tool.</p>\n<p>MCP defines the semantics that give agents the information they need to string together novel workflows and tasks.</p>\n<h3 id=\"mcp-is-a-new-technology\">MCP is a New Technology</h3>\n<p>MCP is only a new standard. It is based on existing technologies, and this is what makes it powerful. The MCP standard lets us quickly develop new user experiences because it uses existing technologies for connecting two applications.</p>\n",
				"date_published": "2026-09-18T00:00:00+00:00",
				"url": "https://enumerator.dev/what-is-mcp/",
				"tags": ["ai","mcp"]
			},
			{
				"id": "https://enumerator.dev/everything-was-done-with-an-ascii-editor/",
				"title": "Everything was done with an ASCII editor",
				"content_html": "<p>The <a href=\"https://gamefaqs.gamespot.com/snes/588741-super-metroid/faqs/10114\">Super Metroid – FAQ/Speed Guide</a> is 17,000 words of perfectly-spaced, full justified, mono-spaced text. It&rsquo;s beautiful to look at.</p>\n<p>From the FAQ</p>\n<blockquote>\n<p>What program did you use to justify the text?</p>\n<p>None. I just chose words carefully so that everything lined up on the right hand side. Everything was done with an ASCII editor.</p>\n</blockquote>\n<p><a href=\"https://unsung.aresluna.org/i-just-chose-words-carefully/\">via Unsung</a> <a href=\"https://news.ycombinator.com/item?id=49503601\">via Hacker News</a></p>\n<p>This reminds me of the hours I spent in a trance tracking, kerning, and editing text to create a triangular brochure that folded out into a tessellation with full-justified text that fit perfectly into each repeated triangle.</p>\n",
				"date_published": "2026-08-31T00:00:00+00:00",
				"url": "https://enumerator.dev/everything-was-done-with-an-ascii-editor/",
				"tags": ["writing","ai"]
			},
			{
				"id": "https://enumerator.dev/llms-dont-write-they-generate/",
				"title": "LLMs Don't Write, They Generate",
				"content_html": "<p><a href=\"https://daringfireball.net/2026/08/anthropics_watermark_text_adulteration_in_claude_is_a_perversion_of_writing\">Everyone</a> is <a href=\"https://medium.com/whither-news/words-matter-damnit-fc883733a729\">mad</a> about AI <a href=\"https://www.404media.co/anthropics-text-watermarking-proves-ai-companies-do-not-care-at-all-about-writing/\">watermarking</a>.</p>\n<p>The thing is.</p>\n<p>These machines don&rsquo;t write.</p>\n<p><a href=\"https://buttondown.com/maiht3k/archive/how-to-talk-about-ai-without-adding-to-the/\">They generate text.</a></p>\n<p>Writers write. Machines generate. LLMs can watermark text without affecting the quality of the output because they are benchmarked on the usefulness of their output rather than the meaning of the text.</p>\n<p>LLMs don&rsquo;t reflect on meaning, consider their audience, and convey thoughts with care.</p>\n<p>LLMs generate a probable output to the input. That&rsquo;s all.</p>\n<p>AI companies don&rsquo;t value writing because they&rsquo;ve never thought about writing. AI companies value the likelihood that the machine produces useful output. Not a <em>meaningful</em> output.</p>\n<p>LLMs have gotten really good at generating probable outputs that are productive.</p>\n<p>But it&rsquo;s still probable. And only mostly productive. But rarely meaningful.</p>\n<p>Saying that an LLM writes is as accurate as saying a lawn mower gardens, an airplane vacations, or a <a href=\"https://www.colincornaby.me/2025/08/in-the-future-all-food-will-be-cooked-in-a-microwave-and-if-you-cant-deal-with-that-then-you-need-to-get-out-of-the-kitchen/\">microwave is a chef</a>.<sup id=\"fnref:1\"><a href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\">1</a></sup></p>\n<div class=\"footnotes\" role=\"doc-endnotes\">\n<hr>\n<ol>\n<li id=\"fn:1\">\n<p>Ha! Oh dear. I wanted to write the word &ldquo;cooks&rdquo; here but I can&rsquo;t. Is there another word for &ldquo;cooks with great culinary skill&rdquo; in English? Perhaps language is changing and LLMs do, in fact, &ldquo;write&rdquo; with the same skill that a microwave cooks.&#160;<a href=\"#fnref:1\" class=\"footnote-backref\" role=\"doc-backlink\">&#x21a9;&#xfe0e;</a></p>\n</li>\n</ol>\n</div>\n",
				"date_published": "2026-08-18T00:00:00+00:00",
				"url": "https://enumerator.dev/llms-dont-write-they-generate/",
				"tags": ["writing","ai"]
			},
			{
				"id": "https://enumerator.dev/tenets-of-storytelling-how-to-write-a-technical-document/",
				"title": "Tenets of Storytelling: How to Write a Technical Proposal",
				"content_html": "<p>Sophie Alpert recently posted her internal policy on the <a href=\"https://sophiebits.com/2026/06/25/there-are-no-lossless-transformations-of-natural-language-text\">acceptable use of AI for writing technical documents</a>, which included this line:</p>\n<blockquote>\n<p><strong>More time should be spent authoring a document than consuming it.</strong></p>\n</blockquote>\n<p>Similarly, Over at <a href=\"https://www.themarginalian.org\">The Marginalian</a> Maria Popova has been posting about reading more and writing better.</p>\n<p>This week, she posted <a href=\"https://www.themarginalian.org/2026/08/12/kurt-vonnegut-on-writing-stories/\">Kurt Vonnegut&rsquo;s 8 Tenets of Storytelling</a>, which includes a similar line:</p>\n<blockquote>\n<p>Use the time of a total stranger in such a way that he or she will not feel the time was wasted.</p>\n</blockquote>\n<p>Vonnegut&rsquo;s tenets can teach us something about technical writing, especially these days when an LLM can generate a <a href=\"/smitten-with-whats-written/\">seemingly polished</a> technical document in seconds.</p>\n<p>I&rsquo;m sure Vonnegut would loath being a technical writer and despise being quoted for technical writing.</p>\n<p>Nonetheless, here are Vonnegut&rsquo;s eight tenets rewritten by yours truly as tenets for writing technical proposals. Unsurprisingly, some of these tenets work, word-for-word.</p>\n<ol>\n<li>Use your colleagues&rsquo; time wisely. They will thank you for it.</li>\n<li>Give your audience at least one thing to be excited about.</li>\n<li>Every component should do something useful, even if only to write a log.</li>\n<li>Every sentence must do one of two things — reveal details or propose action.</li>\n<li>Start as close to the solution as possible.</li>\n<li>Be a Sadist. No matter how much you or your team love a feature, show its faults so you can make good decisions.</li>\n<li>Write to please just one person. If you solve every technical problem, your proposal will get pneumonia.</li>\n<li>Give your readers as much information as possible as soon as possible. To hell with suspense. Readers should have such complete understanding of what is going on, where and why, that they could finish the design themselves, should entropy degrade the last few bytes.</li>\n</ol>\n",
				"date_published": "2026-08-13T00:00:00+00:00",
				"url": "https://enumerator.dev/tenets-of-storytelling-how-to-write-a-technical-document/",
				"tags": ["writing"]
			},
			{
				"id": "https://enumerator.dev/technical-diagrams-for-communication/",
				"title": "Technical Diagrams for Communication",
				"content_html": "<p>How many ways are there to draw the same diagram? Lots! It turns out. The screenshot below shows image search results for &ldquo;google oauth login diagram.&rdquo; Some of these diagrams are clear while others are cluttered and confusing.</p>\n<p><img\n    src=\"/images/google-oauth-login-diagram-search-results_hu_ae422404a4bd4b4e.webp\"\n    srcset=\"/images/google-oauth-login-diagram-search-results_hu_dd113f9c0e6906b4.webp 576w, /images/google-oauth-login-diagram-search-results_hu_4b5e4bd0fecdb0d9.webp 864w, /images/google-oauth-login-diagram-search-results_hu_ae422404a4bd4b4e.webp 1152w\" sizes=\"(max-width: 36rem) 100vw, 36rem\"\n    width=\"1152\"\n    height=\"622\"\n    loading=\"lazy\"\n    decoding=\"async\" alt=\"Image search results for “google oauth login diagram” showing very different diagrams of the same flow\"></p>\n<p>Diagrams are a communication tool. Used well, they focus a conversation, communicate objectives, and enforce guidelines. Used poorly, they can derail conversations and result in frustration.</p>\n<p>The slides below are from a short workshop I gave on how I use diagramming in my technical proposals.</p>\n<div class=\"slides my-10\"><div class=\"carousel flex w-full rounded-box border border-base-300\"><figure id=\"slides-0-1\" class=\"carousel-item w-full flex-col\" aria-label=\"Slide 1 of 18\"><div class=\"slide-frame relative aspect-square w-full shrink-0 sm:aspect-video\">\n                <div class=\"slide-body absolute inset-0 overflow-y-auto p-6 sm:p-10\" tabindex=\"0\">\n                    <h2 id=\"reasons-to-draw\">Reasons to Draw</h2>\n<ul>\n<li>Explore a problem</li>\n<li>Make a decision</li>\n<li>Share a solution</li>\n</ul>\n\n                </div>\n            </div>\n            <div class=\"slide-controls not-prose flex w-full shrink-0 items-center justify-between gap-2 border-t border-base-300 px-4 py-2\">\n                <a href=\"#slides-0-18\" class=\"btn btn-circle btn-sm\" aria-label=\"Previous slide\">❮</a>\n                <span class=\"text-sm text-base-content/50\">1 / 18</span>\n                <a href=\"#slides-0-2\" class=\"btn btn-circle btn-sm\" aria-label=\"Next slide\">❯</a>\n            </div>\n            <figcaption class=\"slide-notes w-full grow border-t border-base-300 px-6 py-4 text-sm text-base-content/70 sm:px-10\">\n                <p>I am going to talk about three different reasons to draw diagrams and why each reason requires a different thought process.</p>\n<p>Diagrams are a powerful communication tool. If they are done wrong, they will send your audience off in the wrong direction and you&rsquo;ll misunderstand each other. If they are done right, you&rsquo;ll have a focused, productive conversation.</p>\n\n            </figcaption>\n        </figure><figure id=\"slides-0-2\" class=\"carousel-item w-full flex-col\" aria-label=\"Slide 2 of 18\"><div class=\"slide-frame relative aspect-square w-full shrink-0 sm:aspect-video\">\n                <div class=\"slide-body absolute inset-0 overflow-y-auto p-6 sm:p-10\" tabindex=\"0\">\n                    <h2 id=\"diagrams-create-a-shared-mental-model-of-a-system\">Diagrams Create a Shared Mental Model of a System</h2>\n\n                </div>\n            </div>\n            <div class=\"slide-controls not-prose flex w-full shrink-0 items-center justify-between gap-2 border-t border-base-300 px-4 py-2\">\n                <a href=\"#slides-0-1\" class=\"btn btn-circle btn-sm\" aria-label=\"Previous slide\">❮</a>\n                <span class=\"text-sm text-base-content/50\">2 / 18</span>\n                <a href=\"#slides-0-3\" class=\"btn btn-circle btn-sm\" aria-label=\"Next slide\">❯</a>\n            </div>\n            <figcaption class=\"slide-notes w-full grow border-t border-base-300 px-6 py-4 text-sm text-base-content/70 sm:px-10\">\n                <p>Put yourself in your audience&rsquo;s position when you draw a diagram.</p>\n<ul>\n<li>What are their goals when they read your diagram?</li>\n<li>What context do they have that will affect how they read it?</li>\n<li>What do you need from them when they see the diagram? Are you telling them how something works? Proposing a solution? Demonstrating a tradeoff?</li>\n</ul>\n\n            </figcaption>\n        </figure><figure id=\"slides-0-3\" class=\"carousel-item w-full flex-col\" aria-label=\"Slide 3 of 18\"><div class=\"slide-frame relative aspect-square w-full shrink-0 sm:aspect-video\">\n                <div class=\"slide-body absolute inset-0 overflow-y-auto p-6 sm:p-10\" tabindex=\"0\">\n                    <h2 id=\"what-makes-a-good-diagram\">What Makes a Good Diagram?</h2>\n<ol>\n<li>Follows a formula</li>\n<li>Minimal colour</li>\n<li>Clear visual hierarchy</li>\n<li>Incorporates time, relationships, &amp; decisions as needed</li>\n</ol>\n\n                </div>\n            </div>\n            <div class=\"slide-controls not-prose flex w-full shrink-0 items-center justify-between gap-2 border-t border-base-300 px-4 py-2\">\n                <a href=\"#slides-0-2\" class=\"btn btn-circle btn-sm\" aria-label=\"Previous slide\">❮</a>\n                <span class=\"text-sm text-base-content/50\">3 / 18</span>\n                <a href=\"#slides-0-4\" class=\"btn btn-circle btn-sm\" aria-label=\"Next slide\">❯</a>\n            </div>\n            <figcaption class=\"slide-notes w-full grow border-t border-base-300 px-6 py-4 text-sm text-base-content/70 sm:px-10\">\n                <ol>\n<li>Following a formula makes it easy for your audience to understand the content. They will spend less time trying to take in the colours, symbols, and lines and instead see the whole picture.</li>\n<li>Minimal colour. Too much colour is distracting. Only use colour to focus the eye on important details.</li>\n<li>Clear visual hierarchy. The reader should clearly see the important parts of the diagram on the first glance. Cut extra details.</li>\n<li>Incorporates time, relationships &amp; decisions. Don&rsquo;t use a single formula for every diagram. If time is an important factor, pick a structure that adds time on an axis. If relationships are important, use a diagram with symbols to indicate this aspect.</li>\n</ol>\n\n            </figcaption>\n        </figure><figure id=\"slides-0-4\" class=\"carousel-item w-full flex-col\" aria-label=\"Slide 4 of 18\"><div class=\"slide-frame relative aspect-square w-full shrink-0 sm:aspect-video\">\n                <div class=\"slide-body absolute inset-0 overflow-y-auto p-6 sm:p-10\" tabindex=\"0\">\n                    <h2 id=\"a-diagram-should-explain-itself\">A Diagram Should Explain Itself</h2>\n\n                </div>\n            </div>\n            <div class=\"slide-controls not-prose flex w-full shrink-0 items-center justify-between gap-2 border-t border-base-300 px-4 py-2\">\n                <a href=\"#slides-0-3\" class=\"btn btn-circle btn-sm\" aria-label=\"Previous slide\">❮</a>\n                <span class=\"text-sm text-base-content/50\">4 / 18</span>\n                <a href=\"#slides-0-5\" class=\"btn btn-circle btn-sm\" aria-label=\"Next slide\">❯</a>\n            </div>\n            <figcaption class=\"slide-notes w-full grow border-t border-base-300 px-6 py-4 text-sm text-base-content/70 sm:px-10\">\n                <p>I have drawn many diagrams and when I step back I wonder, &ldquo;What do these scribbles mean?&rdquo; If you have to write extensive notes to explain a diagram, take another pass at it. Try using a different diagramming syntax or cut out unnecessary elements.</p>\n\n            </figcaption>\n        </figure><figure id=\"slides-0-5\" class=\"carousel-item w-full flex-col\" aria-label=\"Slide 5 of 18\"><div class=\"slide-frame relative aspect-square w-full shrink-0 sm:aspect-video\">\n                <div class=\"slide-body absolute inset-0 overflow-y-auto p-6 sm:p-10\" tabindex=\"0\">\n                    <h2 id=\"diagram-to-explore-a-problem\">Diagram to Explore a Problem</h2>\n<ul>\n<li>Rough drawings</li>\n<li>Good for collaborating live and figuring out ideas</li>\n<li>Only make sense in the context of exploration</li>\n</ul>\n\n                </div>\n            </div>\n            <div class=\"slide-controls not-prose flex w-full shrink-0 items-center justify-between gap-2 border-t border-base-300 px-4 py-2\">\n                <a href=\"#slides-0-4\" class=\"btn btn-circle btn-sm\" aria-label=\"Previous slide\">❮</a>\n                <span class=\"text-sm text-base-content/50\">5 / 18</span>\n                <a href=\"#slides-0-6\" class=\"btn btn-circle btn-sm\" aria-label=\"Next slide\">❯</a>\n            </div>\n            <figcaption class=\"slide-notes w-full grow border-t border-base-300 px-6 py-4 text-sm text-base-content/70 sm:px-10\">\n                <p>Diagrams for exploring problems are the kind that I make when I&rsquo;m pairing with a group to solve a problem or hashing something out on a notepad for myself. These diagrams only make sense in the moment and only to the people present.</p>\n\n            </figcaption>\n        </figure><figure id=\"slides-0-6\" class=\"carousel-item w-full flex-col\" aria-label=\"Slide 6 of 18\"><div class=\"slide-frame relative aspect-square w-full shrink-0 sm:aspect-video\">\n                <div class=\"slide-body absolute inset-0 overflow-y-auto p-6 sm:p-10\" tabindex=\"0\">\n                    <h3 id=\"example-oauth-flow\">Example: OAuth Flow</h3>\n<p><img\n    src=\"/images/hand-drawn-oauth-authentication-flow-sketch_hu_2a2ed2694c2dcd6b.webp\"\n    srcset=\"/images/hand-drawn-oauth-authentication-flow-sketch_hu_c0f3dba26966a82a.webp 576w, /images/hand-drawn-oauth-authentication-flow-sketch_hu_2a2ed2694c2dcd6b.webp 860w\" sizes=\"(max-width: 36rem) 100vw, 36rem\"\n    width=\"860\"\n    height=\"608\"\n    loading=\"lazy\"\n    decoding=\"async\" alt=\"A rough hand-drawn sketch titled “Authentication Flow”, with boxes for User, /login, Backend, OAuth Provider, and OAuth Validate joined by arrows\"></p>\n\n                </div>\n            </div>\n            <div class=\"slide-controls not-prose flex w-full shrink-0 items-center justify-between gap-2 border-t border-base-300 px-4 py-2\">\n                <a href=\"#slides-0-5\" class=\"btn btn-circle btn-sm\" aria-label=\"Previous slide\">❮</a>\n                <span class=\"text-sm text-base-content/50\">6 / 18</span>\n                <a href=\"#slides-0-7\" class=\"btn btn-circle btn-sm\" aria-label=\"Next slide\">❯</a>\n            </div>\n            <figcaption class=\"slide-notes w-full grow border-t border-base-300 px-6 py-4 text-sm text-base-content/70 sm:px-10\">\n                <p>Here&rsquo;s an example of a very rough diagram of an OAuth flow. This is the kind of diagram I&rsquo;d draw to understand the key elements involved. But this diagram is useless for communicating the components and sequences involved.</p>\n\n            </figcaption>\n        </figure><figure id=\"slides-0-7\" class=\"carousel-item w-full flex-col\" aria-label=\"Slide 7 of 18\"><div class=\"slide-frame relative aspect-square w-full shrink-0 sm:aspect-video\">\n                <div class=\"slide-body absolute inset-0 overflow-y-auto p-6 sm:p-10\" tabindex=\"0\">\n                    <h2 id=\"diagrams-for-exploring-are-not-diagrams-for-communication\">Diagrams for Exploring are Not Diagrams for Communication</h2>\n\n                </div>\n            </div>\n            <div class=\"slide-controls not-prose flex w-full shrink-0 items-center justify-between gap-2 border-t border-base-300 px-4 py-2\">\n                <a href=\"#slides-0-6\" class=\"btn btn-circle btn-sm\" aria-label=\"Previous slide\">❮</a>\n                <span class=\"text-sm text-base-content/50\">7 / 18</span>\n                <a href=\"#slides-0-8\" class=\"btn btn-circle btn-sm\" aria-label=\"Next slide\">❯</a>\n            </div>\n            <figcaption class=\"slide-notes w-full grow border-t border-base-300 px-6 py-4 text-sm text-base-content/70 sm:px-10\">\n                <p>Draw lots of diagrams! Exploring problems with diagrams is great. But they are for a small audience. These diagrams are not good for communicating to a broader audience.</p>\n\n            </figcaption>\n        </figure><figure id=\"slides-0-8\" class=\"carousel-item w-full flex-col\" aria-label=\"Slide 8 of 18\"><div class=\"slide-frame relative aspect-square w-full shrink-0 sm:aspect-video\">\n                <div class=\"slide-body absolute inset-0 overflow-y-auto p-6 sm:p-10\" tabindex=\"0\">\n                    <h2 id=\"diagram-to-make-decisions\">Diagram to Make Decisions</h2>\n<ul>\n<li>Communicate options</li>\n<li>Include time component when relevant</li>\n<li>Distinguish current and future state</li>\n</ul>\n\n                </div>\n            </div>\n            <div class=\"slide-controls not-prose flex w-full shrink-0 items-center justify-between gap-2 border-t border-base-300 px-4 py-2\">\n                <a href=\"#slides-0-7\" class=\"btn btn-circle btn-sm\" aria-label=\"Previous slide\">❮</a>\n                <span class=\"text-sm text-base-content/50\">8 / 18</span>\n                <a href=\"#slides-0-9\" class=\"btn btn-circle btn-sm\" aria-label=\"Next slide\">❯</a>\n            </div>\n            <figcaption class=\"slide-notes w-full grow border-t border-base-300 px-6 py-4 text-sm text-base-content/70 sm:px-10\">\n                <p>Drawing to make decisions means you have an external audience. This is where it is important to think about your audience&rsquo;s context and pick the right visual language.</p>\n\n            </figcaption>\n        </figure><figure id=\"slides-0-9\" class=\"carousel-item w-full flex-col\" aria-label=\"Slide 9 of 18\"><div class=\"slide-frame relative aspect-square w-full shrink-0 sm:aspect-video\">\n                <div class=\"slide-body absolute inset-0 overflow-y-auto p-6 sm:p-10\" tabindex=\"0\">\n                    <h3 id=\"oauth-flow-as-flow-chart\">OAuth Flow as Flow Chart</h3>\n<pre><code class=\"language-mermaid\">flowchart LR\nUser --&gt;|Enter credentials| Browser\nBrowser --&gt;|POST /login| App[App Backend]\nApp --&gt;|Verify password| DB[(Database)]\nApp --&gt;|Set session cookie| Browser\nBrowser --&gt; User</code></pre>\n\n                </div>\n            </div>\n            <div class=\"slide-controls not-prose flex w-full shrink-0 items-center justify-between gap-2 border-t border-base-300 px-4 py-2\">\n                <a href=\"#slides-0-8\" class=\"btn btn-circle btn-sm\" aria-label=\"Previous slide\">❮</a>\n                <span class=\"text-sm text-base-content/50\">9 / 18</span>\n                <a href=\"#slides-0-10\" class=\"btn btn-circle btn-sm\" aria-label=\"Next slide\">❯</a>\n            </div>\n            <figcaption class=\"slide-notes w-full grow border-t border-base-300 px-6 py-4 text-sm text-base-content/70 sm:px-10\">\n                <p>The flow chart in this example works for a simple flow but makes the details of OAuth very hard for an audience to understand.</p>\n\n            </figcaption>\n        </figure><figure id=\"slides-0-10\" class=\"carousel-item w-full flex-col\" aria-label=\"Slide 10 of 18\"><div class=\"slide-frame relative aspect-square w-full shrink-0 sm:aspect-video\">\n                <div class=\"slide-body absolute inset-0 overflow-y-auto p-6 sm:p-10\" tabindex=\"0\">\n                    <h3 id=\"auth-flow-as-sequence-diagram\">Auth Flow as Sequence Diagram</h3>\n<pre><code class=\"language-mermaid\">sequenceDiagram\nactor User\nparticipant Browser\nparticipant App as App (Backend)\nparticipant DB as Database\nUser-&gt;&gt;Browser: Enter email &#43; password\nBrowser-&gt;&gt;App: POST\nApp-&gt;&gt;DB: Verify credentials\nDB--&gt;&gt;App: Valid\nApp--&gt;&gt;Browser: Set session cookie\nBrowser--&gt;&gt;User: Logged in</code></pre>\n\n                </div>\n            </div>\n            <div class=\"slide-controls not-prose flex w-full shrink-0 items-center justify-between gap-2 border-t border-base-300 px-4 py-2\">\n                <a href=\"#slides-0-9\" class=\"btn btn-circle btn-sm\" aria-label=\"Previous slide\">❮</a>\n                <span class=\"text-sm text-base-content/50\">10 / 18</span>\n                <a href=\"#slides-0-11\" class=\"btn btn-circle btn-sm\" aria-label=\"Next slide\">❯</a>\n            </div>\n            <figcaption class=\"slide-notes w-full grow border-t border-base-300 px-6 py-4 text-sm text-base-content/70 sm:px-10\">\n                <p>Here&rsquo;s the basic auth flow as a sequence diagram. There is a clear syntax. You don&rsquo;t even have to read &ldquo;user&rdquo; to know that the stick person is a human, and the x axis clearly distinguishes components while the y axis delineates time.</p>\n\n            </figcaption>\n        </figure><figure id=\"slides-0-11\" class=\"carousel-item w-full flex-col\" aria-label=\"Slide 11 of 18\"><div class=\"slide-frame relative aspect-square w-full shrink-0 sm:aspect-video\">\n                <div class=\"slide-body absolute inset-0 overflow-y-auto p-6 sm:p-10\" tabindex=\"0\">\n                    <h3 id=\"oauth-flow-as-sequence-diagram\">OAuth Flow as Sequence Diagram</h3>\n<pre><code class=\"language-mermaid\">sequenceDiagram\nactor User\nparticipant Browser\nparticipant App as App (Backend)\nparticipant Google\n\nUser-&gt;&gt;Browser: Click &#34;Sign in with Google&#34;\nBrowser-&gt;&gt;Google: Redirect to authorize\nGoogle-&gt;&gt;User: Show consent screen\nUser-&gt;&gt;Google: Authenticate &#43; grant access\nGoogle--&gt;&gt;Browser: Redirect with auth code\nBrowser-&gt;&gt;App: Send auth code\nApp-&gt;&gt;Google: Exchange code for tokens\nGoogle--&gt;&gt;App: Return tokens\nApp--&gt;&gt;Browser: Set session cookie\nBrowser--&gt;&gt;User: Logged in</code></pre>\n\n                </div>\n            </div>\n            <div class=\"slide-controls not-prose flex w-full shrink-0 items-center justify-between gap-2 border-t border-base-300 px-4 py-2\">\n                <a href=\"#slides-0-10\" class=\"btn btn-circle btn-sm\" aria-label=\"Previous slide\">❮</a>\n                <span class=\"text-sm text-base-content/50\">11 / 18</span>\n                <a href=\"#slides-0-12\" class=\"btn btn-circle btn-sm\" aria-label=\"Next slide\">❯</a>\n            </div>\n            <figcaption class=\"slide-notes w-full grow border-t border-base-300 px-6 py-4 text-sm text-base-content/70 sm:px-10\">\n                <p>And here&rsquo;s the OAuth flow. You can now look at the two side by side and clearly see the difference. If we were building a product and wanted to decide between Google OAuth or basic auth, these two diagrams could help us understand the tradeoffs.</p>\n\n            </figcaption>\n        </figure><figure id=\"slides-0-12\" class=\"carousel-item w-full flex-col\" aria-label=\"Slide 12 of 18\"><div class=\"slide-frame relative aspect-square w-full shrink-0 sm:aspect-video\">\n                <div class=\"slide-body absolute inset-0 overflow-y-auto p-6 sm:p-10\" tabindex=\"0\">\n                    <h2 id=\"an-artifact-to-build-from\">An Artifact to Build From</h2>\n<ul>\n<li>Document a desired state</li>\n<li>Can be used in Jira tickets</li>\n<li>Can change if you discover gotchas along the way</li>\n</ul>\n\n                </div>\n            </div>\n            <div class=\"slide-controls not-prose flex w-full shrink-0 items-center justify-between gap-2 border-t border-base-300 px-4 py-2\">\n                <a href=\"#slides-0-11\" class=\"btn btn-circle btn-sm\" aria-label=\"Previous slide\">❮</a>\n                <span class=\"text-sm text-base-content/50\">12 / 18</span>\n                <a href=\"#slides-0-13\" class=\"btn btn-circle btn-sm\" aria-label=\"Next slide\">❯</a>\n            </div>\n            <figcaption class=\"slide-notes w-full grow border-t border-base-300 px-6 py-4 text-sm text-base-content/70 sm:px-10\">\n                <p>Diagrams as artifacts are like blueprints for projects. A good diagram can be referenced over the course of a project and shared with the team. Nothing is certain in software, though! It&rsquo;s good practice to update artifacts to keep a shared mental model of the system.</p>\n\n            </figcaption>\n        </figure><figure id=\"slides-0-13\" class=\"carousel-item w-full flex-col\" aria-label=\"Slide 13 of 18\"><div class=\"slide-frame relative aspect-square w-full shrink-0 sm:aspect-video\">\n                <div class=\"slide-body absolute inset-0 overflow-y-auto p-6 sm:p-10\" tabindex=\"0\">\n                    <h3 id=\"oauth-artifact\">OAuth Artifact</h3>\n<pre><code class=\"language-mermaid\">sequenceDiagram\n    participant User\n    participant Browser\n    participant App as App (Backend)\n    participant Google\n\n    User-&gt;&gt;Browser: Click &#34;Sign in with Google&#34;\n    Browser-&gt;&gt;App: GET /auth/google\n    App--&gt;&gt;Browser: 302 Redirect to Google authorize URL&lt;br/&gt;(client_id, redirect_uri, scope, state, response_type=code)\n    Browser-&gt;&gt;Google: GET /o/oauth2/v2/auth\n    Google--&gt;&gt;Browser: Show account picker &#43; consent screen\n    User-&gt;&gt;Google: Authenticate &#43; grant consent\n    Google--&gt;&gt;Browser: 302 Redirect to redirect_uri&lt;br/&gt;(authorization code, state)\n    Browser-&gt;&gt;App: GET /auth/google/callback?code=...&amp;state=...\n    Note over App: Validate state (CSRF check)\n    App-&gt;&gt;Google: POST /token&lt;br/&gt;(code, client_id, client_secret, redirect_uri, grant_type=authorization_code)\n    Google--&gt;&gt;App: access_token, id_token (JWT), refresh_token\n    Note over App: Verify id_token signature &#43; claims&lt;br/&gt;(read sub, email, name)\n    Note over App: Find or create user, establish session\n    App--&gt;&gt;Browser: Set session cookie, redirect to app\n    Browser--&gt;&gt;User: Logged in</code></pre>\n\n                </div>\n            </div>\n            <div class=\"slide-controls not-prose flex w-full shrink-0 items-center justify-between gap-2 border-t border-base-300 px-4 py-2\">\n                <a href=\"#slides-0-12\" class=\"btn btn-circle btn-sm\" aria-label=\"Previous slide\">❮</a>\n                <span class=\"text-sm text-base-content/50\">13 / 18</span>\n                <a href=\"#slides-0-14\" class=\"btn btn-circle btn-sm\" aria-label=\"Next slide\">❯</a>\n            </div>\n            <figcaption class=\"slide-notes w-full grow border-t border-base-300 px-6 py-4 text-sm text-base-content/70 sm:px-10\">\n                <p>As an artifact, the OAuth flow has more detail and notes on each interaction. You could reference this diagram if you needed to sort out the requirements to implement a part of this flow.</p>\n\n            </figcaption>\n        </figure><figure id=\"slides-0-14\" class=\"carousel-item w-full flex-col\" aria-label=\"Slide 14 of 18\"><div class=\"slide-frame relative aspect-square w-full shrink-0 sm:aspect-video\">\n                <div class=\"slide-body absolute inset-0 overflow-y-auto p-6 sm:p-10\" tabindex=\"0\">\n                    <h2 id=\"four-types-of-diagrams\">Four Types of Diagrams</h2>\n\n                </div>\n            </div>\n            <div class=\"slide-controls not-prose flex w-full shrink-0 items-center justify-between gap-2 border-t border-base-300 px-4 py-2\">\n                <a href=\"#slides-0-13\" class=\"btn btn-circle btn-sm\" aria-label=\"Previous slide\">❮</a>\n                <span class=\"text-sm text-base-content/50\">14 / 18</span>\n                <a href=\"#slides-0-15\" class=\"btn btn-circle btn-sm\" aria-label=\"Next slide\">❯</a>\n            </div>\n            <figcaption class=\"slide-notes w-full grow border-t border-base-300 px-6 py-4 text-sm text-base-content/70 sm:px-10\">\n                <p>I used sequence diagrams today as an example. There are many other diagramming syntaxes. Here are four more that I find useful.</p>\n\n            </figcaption>\n        </figure><figure id=\"slides-0-15\" class=\"carousel-item w-full flex-col\" aria-label=\"Slide 15 of 18\"><div class=\"slide-frame relative aspect-square w-full shrink-0 sm:aspect-video\">\n                <div class=\"slide-body absolute inset-0 overflow-y-auto p-6 sm:p-10\" tabindex=\"0\">\n                    <h2 id=\"flow\">Flow</h2>\n<p>Use when a sequence of events can have alternative branches.</p>\n<pre><code class=\"language-mermaid\">flowchart LR\nStart([Start]) --&gt; Decision{Condition met?}\nDecision --&gt;|Yes| Path1[Do this]\nDecision --&gt;|No| Path2[Do that]\nPath1 --&gt; Stop([Stop])\nPath2 --&gt; Stop</code></pre>\n\n                </div>\n            </div>\n            <div class=\"slide-controls not-prose flex w-full shrink-0 items-center justify-between gap-2 border-t border-base-300 px-4 py-2\">\n                <a href=\"#slides-0-14\" class=\"btn btn-circle btn-sm\" aria-label=\"Previous slide\">❮</a>\n                <span class=\"text-sm text-base-content/50\">15 / 18</span>\n                <a href=\"#slides-0-16\" class=\"btn btn-circle btn-sm\" aria-label=\"Next slide\">❯</a>\n            </div>\n            <figcaption class=\"slide-notes w-full grow border-t border-base-300 px-6 py-4 text-sm text-base-content/70 sm:px-10\">\n                <p>Don&rsquo;t go overboard on syntax. Stick to simple diagram syntax that your audience will understand.</p>\n\n            </figcaption>\n        </figure><figure id=\"slides-0-16\" class=\"carousel-item w-full flex-col\" aria-label=\"Slide 16 of 18\"><div class=\"slide-frame relative aspect-square w-full shrink-0 sm:aspect-video\">\n                <div class=\"slide-body absolute inset-0 overflow-y-auto p-6 sm:p-10\" tabindex=\"0\">\n                    <h2 id=\"entity-relationship-diagram\">Entity Relationship Diagram</h2>\n<p>Use when entities (often database tables) have relationships to one another.</p>\n<pre><code class=\"language-mermaid\">erDiagram\nUSER {\nint id\nstring email\nstring password_hash\n}\nSESSION {\nint id\nint user_id\nstring session_token\ndatetime expires_at\n}\nOAUTH_ACCOUNT {\nint id\nint user_id\nstring provider\nstring provider_user_id\n}\nUSER ||--o{ SESSION : has\nUSER ||--o{ OAUTH_ACCOUNT : has</code></pre>\n\n                </div>\n            </div>\n            <div class=\"slide-controls not-prose flex w-full shrink-0 items-center justify-between gap-2 border-t border-base-300 px-4 py-2\">\n                <a href=\"#slides-0-15\" class=\"btn btn-circle btn-sm\" aria-label=\"Previous slide\">❮</a>\n                <span class=\"text-sm text-base-content/50\">16 / 18</span>\n                <a href=\"#slides-0-17\" class=\"btn btn-circle btn-sm\" aria-label=\"Next slide\">❯</a>\n            </div>\n        </figure><figure id=\"slides-0-17\" class=\"carousel-item w-full flex-col\" aria-label=\"Slide 17 of 18\"><div class=\"slide-frame relative aspect-square w-full shrink-0 sm:aspect-video\">\n                <div class=\"slide-body absolute inset-0 overflow-y-auto p-6 sm:p-10\" tabindex=\"0\">\n                    <h2 id=\"network-architecture\">Network Architecture</h2>\n<p>Use when describing how software systems interact over a network.</p>\n<pre><code class=\"language-mermaid\">architecture-beta\n\tgroup api(cloud)[API]\n\n\tservice db(database)[Database] in api\n\tservice disk1(disk)[Storage] in api\n\tservice disk2(disk)[Storage] in api\n\tservice server(server)[Server] in api\n\tdb:L -- R:server\n\tdisk1:T -- B:server\n\tdisk2:T -- B:db</code></pre>\n\n                </div>\n            </div>\n            <div class=\"slide-controls not-prose flex w-full shrink-0 items-center justify-between gap-2 border-t border-base-300 px-4 py-2\">\n                <a href=\"#slides-0-16\" class=\"btn btn-circle btn-sm\" aria-label=\"Previous slide\">❮</a>\n                <span class=\"text-sm text-base-content/50\">17 / 18</span>\n                <a href=\"#slides-0-18\" class=\"btn btn-circle btn-sm\" aria-label=\"Next slide\">❯</a>\n            </div>\n        </figure><figure id=\"slides-0-18\" class=\"carousel-item w-full flex-col\" aria-label=\"Slide 18 of 18\"><div class=\"slide-frame relative aspect-square w-full shrink-0 sm:aspect-video\">\n                <div class=\"slide-body absolute inset-0 overflow-y-auto p-6 sm:p-10\" tabindex=\"0\">\n                    <h2 id=\"resources\">Resources</h2>\n<ul>\n<li><a href=\"https://www.anildash.com/2024/03/10/make-better-documents/\">Make Better Documents</a></li>\n<li><a href=\"https://sportebois.medium.com/better-architecture-diagrams-for-agile-teams-actionable-tips-and-lessons-e76627dc4315\">Better Architecture Diagrams for Agile Teams</a></li>\n<li><a href=\"https://terrastruct.com/blog/post/draw-software-architecture-diagrams/\">How to draw beautiful software architecture diagrams</a></li>\n<li><a href=\"https://mermaid.ai/open-source/intro/\">Mermaid Diagrams</a></li>\n</ul>\n\n                </div>\n            </div>\n            <div class=\"slide-controls not-prose flex w-full shrink-0 items-center justify-between gap-2 border-t border-base-300 px-4 py-2\">\n                <a href=\"#slides-0-17\" class=\"btn btn-circle btn-sm\" aria-label=\"Previous slide\">❮</a>\n                <span class=\"text-sm text-base-content/50\">18 / 18</span>\n                <a href=\"#slides-0-1\" class=\"btn btn-circle btn-sm\" aria-label=\"Next slide\">❯</a>\n            </div>\n        </figure>\n    </div>\n</div>\n\n",
				"date_published": "2026-08-07T00:00:00+00:00",
				"url": "https://enumerator.dev/technical-diagrams-for-communication/",
				"tags": ["tools"]
			},
			{
				"id": "https://enumerator.dev/smitten-with-whats-written/",
				"title": "Smitten with What's Written",
				"content_html": "<p>As much as I use AI at work every day I find its writing harder and harder to understand. The code? It usually makes enough sense to me and I can follow its logic. But my brain goes foggy when I read the LLM&rsquo;s reply to me.</p>\n<p>The same thing happens to me when I read a blog post or article with the hallmarks of AI. It always starts with me wondering if I&rsquo;m misunderstanding something in the article and then it clicks, &ldquo;Oh, this is AI content.&rdquo;</p>\n<p>AI writing is a disaster. It falls apart so quickly. While LLMs can produce grammatically correct text, their writing does not stay on topic and they frequently invent phrases that sound correct but have have no real meaning.</p>\n<p>In spite of this, there is <em>so much</em> AI writing out there now. It is exhausting.</p>\n<p>On <a href=\"https://overcast.fm/+AAjSw7MbTGI\">The Curiosity Shop</a> Brené Brown proposes that the grammatical correctness of AI writing is so appealing that people overlook the weak content it produces (emphasis my own):</p>\n<blockquote>\n<p>I think with the advent of AI, people who struggle with written communication, persuasive written communication, the problem is they&rsquo;re <em>smitten with what&rsquo;s written</em>.</p>\n<p>They are so smitten with the idea that they can hand off a work deliverable or anything that&rsquo;s well written…because that&rsquo;s new for them. <em>Like, all of a sudden, this is this beautifully crafted thing that I can turn into someone when for 40 or 50 years, I have not been able to make something basically perfect.</em></p>\n</blockquote>\n<p>In a work context, Brown&rsquo;s idea resonates with me. Writing is difficult. English grammar is hard enough! Never mind connecting ideas and creating a coherent argument in text. People publish AI content because it has the veneer of good grammar. Who needs <a href=\"https://en.wikipedia.org/wiki/The_Elements_of_Style\">Strunk &amp; White</a> when you have an LLM!</p>\n<p>I have long detested prescriptive rules for writing. I endured lectures on commas in English 101 and still don&rsquo;t know how to use a comma. I see rules and freeze. How can I write if there are all these rules to follow?!</p>\n<p>Ironically, Strunk &amp; White is too relevant today:</p>\n<blockquote>\n<h3 id=\"omit-needless-words\">Omit needless words.</h3>\n<p>Vigorous writing is concise. A sentence should contain no unnecessary words, a paragraph no unnecessary sentences, for the same reason that a drawing should have no unnecessary lines and a machine no unnecessary parts. This requires not that the writer make all his sentences short, or that he avoid all detail and treat his subjects only in outline, but that he make every word tell.</p>\n</blockquote>\n<p><a href=\"https://www.gutenberg.org/files/37134/37134-h/37134-h.htm#Rule_13\">The Elements of Style</a></p>\n<h2 id=\"i-want-to-read-human-words\">I Want to Read Human Words</h2>\n<p>It is almost cliché to say this today, but, I want to read human-written words. I really don&rsquo;t care how polished human writing is. <em>Especially</em> in a work context. I want <em>human</em> thought process, rigour, taste, and decisions. I don&rsquo;t want writing that&rsquo;s been passed through an LLM and I absolutely don&rsquo;t want an LLM to generate writing.</p>\n",
				"date_published": "2026-08-03T00:00:00+00:00",
				"url": "https://enumerator.dev/smitten-with-whats-written/",
				"tags": ["ai"]
			},
			{
				"id": "https://enumerator.dev/plants/",
				"title": "Plants",
				"content_html": "<p>I have been looking for ways to improve my home office&rsquo;s air quality by increasing ventilation. Naturally, I wondered if I could add a few plants.</p>\n<blockquote>\n<p>Humans make carbon dioxide. Carbon dioxide is bad for cognition. But plants turn carbon dioxide back into oxygen. And plants are the one true home decoration strategy. So maybe if you get a lot of plants, you can you can keep carbon dioxide in check and keep your brain working?</p>\n</blockquote>\n<p><a href=\"https://dynomight.net/plants/\">So you want to use plants to reduce indoor CO₂</a></p>\n<p>I&rsquo;m so glad someone did the math for me!</p>\n<blockquote>\n<p>Realistically, we’re talking about something like 5,000-10,000 watts [of light], most of which is lost to the room as heat. Imagine five space heaters blasting you on high all the time.</p>\n</blockquote>\n<blockquote>\n<p>(1 kg carbon dioxide)<br>\n× (0.273 kg elemental carbon / kg carbon dioxide)<br>\n× (2 kg dry plant / kg elemental carbon)<br>\n× (8.5 kg actual plant / kg dry plant)<br>\n= 4.6 kg actual plant.</p>\n<p>Your garden must grow that much, every day. That’s 140 kg per month. You must prune and discard all that outside, or your garden is not actually sequestering anything</p>\n</blockquote>\n",
				"date_published": "2026-07-31T00:00:00+00:00",
				"url": "https://enumerator.dev/plants/",
				"tags": "misc"
			},
			{
				"id": "https://enumerator.dev/safe-claude-code-settings/",
				"title": "Safe Claude Code Settings",
				"content_html": "<p>Here are a few commands I have in my Claude Code deny-list to prevent bad things from happening. I wish Claude shipped with these by default!</p>\n<p>The following commands disallow Claude from force pushing and skipping pre-commit hooks.</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sh\" data-lang=\"sh\"><span class=\"line\"><span class=\"cl\"><span class=\"o\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\">  <span class=\"s2\">&#34;permissions&#34;</span>: <span class=\"o\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"s2\">&#34;deny&#34;</span>: <span class=\"o\">[</span>\n</span></span><span class=\"line\"><span class=\"cl\">      <span class=\"s2\">&#34;Bash(git push -f)&#34;</span>,\n</span></span><span class=\"line\"><span class=\"cl\">      <span class=\"s2\">&#34;Bash(git push --force)&#34;</span>,\n</span></span><span class=\"line\"><span class=\"cl\">      <span class=\"s2\">&#34;Bash(git commit:*-n:*)&#34;</span>,\n</span></span><span class=\"line\"><span class=\"cl\">      <span class=\"s2\">&#34;Bash(git commit:*--no-verify:*)&#34;</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"o\">]</span>\n</span></span><span class=\"line\"><span class=\"cl\">  <span class=\"o\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"o\">}</span>\n</span></span></code></pre></div><p>To be extra careful, I also have a <a href=\"https://github.com/cassiascheffer/dotfiles/blob/main/.gitignore\">pretty thorough gitignore</a> that I install globally which includes common patterns for secret credentials.</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-fallback\" data-lang=\"fallback\"><span class=\"line\"><span class=\"cl\">*.key\n</span></span><span class=\"line\"><span class=\"cl\">*.pem\n</span></span><span class=\"line\"><span class=\"cl\">*.p12\n</span></span><span class=\"line\"><span class=\"cl\">*.pfx\n</span></span><span class=\"line\"><span class=\"cl\">*.cer\n</span></span><span class=\"line\"><span class=\"cl\">*.crt\n</span></span><span class=\"line\"><span class=\"cl\">**/secrets/**\n</span></span><span class=\"line\"><span class=\"cl\">**/credentials/**\n</span></span><span class=\"line\"><span class=\"cl\">.env\n</span></span><span class=\"line\"><span class=\"cl\">.env.*\n</span></span><span class=\"line\"><span class=\"cl\">**/config/secrets.toml\n</span></span></code></pre></div>",
				"date_published": "2025-11-26T03:08:00+00:00",
				"url": "https://enumerator.dev/safe-claude-code-settings/",
				"tags": ["claude","ai"]
			},
			{
				"id": "https://enumerator.dev/planning-building-reviewing-why-the-jj-workflow-works-better-than-git/",
				"title": "Planning, Building, Reviewing: Why The JJ Workflow Works Better Than Git",
				"content_html": "<p><img\n    src=\"/images/planning-building-reviewing-why-the-jj-workflow-works-better-than-git_hu_5333e9ea5f796d95.webp\"\n    srcset=\"/images/planning-building-reviewing-why-the-jj-workflow-works-better-than-git_hu_6c3ed591a1a22c88.webp 576w, /images/planning-building-reviewing-why-the-jj-workflow-works-better-than-git_hu_c2695f8909b39288.webp 864w, /images/planning-building-reviewing-why-the-jj-workflow-works-better-than-git_hu_5333e9ea5f796d95.webp 1152w\" sizes=\"(max-width: 36rem) 100vw, 36rem\"\n    width=\"1152\"\n    height=\"576\"\n    loading=\"lazy\"\n    decoding=\"async\" alt=\"Photo by Lucas Kepner on Unsplash\"></p>\n<p>After a week of experimenting with using <a href=\"/use-git-to-plan-and-build-with-claude\">git</a> and <a href=\"/use-jujutsu-to-plan-and-build-with-claude\">jj</a> to plan and build, I&rsquo;ve landed on <code>jj</code> as the winner.</p>\n<h2 id=\"rebasing-kills-the-git-workflow\">Rebasing Kills the Git Workflow</h2>\n<p>Automatic rebases are the winning feature here. When I tell Claude to update a change, it will automatically rebase all its descendants. This is a MASSIVE time saver for both me and Claude.</p>\n<p>When I use empty commits in git, Claude performs the same until it has to start editing code. Then things get messy.</p>\n<p>Rebasing in <code>git</code> is almost always painful. Claude can clunk through the rebase workflow faster than I can, but it still gets lost at times.</p>\n<h2 id=\"my-current-workflow\">My Current Workflow</h2>\n<p><a href=\"https://github.com/cassiascheffer/dotfiles/blob/700f901c3cfedc2ae05866c33cc0a99aabed237b/claude/CLAUDE.md\">Here is the CLAUDE.md that&rsquo;s been working well for me lately</a>. It teaches Claude how to use the <code>jj</code> workflow.</p>\n<h3 id=\"1-start-plan-mode\">1. Start plan mode</h3>\n<p>I start a new session in plan mode and give Claude Code all the information I can think of. I describe what I need, reference files, and explain the outcome.</p>\n<h3 id=\"2-iterate-on-the-plan\">2. Iterate on the Plan</h3>\n<p>I almost always refine the plan with Claude. I verify the code samples it provides and verify that the implementation meets my expectations.</p>\n<h3 id=\"3-turn-the-plan-into-commits\">3. Turn the plan into commits</h3>\n<p>I&rsquo;m still in plan mode, and I ask Claude to break the plan down into empty <code>jj</code> changes.</p>\n<h3 id=\"4-review-the-jj-plan\">4. Review the <code>jj</code> Plan</h3>\n<p>This is key. I am still in plan mode here, and I need to ensure the steps Claude has planned make sense and have no gotchas. Iterate on this step.</p>\n<h3 id=\"5-auto-accept-edits\">5. <strong>Auto-accept edits</strong></h3>\n<p>Finally, I let Claude rip. Once the plan looks good, I auto-accept edits and allow Claude to use <code>jj</code> commands as needed.</p>\n<h3 id=\"6-do-something-else\">6. Do Something Else</h3>\n<p>While I refined this workflow, I watched Claude work to keep it on track and assess whether we had come up with a good enough plan. But I&rsquo;ve found that with a focused plan, Claude can work away just fine.</p>\n<h3 id=\"7-code-review-with-claude\">7. Code review with Claude</h3>\n<p>I use a <a href=\"https://github.com/cassiascheffer/dotfiles/blob/7478e2b382376be7972840e3043882073adfad33/claude/commands/code-review.md\">code-review command</a> to kick off code review with Claude. We walk through one change at a time. I sometimes make manual edits, other times I get Claude to refactor things.</p>\n<h3 id=\"8-use-gh-to-create-prs\">8. Use <code>gh</code> to create PRs</h3>\n<p>Depending on the scope of the change, I&rsquo;ll make a PR stack and ask Claude to manage the stack for me, or I&rsquo;ll create a single PR with a linear history.</p>\n<h3 id=\"9-code-review-again\">9. Code Review Again</h3>\n<p>Yes, I do code review again. The code is mine, after all. I go through the GitHub UI and review the code myself. If I want Claude to make a change, I ask it to edit the <code>jj</code> change set where the code was introduced.</p>\n<p>The result is a clean commit history that implements a feature one change set at a time.</p>\n<h2 id=\"why-jj-works-so-well\">Why JJ Works So Well</h2>\n<p>In steps 7 and 9 above, I review code and often want to make changes to specific change sets. In <code>git</code>, this would involve <code>git rebase -i</code>, picking the commits to edit, making the edits, then <code>git rebase --continue</code>. In <code>jj</code>, all that is done for you.</p>\n<h2 id=\"two-takeaways\">Two Takeaways</h2>\n<p>Agent or not. These two things are true in software development. <strong>Planning and reviewing are the most critical steps.</strong></p>\n<h3 id=\"plan-plan-plan-again\">Plan, Plan, Plan Again</h3>\n<p>Working with an AI agent emphasizes the importance of planning. Planning your work has always been essential for successful feature development. Now with agents, we have really fast researchers who can help us refine a plan.</p>\n<p>The more time you put into planning, the more successful your work will be, regardless of whether you&rsquo;re using an agent.</p>\n<h3 id=\"review-review-review-again\">Review, Review, Review Again</h3>\n<p>Like planning, reviewing your work ensures it is accurate, bug-free, and performant. This is YOUR work after all. Even if an agent wrote it, you own the code.</p>\n",
				"date_published": "2025-11-08T14:04:00+00:00",
				"url": "https://enumerator.dev/planning-building-reviewing-why-the-jj-workflow-works-better-than-git/",
				"tags": ["jj","claude"]
			},
			{
				"id": "https://enumerator.dev/use-git-to-plan-and-build-with-claude/",
				"title": "Use Git to Plan and Build With Claude",
				"content_html": "<p><img\n    src=\"/images/use-git-to-plan-and-build-with-claude_hu_fdd681fba2212ff.webp\"\n    srcset=\"/images/use-git-to-plan-and-build-with-claude_hu_55a14073b6302be7.webp 576w, /images/use-git-to-plan-and-build-with-claude_hu_d0dc0b4b4ba97c98.webp 864w, /images/use-git-to-plan-and-build-with-claude_hu_fdd681fba2212ff.webp 1152w\" sizes=\"(max-width: 36rem) 100vw, 36rem\"\n    width=\"1152\"\n    height=\"576\"\n    loading=\"lazy\"\n    decoding=\"async\" alt=\"Git-Icon-1788C.png\"></p>\n<p>When I shared <a href=\"/use-jujutsu-to-plan-and-build-with-claude\">my new JJ and Claude workflow last week</a>, I wondered whether JJ was too much friction for developers. It requires learning a new version-control tool in addition to learning to use Claude. I had an idea in the back of my mind to use plain git for this.</p>\n<p>And then Claude did it for me!</p>\n<p>Towards the end of the week, I was working in a codebase where I hadn&rsquo;t initialized JJ yet. Claude tried to use the JJ workflow, saw that JJ wasn&rsquo;t initialized, and started implementing the same workflow using plain old git. We were working on a relatively simple feature, and Claude created planning commits and implemented it.</p>\n<h2 id=\"you-dont-need-to-learn-a-new-tool\">You Don&rsquo;t Need to Learn a New Tool</h2>\n<p>JJ is a good workflow if you need a graph of related features. I find this happens most often when I&rsquo;m building a new feature or working in an old codebase where I find bugs.</p>\n<p>Most day-to-day work can be done in linear branches. Plus, everyone knows git already.</p>\n<p>This morning, I worked with Claude to <a href=\"https://github.com/cassiascheffer/dotfiles/blob/3d068ff7413f1b114b035f04f4a194d90cf86656/claude/CLAUDE.md\">refine a CLAUDE.md that uses plain old git to plan and build features</a>. This looks really promising so far.</p>\n",
				"date_published": "2025-11-01T12:57:00+00:00",
				"url": "https://enumerator.dev/use-git-to-plan-and-build-with-claude/",
				"tags": ["claude"]
			},
			{
				"id": "https://enumerator.dev/why-is-claude-code-different-from-cursor-if-they-both-use-claude/",
				"title": "Why is Claude Code Different from Cursor if they Both Use Claude?",
				"content_html": "<p>I get this question often. Here&rsquo;s how the story goes:</p>\n<ul>\n<li>Someone complains that AI is bad because it just does a bunch of stuff for you really quickly and does it wrong.</li>\n<li>I ask what tools they&rsquo;re using, and they say they&rsquo;re using Cursor because it&rsquo;s the most familiar IDE for them.</li>\n<li>I ask them if they start a new chat for each feature, and the answer is usually &ldquo;no&rdquo; because this isn&rsquo;t intuitive.</li>\n<li>I suggest they try a different agent — Claude Code is my preferred tool — and they say, &ldquo;It&rsquo;s all Claude, though, how would that be different?&rdquo;</li>\n</ul>\n<p>Few developers have had time to learn these tools. They&rsquo;ve been forced to use them as productivity enhancers, without the time to learn.</p>\n<p>I have seen performance gains in my work. I shipped the first version of <a href=\"https://upliftapp.online\">Uplift</a> in a few hours! But I&rsquo;ve also taken an enormous amount of time learning, training others, and trying terrible AI tools.</p>\n<p>I work with many developers who use Cursor daily. This is good! Cursor seems to work better with their workflow. While I have a distaste for Cursor, others find it useful. Agents, like IDEs, are a preference, after all.</p>\n<p>Going faster means slowing down to learn your tools. Changing IDEs or using a CLI is a BIG workflow change for most people, and if Cursor is your first brush with AI, I do not blame you for thinking it&rsquo;s a waste of time.</p>\n<p>In this post, I want to demystify &ldquo;It&rsquo;s all Claude in the end&rdquo; to help people understand why some agents are good and others are frustrating.</p>\n<h2 id=\"defining-terms\">Defining Terms</h2>\n<p>Let&rsquo;s start with defining three standard terms. I&rsquo;m going to use the common nomenclature in this post, but it&rsquo;s important to note that their meanings are often blended in different contexts.</p>\n<ol>\n<li><strong>AI</strong>: These two small letters carry the weight of &ldquo;agents&rdquo;, &ldquo;LLMs&rdquo;, &ldquo;automation&rdquo;, &ldquo;autocomplete&rdquo;, and everything else. When someone says &ldquo;AI,&rdquo; they mean any one of these. In this post, &ldquo;AI&rdquo; refers to coding agents that have some autonomy in their work.</li>\n<li><strong>Agent</strong>: This term comes up in phrases like &ldquo;agentic workflow&rdquo; or &ldquo;coding agent&rdquo;. The agent is the engine of AI coding. The agent connects the user&rsquo;s input to the local environment&rsquo;s context (files, available tools) and provides it to the LLM. In this post, &ldquo;agent&rdquo; is a loop that takes user input, provides context to the LLM, and calls tools.</li>\n<li><strong>LLM</strong>: We all know LLMs like Claude, ChatGPT, or Codex. &ldquo;LLM&rdquo; stands for &ldquo;large language model&rdquo;. &ldquo;LLM&rdquo; is a misnomer. Many of these models accept multiple types of input and produce various outputs. A more accurate name is &ldquo;Large Multimodal Model&rdquo;. The models we use for coding are autoregressive, meaning they predict the following sequence based on previous context. For consistency with vernacular usage, I&rsquo;ll refer to them as LLMs.</li>\n</ol>\n<h2 id=\"ai-distinguishing-the-agent-from-the-llm\">AI: Distinguishing the Agent from the LLM</h2>\n<p>In the diagram below, the &ldquo;environment&rdquo; is your computer and the tools you let your agent use. If you&rsquo;ve given your agent access to <code>find</code>, for example, it will show up in the list. Depending on the agent, it might also collect some project stats to send to the LLM.</p>\n<p>When you have a <code>CLAUDE.md</code> or <code>AGENTS.md</code> file, this will get sent with the context, too.</p>\n<pre><code class=\"language-mermaid\">sequenceDiagram\n    participant U as User\n    participant AS as Agent\n    participant E as Environment\n    participant LLM as LLM (API)\n    \n    U-&gt;&gt;AS: &#34;Fix bug in auth.js&#34;\n    \n    box rgba(0,0,0,0.05) Your Computer\n      participant AS as Agent\n      participant E as Environment\n    end\n    \n    Note over AS,E: Agent Orchestration Layer&lt;br/&gt;Manages loop, provides tools, maintains context\n    loop Until task complete\n        AS-&gt;&gt;LLM: Context &#43; Available Tools &#43; User Request\n        Note over LLM: Parse intent&lt;br/&gt;Reason about next step&lt;br/&gt;Choose tool to call\n        LLM-&gt;&gt;AS: Tool call decision\n        AS-&gt;&gt;E: Execute tool (view/edit/bash)\n        E-&gt;&gt;AS: Return result\n        AS-&gt;&gt;AS: Append result to context\n    end\n    \n    AS-&gt;&gt;U: &#34;Fixed: added null check&lt;br/&gt;Tests passing ✓&#34;\n    \n    Note over U,E: Different agents = different tools, autonomy levels,&lt;br/&gt;and orchestration strategies (even with same LLM)</code></pre>\n<p>Notice that most of the work actually happens on your own machine. The agent interacts with your environment to collect information to send to the LLM, makes edits, and reports back.</p>\n<p><strong>An agent is not intelligent.</strong> This is really important to understand. An agent without an LLM is a loop with conditionals. Much like the code we write every day.</p>\n<p>Adding an LLM into the loop gives the agent the ability to reason and make decisions beyond pattern matching.</p>\n<p>Every agentic coding company will write its agent differently. The tools available, the actions the agent takes, and the system prompts it sends to the LLM are the product the company builds.</p>\n<p>Cursor is different from Claude Code because Cursor&rsquo;s agent is different, even if they both use a Claude model to reason and make decisions.</p>\n<h2 id=\"the-llm-reads-the-entire-conversation-every-time\">The LLM Reads the Entire Conversation Every Time</h2>\n<p>The LLM is an outside actor. If you are using Cursor or Claude code, every message you send and every agent loop involves API calls to the LLM provider you are using. If you are running a model locally, the model is separate from your agent system.</p>\n<p>Every time the agent calls the LLM API, it is the <em>first</em> time the LLM has ever seen your message. The LLM reads <strong>THE WHOLE MESSAGE THREAD EVERY TIME</strong>.</p>\n<p>That&rsquo;s right. Do you have a long-running conversation where you&rsquo;ve worked on three or four different tasks? When you ask the agent a question, the LLM reads the whole message thread and can easily confuse instructions you previously gave it with your current task.</p>\n<p><strong>In my experience, this is the most common reason agentic coding starts as highly accurate and quickly degrades into chaos.</strong></p>\n<p>How do you fix this? Start a new chat. It&rsquo;s that simple. Start a new chat for every task you work on.</p>\n<p><a href=\"https://www.warp.dev/\">Warp</a> has a great feature in its agent that detects a change in subject and suggests starting a new chat. I wish other agents would do that too.</p>\n<p>This is an important distinction to make. The agent&rsquo;s capabilities and system prompt affect how it works. Cursor tends to be <em>very</em> ambitious, which means the context window can quickly become bloated with failed attempts and misdirection.</p>\n<p>More cautious agents will confirm actions with the user and detect when the conversation is drifting, keeping the context focused on the task at hand.</p>\n<p>No agent is perfect, which is why I&rsquo;ve explored using the <a href=\"/use-jujutsu-to-plan-and-build-with-claude\">Jujutsu VCS to give the agent memory between sessions and narrow the context the LLM receives on each turn</a>.</p>\n<pre><code class=\"language-mermaid\">sequenceDiagram\n    participant CC as Agent\n    participant LLM as LLM (API)\n    \n    box rgba(0,0,0,0.05) Your Computer\n\n    end\n    \n    Note over CC: Context:&lt;br/&gt;[System prompt]\n    \n    CC-&gt;&gt;LLM: [System prompt, User: &#34;Fix bug&#34;]\n    LLM-&gt;&gt;CC: &#34;I&#39;ll check the file&#34;\n    Note over CC: Context:&lt;br/&gt;[System, User,&lt;br/&gt;Assistant]\n    \n    CC-&gt;&gt;LLM: [System, User, Assistant, Tool: file contents]\n    LLM-&gt;&gt;CC: &#34;I&#39;ll edit line 42&#34;\n    Note over CC: Context:&lt;br/&gt;[System, User,&lt;br/&gt;Asst, Tool,&lt;br/&gt;Asst]\n    \n    CC-&gt;&gt;LLM: [System, User, Asst, Tool, Asst, Tool: edit result]\n    LLM-&gt;&gt;CC: &#34;Fixed! Tests passing&#34;\n    Note over CC: Context:&lt;br/&gt;[System, User,&lt;br/&gt;Asst, Tool,&lt;br/&gt;Asst, Tool,&lt;br/&gt;Asst]\n    \n    Note over CC,LLM: Each API call sends the ENTIRE context&lt;br/&gt;Context grows with every message&lt;br/&gt;and tool result.</code></pre>\n<h2 id=\"key-principles\">Key Principles</h2>\n<p>The diagram below illustrates the capabilities of a coding agent. The Agent Loop is the central capability. This is the part that each company builds (Claude Code, Cursor, Zed, Warp, etc.)</p>\n<p>The LLM is separate from the agent loop. Many different agents can use the same LLM, and most agents let you pick which LLM to interact with.</p>\n<pre><code class=\"language-mermaid\">flowchart LR    \n    AgentLoop[Agent Loop] --&gt; LLM[Calls LLM]\n    \n    AgentLoop --&gt; AL1[Stateful Orchestration]\n    AgentLoop --&gt; AL2[Maintains state and history]\n    AgentLoop --&gt; AL3[Executes tools]\n    AgentLoop --&gt; AL4[Manages context window]\n    \n    LLM --&gt; L1[Stateless Reasoning]\n    LLM --&gt; L2[Processes full context each turn]\n    LLM --&gt; L3[Selects tools &amp; plans actions]\n    LLM --&gt; L4[No memory between calls]</code></pre>\n<h2 id=\"conclusion\">Conclusion</h2>\n<p>Try lots of agents! I know this can feel slow and frustrating, but when you do this, you are learning how different agents work. You are learning the new tools you can use in your job.</p>\n<p>Take 15 minutes each day to build a small project with an agent. Pick something simple, like a to-do list app. Build the same app with different agents and see how they perform.</p>\n<p>In the future, I think we will standardize on a handful of coding agents that developers can pick to work with their preferred workflow, much as we have standardized on a handful of IDEs for different use cases.</p>\n<p>I prefer Claude Code for its built-in protections and customization options.</p>\n<p>If you want an agent integrated in your IDE, you can run <code>/ide</code> to hook Claude Code up to VSCode. It&rsquo;s not as deeply integrated as Cursor is, but it works okay. Another good option is to try out <a href=\"https://zed.dev/\">Zed</a>, their agent has similar capabilities to Claude Code and is built into the IDE.</p>\n<p>I&rsquo;ve tried Warp a few times, and the deep integration of chat, CLI, and editing is promising to me, but sometimes confusing.</p>\n<p>In the end, Vim is my home row, so the setup that works for me is Claude Code in one pane and Vim in the other.</p>\n",
				"date_published": "2025-10-27T13:30:00+00:00",
				"url": "https://enumerator.dev/why-is-claude-code-different-from-cursor-if-they-both-use-claude/",
				"tags": ["ai","claude"]
			},
			{
				"id": "https://enumerator.dev/use-jujutsu-to-plan-and-build-with-claude/",
				"title": "Use Jujutsu to Plan and Build with Claude",
				"content_html": "<p><img\n    src=\"/images/use-jujutsu-to-plan-and-build-with-claude_hu_f279a776beada4a8.webp\"\n    srcset=\"/images/use-jujutsu-to-plan-and-build-with-claude_hu_e75b534a3af7f0fb.webp 576w, /images/use-jujutsu-to-plan-and-build-with-claude_hu_5dec6d9f683e501c.webp 864w, /images/use-jujutsu-to-plan-and-build-with-claude_hu_f279a776beada4a8.webp 1152w\" sizes=\"(max-width: 36rem) 100vw, 36rem\"\n    width=\"1152\"\n    height=\"618\"\n    loading=\"lazy\"\n    decoding=\"async\" alt=\"claude-jj-logo.png\"></p>\n<p>This week, I read about <a href=\"https://steve-yegge.medium.com/introducing-beads-a-coding-agent-memory-system-637d7d92514a\">Steve Yegge&rsquo;s Beads</a>.</p>\n<p>I was skeptical.</p>\n<p>He writes like someone trying to prompt inject your LLM. But he&rsquo;s always written that way.</p>\n<p>I tried beads, and it was pretty good. I wrote all of <a href=\"/uplift\">uplift</a> using beads.</p>\n<p>But it was also pretty buggy. I noticed I had multiple <code>bd</code> daemons running and checking git status. Not cool.</p>\n<h2 id=\"jujutsu\">Jujutsu</h2>\n<p>I shared beads with <a href=\"https://theinternate.com/\">Nate Smith</a>, and he said, &ldquo;You could do that with Jujutsu.&rdquo;</p>\n<p>We both tried it and it rocks.</p>\n<p><a href=\"https://github.com/cassiascheffer/dotfiles/blob/436760e954d41458214bc632661fa71af11816a7/claude/CLAUDE.md\">Here is my current CLAUDE.md </a>, which teaches Claude to use Jujutsu to plan and track issue progress.</p>\n<p><a href=\"https://github.com/jj-vcs/jj\">Jujutsu</a> is a Git-compatible version control system. The graph foundation of Jujutsu makes it perfect for this kind of work because Claude can add empty TODO commits along the way and connect them to one or more tasks that also need to be done.</p>\n<p>Here is the gist of the instructions Claude gets:</p>\n<blockquote>\n<h2 id=\"planning-best-practices\">Planning Best Practices</h2>\n<p>Create Descriptive Empty Commits:</p>\n<ul>\n<li>Each commit description should fully explain what needs to be done</li>\n<li>Include acceptance criteria in the description</li>\n<li>Note any dependencies or prerequisites</li>\n<li>Use clear, actionable language</li>\n</ul>\n</blockquote>\n<p>The steps for you are:</p>\n<ol>\n<li>Enter plan mode and build a plan with Claude.</li>\n<li>When you&rsquo;re ready, tell Claude to make empty commits for each step in the plan.</li>\n<li>Tell Claude to work through one commit at a time until it is done.</li>\n<li>If at any point Claude struggles or decides to compact memory. Kill your session and start a new one with &ldquo;We are in the middle of working through a to-do list created in <code>jj</code>. Find the task you were in the middle of and finish it, then move on to the next task until you are done.</li>\n</ol>\n<p>On Friday, I refactored a small Ruby library with Claude and Jujutsu, and it was really nice. I knew what I wanted the end state to look like and described that to Claude. Claude then made a plan with detailed acceptance criteria for 12 distinct steps. Once I approved, Claude turned the plan into <code>jj</code> commits and worked through them.</p>\n<p>Around step 7, Claude slowed down and suggested that steps 7 through 12 were tightly coupled. I ran <code>/clear</code> to start a new session and pick up where we left off. Claude still saw that steps 7 through 12 were coupled, but no longer tried to implement them all at once. It methodically worked through the steps.</p>\n<p>At the end, Claude suggested a five-PR stack so developers could review PRs implementing the requirement in separate logical steps.</p>\n<h2 id=\"why-this-works-my-theory\">Why This Works (My Theory)</h2>\n<p>Steve Yegge is right about why this works, but more than a little off base about needing a whole buggy go program to do it.</p>\n<p>Claude works better with this workflow because the context you send is focused and clear. A big old markdown doc is fine, but Claude has to read the whole thing multiple times to find the next task.</p>\n<p>With the Jujutsu workflow, Claude can read a summary of what is done with <code>jj log</code> and pick up the following task with <code>jj edit</code></p>\n<p>The LLM on the other side of the Claude agent receives a more focused context. As the conversation continues, the thread narrows to specific tasks.</p>\n<p>In past work, when I&rsquo;ve used a <code>plan.md</code> or Claude&rsquo;s default todo list, Claude struggles because it reads <em>the entire plan every time, multiple times</em>, since the whole chat is sent to the LLM with each message. This means the context gets bloated with planning and loses focus on accomplishing tasks.</p>\n<p>With the Jujutsu workflow, planning shows up exactly once in the context. Then <code>jj log</code> shows incremental progress.</p>\n<h2 id=\"i-want-this\">I Want This</h2>\n<p>It&rsquo;s good! You should try it. Get Jujutsu set up and copy my CLAUDE.md.</p>\n<p>If you don&rsquo;t know how to use Jujutsu, Claude can learn by using <code>jj help</code>.</p>\n<p>The most important thing to remember is: if you think the agent is lost, you&rsquo;re probably lost too!</p>\n<p>You can <code>/clear</code> your session, take a break, and start fresh. Claude will pick up where it left off because it knows to use <code>jj log</code> to find the next task.</p>\n",
				"date_published": "2025-10-26T12:58:00+00:00",
				"url": "https://enumerator.dev/use-jujutsu-to-plan-and-build-with-claude/",
				"tags": ["ai","jj","claude"]
			},
			{
				"id": "https://enumerator.dev/navigating-to-get-things-done/",
				"title": "Navigating to Get Things Done",
				"content_html": "<p><img\n    src=\"/images/navigating-to-get-things-done_hu_8587b4445aab8870.webp\"\n    srcset=\"/images/navigating-to-get-things-done_hu_30c483914a4ae5bf.webp 576w, /images/navigating-to-get-things-done_hu_e37e933fbd681007.webp 864w, /images/navigating-to-get-things-done_hu_8587b4445aab8870.webp 1152w\" sizes=\"(max-width: 36rem) 100vw, 36rem\"\n    width=\"1152\"\n    height=\"720\"\n    loading=\"lazy\"\n    decoding=\"async\" alt=\"Photo by Ali Kazal on Unsplash\"></p>\n<p>In engineering culture, we sometimes refer to managers and Staff Engineers as &ldquo;shit umbrellas&rdquo; for their team. I&rsquo;ve never liked this idea. It paints a picture that individual leaders create a peaceful oasis for their team while chaos reigns outside.</p>\n<p>Sounds nice to be one of those engineers! Not a care in the world. Just flow. It must feel special to have a shit umbrella manager.</p>\n<p>But the shit umbrella is a lie.</p>\n<p>If every manager believes they create a sphere of deep focus and flow, where is all the shit coming from? One team&rsquo;s flow is another team&rsquo;s dysfunction.</p>\n<p>The &ldquo;shit umbrella&rdquo; metaphor entrenches an &ldquo;us versus them&rdquo; culture and isolates individual teams from the rest of the company. It pits leaders against lowly engineers and ensures communication starts with arguments rather than curiosity.</p>\n<h2 id=\"be-a-navigator\">Be a Navigator</h2>\n<p>Effective engineers engage across an organization and know how to navigate its complexities. Effective engineers recognize that people are complex, have diverse emotions, make mistakes, and struggle to communicate effectively. And that&rsquo;s okay because we are all human.</p>\n<p>Teams need to understand how an organization operates to be effective engineers, and people leaders are responsible for helping teams focus, navigate complexities, and achieve their goals.</p>\n<p>Being a navigator for a team means helping them find their way and letting them do the work to reach their destination by helping the team understand who is who in the organization and what these individuals care about. Working with the complexities in an org is about curiosity and learning.</p>\n<p>Helping your team navigate an organization begins with trust and focus: trust that your team can handle what comes their way and that the people they work with understand their part of the organization better than you do.</p>\n<p>In <a href=\"https://terriblesoftware.org/2025/10/01/stop-avoiding-politics/\">Matheus Lima&rsquo;s recent post on politics</a>, he says:</p>\n<blockquote>\n<p>The engineers who refuse to engage with politics often complain that their companies make bad technical decisions. But they’re not willing to do what it takes to influence those decisions. They want a world where technical merit alone determines outcomes. That world doesn’t exist and never has.</p>\n</blockquote>\n<p>The same goes for managers who choose to be shit umbrellas. Protecting your team from politics isolates them from the business and creates an insular culture where &ldquo;everything could be better if&hellip;&rdquo; while not improving anything.</p>\n<h2 id=\"protecting-your-team-protects-dysfunction\">Protecting Your Team Protects Dysfunction</h2>\n<p>When people leaders try to protect their team from organizational chaos, they are unintentionally ensuring the dysfunction stays in place.</p>\n<p>The purported &ldquo;shit&rdquo; that a shit umbrella protects people from is just people being human. To quote <a href=\"https://lethain.com/extract-the-kernel/\">Will Larson</a>:</p>\n<blockquote>\n<p>[T]he reality is that executives are human. You’ll make much more progress by focusing on improving how you communicate with them than by blaming them for their deficiencies.</p>\n</blockquote>\n<p>Larson&rsquo;s comments apply to anyone you work with in your org. Improving your communication skills ensures that you approach conversations with a clear understanding of the core business needs.</p>\n<p>Blaming others for their deficiencies ensures organizational dysfunction is the norm and prevents the business from growing.</p>\n<p>People at all levels of an organization need to make mistakes and learn from them. The shit-umbrella culture breaks this feedback cycle. It allows new leaders to fail without receiving feedback and support from their peers, isolates senior leaders from the teams they need to work with, and isolates your team from the business.</p>\n<h2 id=\"focus\">Focus</h2>\n<p>People who talk about being a shit umbrella want to maintain their team&rsquo;s focus and limit distractions. It comes from a well-meaning place, but creates a culture of isolation instead of focus.</p>\n<p>As a navigator, show your team what to focus on and how to reach their destination. Trust them to raise concerns when things get hard, and trust your peers on other teams to support their work.</p>\n<p>Provide your team with a map and help them to focus on the business impact of their work, so that they can approach complex parts of the org with curiosity and an ambition to build great things for the business.</p>\n",
				"date_published": "2025-10-07T11:55:00+00:00",
				"url": "https://enumerator.dev/navigating-to-get-things-done/",
				"tags": ["organizational-culture"]
			},
			{
				"id": "https://enumerator.dev/willow-camp-updates-october-7-2025/",
				"title": "willow.camp Updates - October 7, 2025",
				"content_html": "<p>I&rsquo;ve spent most of my willow.camp work on finding a new backend language. But I&rsquo;ve still updated the Rails app! Here&rsquo;s what&rsquo;s changed since my last update.</p>\n<ul>\n<li>Moved HTML storage out of the <code>post</code> table and into <code>ActionText</code>. It was this deep dive that made me question using Rails. I went down a long rabbit hole, finding a good text editor and then realized this work felt more like building Ikea furniture than I wanted. I&rsquo;m more interested in having access to the application end-to-end than plugging parts together. Anyway, in the meantime, your posts are now stored in an ActionText table.</li>\n<li>Prevented email enumeration attacks by returning an ambiguous error on sign-up. This is my pet peeve with Devise, and I am a bit embarrassed I let this live in production for so long. Previously, the sign-up form would indicate if an email address had already been taken. Since I can&rsquo;t guarantee people use unique passwords, this means an attacker could use a leaked email/password list to access accounts that use poor password hygiene. No more! I&rsquo;ve done my best to give ambiguous, helpful messages when something is wrong on the signup page.</li>\n<li>Updated dependencies! A whole bunch of Ruby Gems and npm packages got minor version bumps.</li>\n<li>Fixed CI errors with vips. This is minor but tests were failing in CI because vips wasn&rsquo;t installed on the machine.</li>\n<li>Cleaned up the Hanami directory. There was a day when I was convinced Hanami was the next framework for willow.camp, and I started a migration in its own directory.</li>\n<li>Cleaned up image things I was trying out with images and media handling.</li>\n</ul>\n",
				"date_published": "2025-10-07T11:25:05+00:00",
				"url": "https://enumerator.dev/willow-camp-updates-october-7-2025/",
				"tags": ["willow-camp","release-notes"]
			},
			{
				"id": "https://enumerator.dev/willow-camp-next/",
				"title": "willow.camp: Next",
				"content_html": "<p>I have spent the past few weeks figuring out the future of willow.camp.</p>\n<p>It turns out I love making it, and I love that a handful of people are using it.</p>\n<p>I built willow.camp in Rails because I could prototype quickly without having to think about things like auth, logging, or building forms. Rails was great for that.</p>\n<p>Now that I have a version of willow.camp I like, I want to be more hands-on with what I&rsquo;m building, and I find that I&rsquo;ve spent more time working around the framework and various libraries than I have creating the things I want.</p>\n<p>Learning other people&rsquo;s code isn&rsquo;t particularly enjoyable.</p>\n<p>Aside: Rails and Ruby have also had a public meltdown lately, and I&rsquo;m tired of feeling like a puny developer watching giants fight and say evil things.</p>\n<h2 id=\"finding-the-next-tech-stack\">Finding the Next Tech Stack</h2>\n<p>This was a long journey.</p>\n<p>I started with <a href=\"https://hanamirb.org/\">Hanami</a>. It is still Ruby, and the community is much friendlier. Hanami didn&rsquo;t work for me. It felt like I kept bumping up against the framework, telling me I was doing it wrong. The community is friendly, but the framework is very rigid about &ldquo;the right way&rdquo; of doing things.</p>\n<p>Then I hopped over to <a href=\"https://crystal-lang.org/\">Crystal</a> and tried <a href=\"https://luckyframework.org/\">Lucky</a>, <a href=\"https://amberframework.org/\">Amber</a>, and <a href=\"https://martenframework.com/\">Marten</a>. They were all okay to work with, but didn&rsquo;t strike my fancy.</p>\n<p>Lucky was the easiest to get going. I had the data models for willow.camp up and running with a couple of rough-looking views in a few hours.</p>\n<p>Marten&rsquo;s &ldquo;apps&rdquo; are nice to work with (think slices in Hanami). But the framework doesn&rsquo;t natively support subdomain routing, and I didn&rsquo;t want to fight that design.</p>\n<p>Crystal is also very slow to compile. A straightforward Lucky app took over 30 seconds on my M1 to compile! Who has that kind of time when you need to compile multiple times an hour?</p>\n<p>Finally, the community. The Lucky devs are really active, which is nice. But Crystal itself seems to have a fragmented community. When I interacted with people, I didn&rsquo;t really see someone like me in any of the channels.</p>\n<h3 id=\"what-i-want-in-a-language-and-framework\">What I Want in A Language and Framework</h3>\n<ol>\n<li>An active, welcoming community that builds trust.</li>\n<li>Helpful error messages from the language.</li>\n<li>Good documentation.</li>\n</ol>\n<p>That&rsquo;s about it. I don&rsquo;t need huge framework features. I need a language with a helpful design and friendly people.</p>\n<h2 id=\"the-next-willowcamp-tech-stack\">The Next willow.camp Tech Stack</h2>\n<p>Once I realized that I valued community and friendly docs more than I valued framework features, I found <a href=\"https://gleam.run\">Gleam</a>.</p>\n<ul>\n<li>Backend:\n<ul>\n<li>Gleam with the <a href=\"https://github.com/gleam-wisp/wisp\">Wisp Framework</a> on a Postgres database.</li>\n</ul>\n</li>\n<li>Frontend:\n<ul>\n<li>So far, I plan to use <a href=\"https://hexdocs.pm/nakai/index.html\">Nakai</a> for server-side rendered HTML and HTMX or a similar library for interactive components.</li>\n</ul>\n</li>\n</ul>\n<p>Oh, and I&rsquo;ve already written a Gleam package! willow.camp needs an accurate domain name parser, so I wrote <a href=\"https://github.com/cassiascheffer/psl\">psl</a> to parse domains using the <a href=\"https://publicsuffix.org/\">Public Suffix List</a>.</p>\n<h2 id=\"the-next-willowcamp-features\">The Next willow.camp Features</h2>\n<p>I have a long list of features I want to build for willow.camp, and it&rsquo;s these features that inspired me to change the language and framework.</p>\n<ol>\n<li>A WYSYIG editor that supports markdown. This is the best of both worlds and opens willow.camp up to non-technical writers.</li>\n<li>An image library via <a href=\"https://openverse.org/\">openverse</a> with an image editor UI.</li>\n<li>Email newsletters! I&rsquo;m excited about this one. You will be able to collect a list of subscribers and send blog posts to your distribution list.</li>\n<li><a href=\"https://atproto.com/\">ATProto</a> and <a href=\"https://www.w3.org/TR/activitypub/\">ActivityPub</a> support.</li>\n<li>An RSS reader so you can follow blogs without a social profile.</li>\n<li>Private bookmarks with notes and highlights.</li>\n</ol>\n",
				"date_published": "2025-10-07T01:26:24+00:00",
				"url": "https://enumerator.dev/willow-camp-next/",
				"tags": ["willow-camp"]
			},
			{
				"id": "https://enumerator.dev/willow-camp-updates-september-21-2025/",
				"title": "willow.camp Updates September 21, 2025",
				"content_html": "<p>Quiet on the updates this week. I could rename &ldquo;Major Features&rdquo; to &ldquo;Major Worries&rdquo; because I&rsquo;ve spent more time worrying about how images should work than testing them with real users!</p>\n<p>I enabled image uploads on the <a href=\"github.com/avo-hq/marksmith\">marksmith</a> text editor this week, and a few people tested it out for me. Turns out marksmith doesn&rsquo;t work how I thought it would with images. Image uploads are still enabled, but I&rsquo;m going back to the drawing board with the editor UI and media management.</p>\n<h2 id=\"major-features\">Major Features</h2>\n<ul>\n<li>Image storage and processing functionality with Active Storage integration, direct uploads, and automatic image processing</li>\n<li>I heard you like willow.camp. How about deleting willow.camp? You can now delete your blog and account if you&rsquo;d like. This will delete all your data and is irreversible. The option is there if you need it.</li>\n</ul>\n<h2 id=\"improvements\">Improvements</h2>\n<ul>\n<li>Enhanced navigation to only show blog title or subdomain in menu</li>\n<li>Updated testing configuration for quieter pre-commit output</li>\n<li>Removed social_share_image functionality</li>\n</ul>\n<h2 id=\"security--dependencies\">Security &amp; Dependencies</h2>\n<ul>\n<li>Updated JavaScript dependencies</li>\n<li>Updated Ruby gem dependencies</li>\n<li>Added new license file</li>\n</ul>\n",
				"date_published": "2025-09-21T12:58:00+00:00",
				"url": "https://enumerator.dev/willow-camp-updates-september-21-2025/",
				"tags": ["willow-camp","release-notes"]
			},
			{
				"id": "https://enumerator.dev/how-much-does-it-cost-to-run-willow-camp/",
				"title": "How Much does it Cost to Run willow.camp?",
				"content_html": "<p><img\n    src=\"/images/how-much-does-it-cost-to-run-willow-camp_hu_ba1bc2c0460fdb3e.webp\"\n    srcset=\"/images/how-much-does-it-cost-to-run-willow-camp_hu_5330240592c623c1.webp 576w, /images/how-much-does-it-cost-to-run-willow-camp_hu_8d21ea327229791c.webp 864w, /images/how-much-does-it-cost-to-run-willow-camp_hu_ba1bc2c0460fdb3e.webp 1152w\" sizes=\"(max-width: 36rem) 100vw, 36rem\"\n    width=\"1152\"\n    height=\"767\"\n    loading=\"lazy\"\n    decoding=\"async\" alt=\"Photo by StellrWeb on Unsplash\"></p>\n<p>I recently enabled image uploads in the marksmith text editor on willow.camp. I&rsquo;ve been hesitant to add media management to willow.camp for two reasons: it is hard to get the user experience right, media management adds extra cost.</p>\n<p>I am being cost-conscious on willow.camp because it doesn&rsquo;t make any money. This is a passion project, and I&rsquo;m having lots of fun doing it. But the more fun I have, the more it might cost.</p>\n<p>It turns out adding storage is <em>way cheaper</em> than I thought. I would have to have a whole lot of media and a few really busy willow.camp sites to start getting charged more for data transfer fees.</p>\n<p>I will eventually build a pricing plan for willow.camp to cover costs for active sites. In the meantime, if you want to support willow.camp, you can send me a tip on <a href=\"https://ko-fi.com/cassiascheffer\">https://ko-fi.com/cassiascheffer</a>.</p>\n<h2 id=\"claude-code\">Claude Code</h2>\n<p>I include Claude Code in here because it is part of my development process. I&rsquo;m on the Max plan, and I use it enough that it pays off.</p>\n<p>In the past 30 days, I&rsquo;ve used CAD $188.49 of tokens, and I even took a week off in the woods.</p>\n<p>I use Claude Code to scaffold ideas, experiment with features, and improve test coverage. I&rsquo;ve been able to build willow.camp so quickly because Claude Code is pretty good at scaffolding a blog in Rails.</p>\n<h2 id=\"august-breakdown\">August Breakdown</h2>\n<table>\n  <thead>\n      <tr>\n          <th>service</th>\n          <th>fee (CAD)</th>\n      </tr>\n  </thead>\n  <tbody>\n      <tr>\n          <td>DigitalOcean Droplet (1 vcpu, 1 gb memory)</td>\n          <td>$8.25</td>\n      </tr>\n      <tr>\n          <td>DigitalOcean Postgres ( 1gb ram, 10 gb disk)</td>\n          <td>$20.82</td>\n      </tr>\n      <tr>\n          <td>Hatchbox.io Deployments</td>\n          <td>$14.13</td>\n      </tr>\n      <tr>\n          <td>Claude Code Max</td>\n          <td>$158.2</td>\n      </tr>\n      <tr>\n          <td>Honeybadger Error Tracking</td>\n          <td>$0</td>\n      </tr>\n      <tr>\n          <td>Total</td>\n          <td>$201.40</td>\n      </tr>\n      <tr>\n          <td>Total without Claude Code</td>\n          <td>$43.20</td>\n      </tr>\n  </tbody>\n</table>\n<h2 id=\"september-estimate\">September Estimate</h2>\n<p>Here is my estimate for September based on today&rsquo;s exchange rate.</p>\n<table>\n  <thead>\n      <tr>\n          <th>service</th>\n          <th>fee (CAD)</th>\n      </tr>\n  </thead>\n  <tbody>\n      <tr>\n          <td>DigitalOcean Droplet (1 vCPU, 1 GB memory)</td>\n          <td>$8.26</td>\n      </tr>\n      <tr>\n          <td>DigitalOcean Postgres ( 1 GB RAM, 10 GB disk)</td>\n          <td>$20.87</td>\n      </tr>\n      <tr>\n          <td>DigitalOcean Spaces (half a month)</td>\n          <td>$3.44</td>\n      </tr>\n      <tr>\n          <td>Hatchbox.io Deployments</td>\n          <td>$14.13</td>\n      </tr>\n      <tr>\n          <td>Claude Code Max</td>\n          <td>$192.84</td>\n      </tr>\n      <tr>\n          <td>Honeybadger Error Tracking</td>\n          <td>$0</td>\n      </tr>\n      <tr>\n          <td>Total</td>\n          <td>$239.54</td>\n      </tr>\n      <tr>\n          <td>Total without Claude Code</td>\n          <td>$46.70</td>\n      </tr>\n  </tbody>\n</table>\n<hr>\n<p>Photo by <a href=\"https://unsplash.com/@stellrweb?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash\"><a href=\"https://unsplash.com/@stellrweb?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash\">StellrWeb</a></a> on <a href=\"https://unsplash.com/photos/white-canon-cash-register-djb1whucfBY?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash\"><a href=\"https://unsplash.com/photos/white-canon-cash-register-djb1whucfBY?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash\">Unsplash</a></a></p>\n",
				"date_published": "2025-09-21T12:21:00+00:00",
				"url": "https://enumerator.dev/how-much-does-it-cost-to-run-willow-camp/",
				"tags": ["willow-camp"]
			},
			{
				"id": "https://enumerator.dev/naming-things-for-what-they-do/",
				"title": "Naming Things for What They Do",
				"content_html": "<p>I just saw the Rails World announcement that flavorjones is working on <a href=\"https://github.com/basecamp/activerecord-tenanted\">ActiveRecord::Tenanted</a> and all I can say is, “What a reasonable name!”</p>\n<p>When I started willow.camp I chose not to use <a href=\"https://github.com/influitive/apartment\">apartment</a> partially because the name and the metaphor were too confusing. I didn&rsquo;t like tho have to get used to the idea of requests taking &ldquo;elevators&rdquo; and all that. The &ldquo;apartment&rdquo; metaphor doesn&rsquo;t work for databases even though &ldquo;tenant&rdquo; does.</p>\n<p>Always choose descriptive names over clever ones. It doesn’t matter how well you think the metaphor fits; you’ll always end up with something confusing in the end.</p>\n",
				"date_published": "2025-09-09T11:23:33+00:00",
				"url": "https://enumerator.dev/naming-things-for-what-they-do/",
				"tags": ["ruby","rails"]
			},
			{
				"id": "https://enumerator.dev/willow-camp-updates-september-7-2025/",
				"title": "willow.camp Updates September 7 2025",
				"content_html": "<p>It&rsquo;s been a busy month! I took some time off for a much-needed vacation and forgot to post about willow.camp. I&rsquo;ve worked on some fun features in the past few weeks.</p>\n<h2 id=\"new-features\">New Features</h2>\n<ol>\n<li>Favicons now use <a href=\"https://openmoji.org/\">OpenMoji</a> emojis! I think these are adorable icons. I have a few qualms with how they fit in the willow.camp design (they&rsquo;re all a little bottom-heavy), but it&rsquo;s not a deal breaker.</li>\n<li>New favicon picker! You can now search for an emoji by name to pick for your blog.</li>\n<li>Now you can have two blogs! You can click on the profile icon and add a second blog. I&rsquo;m going to use this to document my garden and remind myself what I want to do next year.</li>\n</ol>\n<p>I also fiddled around with auto-generating OG images for each blog. I wrote a script that uses your OpenMoji favicon and your blog&rsquo;s colours to create an Open Graph image for your blog.</p>\n<p>Here&rsquo;s what the tiled image looks like for enumerator.dev.</p>\n<p><img src=\"/tiled_1F3D5_1200x630.png\" alt=\"willow.camp tile\"></p>\n<p>Ultimately, the best approach for images is to enable users to upload them instead of doing some clever auto-generation. I&rsquo;m going to put this on my project board and figure out how to do it. Since storage costs me, I might need to change the license on willow.camp and start charging for image uploads.</p>\n<p>Here is the Claude-generated summary of the last few weeks.</p>\n<h2 id=\"summary---august-17---september-7-2025\">Summary - August 17 - September 7, 2025</h2>\n<h3 id=\"major-features\">Major Features</h3>\n<ul>\n<li><strong>Multi-Blog Support</strong>: Complete implementation of &ldquo;Two Blogs per User&rdquo; feature (<a href=\"https://github.com/cassiascheffer/willow_camp/commit/a33fdaa9eec42e4f9e046653886963ce2a986dcc\">#85</a>) - Major architectural change allowing users to have multiple blogs instead of a single-tenant structure</li>\n<li><strong>OpenMoji Favicon System</strong>: Implemented comprehensive emoji favicon selector with search functionality using OpenMoji library (<a href=\"https://github.com/cassiascheffer/willow_camp/commit/2d13fd2e318339b77cf0068a0dd0b945896e44bd\">2d13fd2e</a>)</li>\n</ul>\n<h3 id=\"improvements\">Improvements</h3>\n<ul>\n<li><strong>OG Image Generation</strong>: Added automated Open Graph image generation scripts (<a href=\"https://github.com/cassiascheffer/willow_camp/commit/29ba2359fdd5b761d0cf995fbc3f8153e24a5c3f\">29ba2359</a>, <a href=\"https://github.com/cassiascheffer/willow_camp/commit/290f237256be0ff7f1ab7ddc100cbb82eef751d5\">290f2372</a>)</li>\n</ul>\n",
				"date_published": "2025-09-08T00:20:35+00:00",
				"url": "https://enumerator.dev/willow-camp-updates-september-7-2025/",
				"tags": ["willow-camp","release-notes"]
			},
			{
				"id": "https://enumerator.dev/willow-camp-updates-august-16-2025/",
				"title": "willow.camp Updates August 16, 2025",
				"content_html": "<p>It felt like a quiet week on willow.camp but somehow I still got lots done.</p>\n<p>Aside from the release notes below, I&rsquo;m focusing my energy on getting proper favicons for willow.camp blogs.</p>\n<p>My first version of this will use <a href=\"https://openmoji.org/\">OpenMoji</a> emojis for favicons. I like their simple design principles and especially like that they are CC-BY-SA 4.0, similar to willow.camp&rsquo;s source code.</p>\n<p>Here&rsquo;s how I think I&rsquo;ll approach this:</p>\n<ul>\n<li>Include OpenMoji assets in the willow.camp source code.</li>\n<li>Use their <a href=\"https://github.com/hfg-gmuend/openmoji/blob/master/data/openmoji.json\">source map</a> to populate a choices.js dropdown on the settings page.</li>\n<li>Then save the Unicode reference to the database and use that to fetch the correct favicon files when rendering a page.</li>\n</ul>\n<p>OpenMoji provides pngs in two sizes, so I&rsquo;ll need to resize them for the best cross-device compatibility.</p>\n<p>Okay, on to this week&rsquo;s summary!</p>\n<h2 id=\"improvements\">Improvements</h2>\n<ul>\n<li>Migrated system tests from Selenium to Cuprite for better performance and reliability</li>\n<li>Optimized <code>ReservedWords</code> with <code>Set</code> data structure for faster lookups</li>\n<li>Added post-deploy script to send deploy messages to Discord</li>\n</ul>\n<h2 id=\"security--dependencies\">Security &amp; Dependencies</h2>\n<ul>\n<li>Updated Rails from 8.0.2 to 8.0.2.1</li>\n<li>Updated ActiveRecord from 8.0.2 to 8.0.2.1</li>\n<li>Updated Pagy from 9.3.5 to 9.4.0</li>\n<li>Updated solid_cable from 3.0.11 to 3.0.12</li>\n<li>Updated Honeybadger from 6.0.4 to 6.0.5</li>\n<li>Updated Jbuilder from 2.13.0 to 2.14.1</li>\n<li>Updated CommonMarker from 2.3.1 to 2.3.2</li>\n<li>Updated Lefthook from 1.12.2 to 1.12.3</li>\n<li>Updated GitHub Actions dependencies (actions/checkout v4→v5, browser-actions/setup-chrome v1→v2)</li>\n<li>Implemented subdomain validation and bot protection in <code>Rack::Attack</code></li>\n<li>Added user agent and IP logging to Honeybadger error context to better understand bot behaviour</li>\n</ul>\n<h2 id=\"bug-fixes\">Bug Fixes</h2>\n<ul>\n<li>Fixed deprecated <code>:unprocessable_entity</code> status code (replaced with <code>:unprocessable_content</code>)</li>\n<li>Temporarily disabled auto-save functionality. This isn&rsquo;t a bug fix. Sorry! I want to get to fixing this soon.</li>\n<li>Remove social_share_image JavaScript and HTML. I am going to sit on this a bit. I want this to be rendered server-side instead of in the browser. Once I integrate OpenMoji, this will be much easier because I&rsquo;ll have png assets available on the server.</li>\n</ul>\n",
				"date_published": "2025-08-16T12:32:00+00:00",
				"url": "https://enumerator.dev/willow-camp-updates-august-16-2025/",
				"tags": ["willow-camp"]
			},
			{
				"id": "https://enumerator.dev/end-to-end-tests-with-minitest-and-rails/",
				"title": "End To End Tests with Minitest and Rails",
				"content_html": "<p>willow.camp has a small system test suite that runs on CI. It used to use the out-of-the-box settings that come with Rails: Capybara, Selenium, etc.</p>\n<p>I noticed a flaky test on CI, which launched a journey to replace Selenium with Cuprite.</p>\n<h2 id=\"fixing-the-flaky-test\">Fixing the Flaky Test</h2>\n<p>I worked through the flaky test with Claude Code, and as Claude was running the tests, I noticed this pop-up from Chrome.</p>\n<blockquote>\n<p>A data breach on a site or app exposed your password&hellip;</p>\n</blockquote>\n<p>I don&rsquo;t use Chrome very often, so this jumped out at me. Of course, I use the oh-so-secret password &ldquo;password&rdquo; in my test suite, and Chrome flagged it as unsafe.</p>\n<p>Disabling password leak detection fixed this for me.</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-fallback\" data-lang=\"fallback\"><span class=\"line\"><span class=\"cl\">browser_options: { &#34;disable-features&#34;: &#34;PasswordLeakDetection&#34; }\n</span></span></code></pre></div><p>But I still got timeouts on CI that looked like the tests were running before the browser was ready.</p>\n<p>Waiting for CI is a bit painful. You can see my hilariously bad Git history with Claude&rsquo;s gleeful overconfidence:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-fallback\" data-lang=\"fallback\"><span class=\"line\"><span class=\"cl\">* 0c514d1 - nosandbox (15 minutes ago) &lt;Cassia Scheffer&gt;\n</span></span><span class=\"line\"><span class=\"cl\">...\n</span></span><span class=\"line\"><span class=\"cl\">* 57ac55b - bump timeout to 30 sec for ci (21 minutes ago)\n</span></span><span class=\"line\"><span class=\"cl\">* 9d6e86e - rewirite cuprite/capybara configs (26 minutes ago)\n</span></span><span class=\"line\"><span class=\"cl\">* 39f3238 - fix: simplify Cuprite configuration following best practices (10 hours ago)\n</span></span><span class=\"line\"><span class=\"cl\">* 68bfd53 - fix: simplify Cuprite configuration and fix CI timeout issues (24 hours ago)\n</span></span><span class=\"line\"><span class=\"cl\">* cebfae7 - fix: resolve Cuprite websocket timeout issues in CI (24 hours ago)\n</span></span><span class=\"line\"><span class=\"cl\">* dc288b7 - fix: make Cuprite configuration more robust for CI environments (24 hours ago)\n</span></span><span class=\"line\"><span class=\"cl\">...\n</span></span><span class=\"line\"><span class=\"cl\">* e5bb4df - fix: increase Cuprite timeout and add CI-specific settings for GitHub Acti&gt;\n</span></span><span class=\"line\"><span class=\"cl\">...\n</span></span><span class=\"line\"><span class=\"cl\">* cb36eaa - feat: migrate system tests from Selenium to Cuprite (35 hours ago)\n</span></span><span class=\"line\"><span class=\"cl\">* 39f0ff1 - disable selenium chrome password detection (35 hours ago)\n</span></span></code></pre></div><h3 id=\"that-looks-bad-cassia-why-would-you-let-claude-do-that\">That Looks Bad, Cassia. Why Would You Let Claude Do That!?</h3>\n<p>Because Claude is faster at making mistakes than I am, and I am tired of waiting for CI. Making mistakes quickly means speedier feedback. So I sent Claude off on a fool&rsquo;s errand to help me learn from the mistakes I would otherwise have made myself.</p>\n<p>In the meantime, I read the <a href=\"https://evilmartians.com/chronicles/system-of-a-test-setting-up-end-to-end-rails-testing#dockerizing-system-tests\">Evil Martians article</a> about better system tests.</p>\n<h2 id=\"using-cuprite-in-rails-with-minitest\">Using Cuprite in Rails with Minitest</h2>\n<p>The Evil Martians article on system tests uses RSpec, but willow.camp uses Minitest. Here&rsquo;s what I had to change to make it work with Minitest.</p>\n<h3 id=\"1-precompileassets-becomes-a-module-and-gets-called-when-setup-runs\">1. PrecompileAssets becomes a module and gets called when setup runs.</h3>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-rb\" data-lang=\"rb\"><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Precompile assets before running tests to avoid timeouts.</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Do not precompile if webpack-dev-server is running (NOTE: MUST be launched with RAILS_ENV=test)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">module</span> <span class=\"nn\">PrecompileAssets</span>\n</span></span><span class=\"line\"><span class=\"cl\">  <span class=\"k\">def</span> <span class=\"nc\">self</span><span class=\"o\">.</span><span class=\"nf\">setup</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"c1\"># Check if we&#39;re running system tests by looking at the test files being loaded</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">running_system_tests</span> <span class=\"o\">=</span> <span class=\"nb\">caller</span><span class=\"o\">.</span><span class=\"n\">any?</span> <span class=\"p\">{</span> <span class=\"o\">|</span><span class=\"n\">line</span><span class=\"o\">|</span> <span class=\"n\">line</span><span class=\"o\">.</span><span class=\"n\">include?</span><span class=\"p\">(</span><span class=\"s2\">&#34;test/system&#34;</span><span class=\"p\">)</span> <span class=\"p\">}</span> <span class=\"o\">||</span>\n</span></span><span class=\"line\"><span class=\"cl\">      <span class=\"no\">ARGV</span><span class=\"o\">.</span><span class=\"n\">any?</span> <span class=\"p\">{</span> <span class=\"o\">|</span><span class=\"n\">arg</span><span class=\"o\">|</span> <span class=\"n\">arg</span><span class=\"o\">.</span><span class=\"n\">include?</span><span class=\"p\">(</span><span class=\"s2\">&#34;test/system&#34;</span><span class=\"p\">)</span> <span class=\"p\">}</span> <span class=\"o\">||</span>\n</span></span><span class=\"line\"><span class=\"cl\">      <span class=\"no\">ENV</span><span class=\"o\">[</span><span class=\"s2\">&#34;RAILS_TEST_TYPE&#34;</span><span class=\"o\">]</span> <span class=\"o\">==</span> <span class=\"s2\">&#34;system&#34;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">unless</span> <span class=\"n\">running_system_tests</span>\n</span></span><span class=\"line\"><span class=\"cl\">      <span class=\"nb\">puts</span> <span class=\"s2\">&#34;</span><span class=\"se\">\\n</span><span class=\"s2\">🚀️️  No system test selected. Skip assets compilation.</span><span class=\"se\">\\n</span><span class=\"s2\">&#34;</span>\n</span></span><span class=\"line\"><span class=\"cl\">      <span class=\"k\">return</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">end</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"nb\">puts</span> <span class=\"s2\">&#34;</span><span class=\"se\">\\n</span><span class=\"s2\">🐢  Precompiling assets.</span><span class=\"se\">\\n</span><span class=\"s2\">&#34;</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">original_stdout</span> <span class=\"o\">=</span> <span class=\"vg\">$stdout</span><span class=\"o\">.</span><span class=\"n\">clone</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"n\">start</span> <span class=\"o\">=</span> <span class=\"no\">Time</span><span class=\"o\">.</span><span class=\"n\">current</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">begin</span>\n</span></span><span class=\"line\"><span class=\"cl\">      <span class=\"vg\">$stdout</span><span class=\"o\">.</span><span class=\"n\">reopen</span><span class=\"p\">(</span><span class=\"no\">File</span><span class=\"o\">.</span><span class=\"n\">new</span><span class=\"p\">(</span><span class=\"no\">File</span><span class=\"o\">::</span><span class=\"no\">Constants</span><span class=\"o\">::</span><span class=\"no\">NULL</span><span class=\"p\">,</span> <span class=\"s2\">&#34;w&#34;</span><span class=\"p\">))</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">      <span class=\"nb\">require</span> <span class=\"s2\">&#34;rake&#34;</span>\n</span></span><span class=\"line\"><span class=\"cl\">      <span class=\"no\">Rails</span><span class=\"o\">.</span><span class=\"n\">application</span><span class=\"o\">.</span><span class=\"n\">load_tasks</span>\n</span></span><span class=\"line\"><span class=\"cl\">      <span class=\"no\">Rake</span><span class=\"o\">::</span><span class=\"no\">Task</span><span class=\"o\">[</span><span class=\"s2\">&#34;assets:precompile&#34;</span><span class=\"o\">].</span><span class=\"n\">invoke</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">ensure</span>\n</span></span><span class=\"line\"><span class=\"cl\">      <span class=\"vg\">$stdout</span><span class=\"o\">.</span><span class=\"n\">reopen</span><span class=\"p\">(</span><span class=\"n\">original_stdout</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">      <span class=\"nb\">puts</span> <span class=\"s2\">&#34;Finished in </span><span class=\"si\">#{</span><span class=\"p\">(</span><span class=\"no\">Time</span><span class=\"o\">.</span><span class=\"n\">current</span> <span class=\"o\">-</span> <span class=\"n\">start</span><span class=\"p\">)</span><span class=\"o\">.</span><span class=\"n\">round</span><span class=\"p\">(</span><span class=\"mi\">2</span><span class=\"p\">)</span><span class=\"si\">}</span><span class=\"s2\"> seconds&#34;</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">end</span>\n</span></span><span class=\"line\"><span class=\"cl\">  <span class=\"k\">end</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">end</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Run the setup when this file is loaded</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"no\">PrecompileAssets</span><span class=\"o\">.</span><span class=\"n\">setup</span> <span class=\"k\">if</span> <span class=\"n\">defined?</span><span class=\"p\">(</span><span class=\"no\">Rails</span><span class=\"p\">)</span>\n</span></span></code></pre></div><h3 id=\"2-betterrailssystemtests-becomes-a-moduel\">2. BetterRailsSystemTests Becomes a Moduel</h3>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-rb\" data-lang=\"rb\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">module</span> <span class=\"nn\">BetterRailsSystemTests</span>\n</span></span><span class=\"line\"><span class=\"cl\">  <span class=\"c1\"># Make failure screenshots compatible with multi-session setup.</span>\n</span></span><span class=\"line\"><span class=\"cl\">  <span class=\"k\">def</span> <span class=\"nf\">take_screenshot</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">return</span> <span class=\"k\">super</span> <span class=\"k\">unless</span> <span class=\"no\">Capybara</span><span class=\"o\">.</span><span class=\"n\">last_used_session</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"no\">Capybara</span><span class=\"o\">.</span><span class=\"n\">using_session</span><span class=\"p\">(</span><span class=\"no\">Capybara</span><span class=\"o\">.</span><span class=\"n\">last_used_session</span><span class=\"p\">)</span> <span class=\"p\">{</span> <span class=\"k\">super</span> <span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\">  <span class=\"k\">end</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">end</span>\n</span></span></code></pre></div><h3 id=\"3-modules-are-included-in-applicationsystemtestcase\">3. Modules are included in ApplicationSystemTestCase</h3>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-rb\" data-lang=\"rb\"><span class=\"line\"><span class=\"cl\"><span class=\"nb\">require</span> <span class=\"s2\">&#34;test_helper&#34;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"nb\">require</span> <span class=\"s2\">&#34;capybara/cuprite&#34;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">require_relative</span> <span class=\"s2\">&#34;system/system_helper&#34;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">class</span> <span class=\"nc\">ApplicationSystemTestCase</span> <span class=\"o\">&lt;</span> <span class=\"no\">ActionDispatch</span><span class=\"o\">::</span><span class=\"no\">SystemTestCase</span>\n</span></span><span class=\"line\"><span class=\"cl\">  <span class=\"kp\">include</span> <span class=\"no\">BetterRailsSystemTests</span>\n</span></span><span class=\"line\"><span class=\"cl\">  <span class=\"kp\">include</span> <span class=\"no\">CupriteHelpers</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">  <span class=\"n\">driven_by</span> <span class=\"no\">Capybara</span><span class=\"o\">.</span><span class=\"n\">javascript_driver</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">  <span class=\"k\">def</span> <span class=\"nf\">setup</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">super</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"c1\"># Use JS driver always</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"c1\"># Store original host for cleanup</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"vi\">@original_host</span> <span class=\"o\">=</span> <span class=\"no\">Rails</span><span class=\"o\">.</span><span class=\"n\">application</span><span class=\"o\">.</span><span class=\"n\">default_url_options</span><span class=\"o\">[</span><span class=\"ss\">:host</span><span class=\"o\">]</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"c1\"># Make urls in mailers contain the correct server host.</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"c1\"># This is required for testing links in emails (e.g., via capybara-email).</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"no\">Rails</span><span class=\"o\">.</span><span class=\"n\">application</span><span class=\"o\">.</span><span class=\"n\">default_url_options</span><span class=\"o\">[</span><span class=\"ss\">:host</span><span class=\"o\">]</span> <span class=\"o\">=</span> <span class=\"no\">Capybara</span><span class=\"o\">.</span><span class=\"n\">server_host</span>\n</span></span><span class=\"line\"><span class=\"cl\">  <span class=\"k\">end</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">  <span class=\"k\">def</span> <span class=\"nf\">teardown</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"c1\"># Restore original host</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"no\">Rails</span><span class=\"o\">.</span><span class=\"n\">application</span><span class=\"o\">.</span><span class=\"n\">default_url_options</span><span class=\"o\">[</span><span class=\"ss\">:host</span><span class=\"o\">]</span> <span class=\"o\">=</span> <span class=\"vi\">@original_host</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"k\">super</span>\n</span></span><span class=\"line\"><span class=\"cl\">  <span class=\"k\">end</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">end</span>\n</span></span></code></pre></div><h2 id=\"flakiness-still-included-on-ci\">Flakiness Still Included on CI</h2>\n<p>I still see a bit of system test flakiness on CI, but far less than before. I am running on the default GitHub Actions runner, which is pretty small. So there&rsquo;s a good chance that the flakiness is due to low resources on the machine. So far, tests have consistently passed when I run these on my M1 MacBook Air.</p>\n<p>I&rsquo;ll dig into these flaky tests another day! But this looks like an improvement overall to me!</p>\n",
				"date_published": "2025-08-13T22:27:26+00:00",
				"url": "https://enumerator.dev/end-to-end-tests-with-minitest-and-rails/",
				"tags": ["willow-camp","rails","capybra","minitest"]
			}
	]
}
