<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[The Engineering Product]]></title><description><![CDATA[I write about building software in the age of AI. Most posts explore the intersection of engineering leadership, AI-assisted development, and what it means to be a useful generalist. I draw from a decade of full-stack work, open source contributions across 50+ repos, and a belief that design, product, and engineering are converging into a single builder role. Expect practical takes on tools like Claude Code, patterns like state machines and functional programming, and the organizational dynamics that shape how software actually ships.]]></description><link>https://blog.jcosta.tech</link><generator>RSS for Node</generator><lastBuildDate>Wed, 08 Apr 2026 15:06:16 GMT</lastBuildDate><atom:link href="https://blog.jcosta.tech/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[AI Is Bringing Developers Back to the Terminal. Ink Is Making It Beautiful.]]></title><description><![CDATA[The terminal is having a moment, and AI is the reason.
Claude Code. Gemini CLI. GitHub Copilot CLI. The most talked-about developer tools of the past year all live in the terminal. Not web apps or des]]></description><link>https://blog.jcosta.tech/ai-is-bringing-developers-back-to-the-terminal-ink-is-making-it-beautiful</link><guid isPermaLink="true">https://blog.jcosta.tech/ai-is-bringing-developers-back-to-the-terminal-ink-is-making-it-beautiful</guid><category><![CDATA[cli]]></category><category><![CDATA[React]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[terminal]]></category><category><![CDATA[Developer Tools]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[John Costa]]></dc:creator><pubDate>Tue, 07 Apr 2026 15:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/62fdd4ef1ebeafdc3d32f72e/80c7e666-2037-4a69-812f-44ec8935610b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The terminal is having a moment, and AI is the reason.</p>
<p>Claude Code. Gemini CLI. GitHub Copilot CLI. The most talked-about developer tools of the past year all live in the terminal. Not web apps or desktop GUIs, but terminal applications. Traditional developers leveraging AI assistance are spending more time in the terminal than ever before, and it's not just for git commands and SSH anymore. Meanwhile, a whole new generation of vibe coders are discovering the command line for the first time because that's where the AI tools are meeting them.</p>
<p>What most people don't realize is that all three of those tools and many others are all built on the same library: <a href="https://github.com/vadimdemedes/ink">Ink</a>. Ink has <strong>36k stars</strong> on GitHub and <strong>2.8 million weekly downloads</strong> on npm.</p>
<p>Ink is the layer that makes terminal applications look and feel good. Colors, layouts, interactive elements, smooth scrolling. Without it, most CLI tools would just be raw text on a black screen. Ink is to the terminal what CSS and React are to the web browser.</p>
<p>But Ink's core only covers the basics. For richer interactive components, you need community-built libraries, and there's still a lot of room to build.</p>
<p>That's where I come in. I'm currently the project's <strong>third most active contributor</strong>, behind only the creator and primary maintainer. I added <a href="https://github.com/vadimdemedes/ink/pull/855">kitty keyboard protocol support</a>, built a <code>renderToString()</code> API, and fixed bugs in the reconciler and fullscreen rendering. Working on the internals showed me exactly what was missing.</p>
<h2>Six components I shipped</h2>
<h3><a href="https://github.com/costajohnt/ink-timer">ink-timer</a></h3>
<p>Ready-made timers, countdowns, and stopwatches for any CLI that needs them. Surprisingly, there wasn't a good one.</p>
<h3><a href="https://github.com/costajohnt/ink-tree-view">ink-tree-view</a></h3>
<p>Collapsible tree with keyboard navigation, virtual scrolling, multi-select, and async child loading. Think VS Code's file explorer, but in the terminal.</p>
<img src="https://cdn.hashnode.com/uploads/covers/62fdd4ef1ebeafdc3d32f72e/e5ee2c26-4f73-43e5-af5e-c6b07927b1d4.gif" alt="" style="display:block;margin:0 auto" />

<h3><a href="https://github.com/costajohnt/ink-autocomplete">ink-combobox</a></h3>
<p>Fuzzy-search autocomplete that filters and ranks as you type. Supports both static option lists and async providers.</p>
<img src="https://cdn.hashnode.com/uploads/covers/62fdd4ef1ebeafdc3d32f72e/061a8bd4-1e3d-4b42-b03e-2818b614c88d.gif" alt="" style="display:block;margin:0 auto" />

<h3><a href="https://github.com/costajohnt/ink-file-picker">ink-file-picker</a></h3>
<p>Filesystem browser where you can navigate directories, filter by glob, and select files. Any CLI that needs the user to pick a file benefits from an interactive picker instead of asking them to type a path.</p>
<img src="https://cdn.hashnode.com/uploads/covers/62fdd4ef1ebeafdc3d32f72e/c2e777ee-0e8e-414a-9349-b75eab1728f3.gif" alt="" style="display:block;margin:0 auto" />

<h3><a href="https://github.com/costajohnt/ink-json-viewer">ink-json-viewer</a></h3>
<p>Interactive JSON tree with syntax coloring, expand/collapse, and virtual scrolling. Saves you from the <code>JSON.stringify(data, null, 2)</code> wall of text.</p>
<img src="https://cdn.hashnode.com/uploads/covers/62fdd4ef1ebeafdc3d32f72e/e85af8f6-3bea-40ac-9909-08520174563e.gif" alt="" style="display:block;margin:0 auto" />

<h3><a href="https://github.com/costajohnt/ink-scrollable-box">ink-scrollable-box</a></h3>
<p>Scrollable container with keyboard navigation, vim bindings, and auto-follow for streaming content. Drop it around any content that might overflow and it just works.</p>
<hr />
<p>As more people spend more time in the terminal, these are the kinds of building blocks the ecosystem needs. The terminal isn't going anywhere. It's getting better.</p>
<p>If you want to follow along with what I'm working on, you can find me on <a href="https://github.com/costajohnt">GitHub</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Building a Self-Improving Trading System With AI]]></title><description><![CDATA[I built an automated paper trading system. Then I gave it an AI agent that reviews its own performance and deploys improvements. Here's what that looks like in practice, and what I learned about givin]]></description><link>https://blog.jcosta.tech/building-a-self-improving-trading-system-with-ai</link><guid isPermaLink="true">https://blog.jcosta.tech/building-a-self-improving-trading-system-with-ai</guid><category><![CDATA[ai agents]]></category><category><![CDATA[trading, ]]></category><category><![CDATA[autonomous agents]]></category><category><![CDATA[safety]]></category><category><![CDATA[GitHub Actions]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[John Costa]]></dc:creator><pubDate>Tue, 31 Mar 2026 15:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/62fdd4ef1ebeafdc3d32f72e/35b09890-fe4b-468c-95d5-226fca354cc1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I built an automated paper trading system. Then I gave it an AI agent that reviews its own performance and deploys improvements. Here's what that looks like in practice, and what I learned about giving an AI real autonomy over a system that matters.</p>
<h2>The System</h2>
<p>The trading system runs on Alpaca's paper trading API. It combines four strategies: mean reversion with weekly trend confirmation, AI-powered news sentiment, Finnhub insider trading signals, and momentum detection. Every trade goes through three layers of analysis before execution. Position sizing is conviction-based and capped. Sector exposure is tracked and limited.</p>
<p>It runs twice daily via GitHub Actions. The pipeline manages exits first, then scans for new opportunities across 11K+ equities, and logs everything to CSV for analysis later.</p>
<p>None of this is novel. Retail algo trading has been a popular hobby project for years. What made it interesting to me was the next step.</p>
<h2>The Agent</h2>
<p>Once the system was stable and generating data, I added an autonomous strategy agent. It's Claude Opus 4.6 running through OpenRouter, triggered weekly by a separate GitHub Actions workflow.</p>
<p>The agent has a structured reasoning process: observe performance data, compare against previous changes, diagnose the biggest problem, validate a fix via backtesting, and either deploy or wait. It can modify strategy parameters, adjust filters, create shadow experiments, and roll back failures.</p>
<p>This is the part where most people get nervous. An AI modifying production code autonomously? On a system that trades real money (well, paper money)?</p>
<p>Fair concern. Here's how I handled it.</p>
<h2>The Safety Architecture</h2>
<p>The agent has tools. Those tools have hardcoded constraints that the agent cannot override, because they live in a file the agent cannot modify.</p>
<p>Minimum sample sizes are enforced at the tool level, not the prompt level. The agent can't deploy a parameter change unless there are at least 50 trades worth of data backing the decision. It can't modify filters without 30 data points. These aren't suggestions in a system prompt. They're Python assertions that throw errors.</p>
<p>The agent can't touch its own code, the risk guard, the resilience layer, tests, or workflows. It gets a maximum of 2 deploys per week. Any file it changes enters a 14-day cooling period before it can be changed again. All tests must pass before any deploy goes through.</p>
<p>Position size is capped at 5% of equity. The max-loss stop can never be disabled. These are hardcoded constants, not configurable parameters.</p>
<p>If the agent's run ends without it calling <code>log_decision</code> (because of an API error or hitting the turn limit), the system forces a minimal log entry. Every run produces an audit trail.</p>
<p>If the changelog file is corrupt, the agent halts entirely. It doesn't try to recover or start fresh. A corrupt changelog means something unexpected happened, and the right response is to stop and let a human look at it.</p>
<h2>What I Learned</h2>
<p><strong>The prompt is not the safety layer.</strong> Early on, I had safety rules in the system prompt: "never exceed 50 trades minimum sample size." That's a suggestion. The model can ignore it, misinterpret it, or forget it in a long conversation. The real safety layer is the code that executes the tools. If the tool throws an error when you try to deploy without enough data, it doesn't matter what the model thinks.</p>
<p><strong>Shadow experiments are underrated.</strong> The agent can create experiments where it logs what a parameter change <em>would</em> have done without actually changing anything. This lets it gather evidence before committing to a change. Most of the time, the right decision is to wait and observe. Giving the agent a structured way to "wait and observe" prevents it from deploying premature changes just because it feels like it should do something.</p>
<p><strong>First run behavior matters.</strong> I had to explicitly handle the case where the agent runs for the first time and there's no historical data. Without guidance, it would try to "fix" the lack of data by deploying changes. The system prompt now says: if this is your first run, establish a baseline and do NOT make changes. And the minimum sample size enforcement backs this up at the tool level.</p>
<p><strong>Atomic operations everywhere.</strong> Every file write goes through <code>tempfile.mkstemp()</code> + <code>os.replace()</code>. If the process crashes mid-write, you get either the old file or the new file, never a half-written one. This matters more than you'd think when you have concurrent GitHub Actions runs writing to the same repository.</p>
<p><strong>The git push race condition.</strong> Two cron schedules can overlap if one runs long. The first run pushes, the second run's push gets rejected. I lost a day's worth of state data before adding <code>git pull --rebase</code> before every push in the workflow. Boring problem. Real consequence.</p>
<h2>The Engineering Management Parallel</h2>
<p>I've <a href="https://blog.jcosta.tech/why-ai-assisted-development-feels-like-engineering-management">written before</a> about how AI-assisted development feels like engineering management. This project made that analogy feel very literal.</p>
<p>The strategy agent is like a junior developer with good instincts but no judgment about when to act. The safety rails are the code review process. The minimum sample sizes are the "show me the data" conversations. The cooling period is the "let's see how the last change lands before making another one."</p>
<p>I still review its changelog. I still check what it deployed. I still override it when I disagree. But I don't have to be there for every decision. That's the point.</p>
<h2>The Stack</h2>
<p>For anyone curious:</p>
<ul>
<li><strong>Trading</strong>: Alpaca API (paper trading), Python, pandas</li>
<li><strong>Strategies</strong>: Mean reversion, sentiment (via OpenRouter), insider signals (Finnhub), momentum</li>
<li><strong>Agent</strong>: Claude Opus 4.6 via OpenRouter, OpenAI-compatible SDK</li>
<li><strong>Infrastructure</strong>: GitHub Actions (daily pipeline + weekly agent review)</li>
<li><strong>Safety</strong>: Hardcoded tool constraints, atomic writes, walk-forward backtesting with holdout validation</li>
<li><strong>Tests</strong>: 654 tests across 30 files</li>
</ul>
<p>The whole thing runs on free-tier GitHub Actions. No servers to maintain. The project is <a href="https://github.com/costajohnt/alpaca-trader">open source</a>.</p>
<h2>Is It Making Money?</h2>
<p>Not yet. It's paper trading, and the portfolio is down about 3% over its first six weeks. The agent hasn't deployed any live parameter changes because it hasn't hit the minimum sample sizes required to justify one. That's the correct behavior.</p>
<p>What it has done is run shadow experiments. It noticed that positions were showing poor risk/reward asymmetry (6% take-profit vs 12% stop-loss) and created an experiment to test tighter trailing stops without actually changing anything in production. It also paused the screener strategy entirely after the win rate dropped below 30%, which is exactly the kind of tactical decision it's designed to make.</p>
<p>So the system is working as intended. It's observing, gathering data, making small low-risk moves, and waiting for evidence before committing to real changes. It's not making money, but it's not supposed to yet.</p>
<p>The idea is that over time, as it accumulates enough trades and enough data, it starts proving gains in paper trading. If it can demonstrate consistent improvement over months of autonomous operation, then maybe it earns the right to trade with real money. That's the experiment.</p>
]]></content:encoded></item><item><title><![CDATA[Automate Your Life With Cron Jobs, GitHub Actions, and Telegram]]></title><description><![CDATA[I check my phone in the morning and there are two Telegram messages waiting. One is a summary of trades a bot made overnight. The other is a heads-up that a new open source bounty just got posted.
I d]]></description><link>https://blog.jcosta.tech/automate-your-life-with-cron-jobs-github-actions-and-telegram</link><guid isPermaLink="true">https://blog.jcosta.tech/automate-your-life-with-cron-jobs-github-actions-and-telegram</guid><category><![CDATA[automation]]></category><category><![CDATA[github-actions]]></category><category><![CDATA[telegram bot]]></category><category><![CDATA[AI]]></category><category><![CDATA[cronjob]]></category><dc:creator><![CDATA[John Costa]]></dc:creator><pubDate>Thu, 26 Mar 2026 15:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/62fdd4ef1ebeafdc3d32f72e/d8f440fb-31b5-4942-9255-81226e523436.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I check my phone in the morning and there are two Telegram messages waiting. One is a summary of trades a bot made overnight. The other is a heads-up that a new open source bounty just got posted.</p>
<p>I didn't set an alarm or check a dashboard. A few scheduled jobs and a Telegram bot handle all of this, running on GitHub Actions free tier. No servers or hosting costs.</p>
<p>Here's what I've built with that setup.</p>
<h2>Trade notifications (and a bot that improves itself)</h2>
<p>I have an automated trading system that runs on Alpaca's paper trading API. A cron job runs the trading pipeline twice a day on GitHub Actions. When it finishes, it sends me a Telegram message with what it did.</p>
<p>The more interesting part is a separate weekly job. Once a week, an AI agent reviews the bot's performance, looks at what's working and what isn't, and if it finds a justified improvement, it actually modifies the trading strategy and deploys the change. The bot is tweaking its own code based on how it performs.</p>
<p>There's a lot of safety architecture around that part (full post on this coming soon). But the trigger for all of it is just a cron schedule in a config file.</p>
<h2>Bounty alerts</h2>
<p>Open source bounties are GitHub issues with cash rewards attached. Anyone can work on them, but you usually need to claim the issue first. Popular ones get grabbed fast.</p>
<p>I built a tool that polls GitHub and Algora every few minutes for new bounty issues matching my watchlist. When one appears, Telegram buzzes. Seeing a bounty 5 minutes after it's posted vs. 5 hours later is often the difference between getting it and not.</p>
<p>There's also an AI-assisted mode that investigates the codebase and drafts a proposal when I decide to go after one. But the core of it is just a polling script and a Telegram message.</p>
<h2>A website that updates itself</h2>
<p>I rebuilt my personal portfolio site as plain HTML. No frameworks, no build step. But it has a <a href="https://blog.jcosta.tech/your-portfolio-site-deserves-a-pipeline">data pipeline behind it</a>.</p>
<p>A daily cron job pulls my latest blog posts from Hashnode and my open source contribution stats from GitHub. If anything changed, it updates the HTML and commits. There's also a webhook path for real-time updates when I publish, but the cron job is the safety net for when webhooks fail.</p>
<p>I published a blog post last week and didn't think about my portfolio at all. The next morning, it was already there.</p>
<h2>Claude Code from your phone</h2>
<p>This one isn't a cron job, but it fits the theme.</p>
<p>Claude Code has a feature called Channels that connects a running session to a Telegram bot. Once paired, you message Claude from your phone and it works with your actual dev environment. Files, terminal, tools. It replies back in Telegram.</p>
<p>Your machine needs to stay on, but as long as it does, you've got a dev environment in your pocket.</p>
<h2>The pattern</h2>
<p>Something runs on a schedule. It does useful work. It tells you about it, or just handles it quietly.</p>
<p>You don't need to understand cron syntax or YAML deeply. Describe what you want to an AI and it'll help you wire it up. The hardest part is the idea. Once you build your first one and your phone buzzes with something useful that happened while you weren't looking, you start seeing them everywhere.</p>
]]></content:encoded></item><item><title><![CDATA[Your Portfolio Site Deserves a Pipeline]]></title><description><![CDATA[I recently rebuilt my portfolio site from scratch. No React. No Next.js. No build step. Just HTML, CSS, and a few scripts.
It loads in under 200ms. There's nothing to hydrate, no bundle to download, n]]></description><link>https://blog.jcosta.tech/your-portfolio-site-deserves-a-pipeline</link><guid isPermaLink="true">https://blog.jcosta.tech/your-portfolio-site-deserves-a-pipeline</guid><category><![CDATA[Web Development]]></category><category><![CDATA[portfolio]]></category><category><![CDATA[GitHub Actions]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[automation]]></category><dc:creator><![CDATA[John Costa]]></dc:creator><pubDate>Sun, 15 Mar 2026 04:11:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/62fdd4ef1ebeafdc3d32f72e/4fb15c38-828d-4200-a6d0-02dc6b7578fc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I recently rebuilt my portfolio site from scratch. No React. No Next.js. No build step. Just HTML, CSS, and a few scripts.</p>
<p>It loads in under 200ms. There's nothing to hydrate, no bundle to download, no client-side rendering. It's the simplest thing I could have built.</p>
<p>But here's the thing: it updates itself.</p>
<img src="https://cdn.hashnode.com/uploads/covers/62fdd4ef1ebeafdc3d32f72e/b2bbf38a-2264-4a5a-a8ab-93eeab70a369.png" alt="" style="display:block;margin:0 auto" />

<h2><strong>The problem with portfolio sites</strong></h2>
<p>Every developer has built a portfolio site and then abandoned it. The blog section still shows posts from 2022. The "recent projects" haven't been updated since you launched. It was supposed to represent who you are, but it represents who you were.</p>
<p>The reason is obvious. Updating a static site means opening the HTML, editing content by hand, and pushing to GitHub. It takes ten minutes. You'll never do it.</p>
<p>The site I replaced had exactly this problem. I'd publish a blog post on Hashnode and forget to update my portfolio. I'd merge a PR into a major open source project and my site had no idea. The portfolio was a snapshot, not a reflection.</p>
<p>So when I rebuilt it, I decided the site itself could stay simple. The engineering would go into keeping it alive.</p>
<h2><strong>What stays fresh automatically</strong></h2>
<p>My site tracks four things without me touching it:</p>
<ul>
<li><p><strong>Blog posts</strong> pulled from Hashnode's GraphQL API</p>
</li>
<li><p><strong>OSS contribution stats</strong>: total PRs merged, repos contributed to, languages used (GitHub API)</p>
</li>
<li><p><strong>Recently merged PRs</strong> with repo name, star count, and language</p>
</li>
<li><p><strong>Notable repos</strong> ranked by a composite score of project size and contribution depth</p>
</li>
</ul>
<p>None of this is hardcoded. If I merge a PR into a new repo tomorrow morning, it shows up on my site by tomorrow afternoon.</p>
<h2><strong>The architecture</strong></h2>
<p>The system has three layers: data fetching, HTML injection, and triggers.</p>
<img src="https://cdn.hashnode.com/uploads/covers/62fdd4ef1ebeafdc3d32f72e/7ece78c2-c03b-48ef-a138-837ce990d3ef.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>Layer 1: Data fetching</strong></h3>
<p>Two Node.js scripts pull data from external APIs and write JSON files. One hits Hashnode's GraphQL API for blog posts. The other uses GitHub's search API to find all my merged PRs across public repos.</p>
<p>The OSS stats script is <strong>incremental</strong>. It maintains a full list of merged PRs in <code>data/merged-prs.json</code> and only fetches PRs newer than the most recent one on file. This matters because GitHub's search API caps results at 1000. If you have more than that and try to fetch them all at once, you'll silently lose data. The incremental approach sidesteps this entirely: each run only needs to fetch what's new since yesterday.</p>
<p>For each new PR, the script fetches repo metadata (star count, primary language), filters out repos below 50 stars, handles rate limiting by checking <code>X-RateLimit-Remaining</code> and backing off when it gets low, and caches repo metadata to avoid redundant API calls.</p>
<p>From the merged PR list, a <code>deriveOssStats()</code> function builds everything the frontend needs: total counts, per-repo breakdowns, language distribution, and a ranked "notable repos" list. The ranking was an interesting problem.</p>
<p>I didn't want it to be pure star count (that would just show whichever massive repo I happened to send one PR to). I also didn't want pure PR count (that would over-index on small repos I had a drive-by fix for). The composite score:</p>
<pre><code class="language-plaintext">score = stars * sqrt(prCount)
</code></pre>
<p><code>sqrt</code> dampens the PR count so that quantity matters, but with diminishing returns. In practice this means <a href="https://github.com/vadimdemedes/ink">ink</a> (35.6k stars, 15 PRs, score ~137k) ranks above <a href="https://github.com/Homebrew/brew">Homebrew</a> (47k stars, 2 PRs, score ~66k). Depth of contribution wins over drive-by fame.</p>
<h3><strong>Layer 2: Marker-based HTML injection</strong></h3>
<p>The site is plain HTML. There's no templating engine, no static site generator, no build step. So how does data get into the page?</p>
<p>Comment markers. An embed script reads each JSON data file, generates HTML snippets, and replaces everything between matching <code>&lt;!-- BEGIN:X --&gt;</code> and <code>&lt;!-- END:X --&gt;</code> comment pairs. The core is a single regex replacement function:</p>
<pre><code class="language-javascript">function replaceBetweenMarkers(html, beginMarker, endMarker, newContent) {
  const pattern = new RegExp(
    `(\({escaped(beginMarker)})([\\s\\S]*?)( *\){escaped(endMarker)})`, 'g'
  );
  return html.replace(pattern, `\(1\n\){newContent}\n$3`);
}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/62fdd4ef1ebeafdc3d32f72e/664e7b07-5189-4a97-bb7e-d398d03c8331.png" alt="" style="display:block;margin:0 auto" />

<p>The regex captures the begin marker, everything between, and the end marker. The replacement keeps both markers in place and swaps the middle. This makes it idempotent: you can run it repeatedly and it produces the same result. You can also view source on the live site and see exactly where the dynamic content lives.</p>
<p>I use six marker pairs across two HTML files: four in <code>index.html</code> (stats, repos, recent PRs, blog posts) and two in <code>contributions.html</code> (stats summary, full PR list). Adding a new dynamic section means adding a marker pair and a generate function. That's it.</p>
<p>Since the pipeline fetches from external APIs I don't control, every field gets run through <code>escapeHtml()</code> before interpolation. If someone names their repo <code>&lt;script&gt;alert(1)&lt;/script&gt;</code>, it renders as text.</p>
<h3><strong>Layer 3: Triggers (and why there are two)</strong></h3>
<p>When I publish a blog post on Hashnode, I want it on my portfolio immediately. Hashnode supports webhooks. So the primary path is real-time: I publish, Hashnode fires a webhook, my site updates within 30 seconds.</p>
<p>But I've been building production systems long enough to know that you can't trust third parties. Webhooks fail silently. Hashnode might change their webhook format. Cloudflare might have an outage. The GitHub API might rate-limit the dispatch. Any link in a three-service chain can break, and you won't know until someone visits your site and sees stale data.</p>
<p>So the webhook is the fast path, and a daily cron job is the safety net. If the webhook fires, great, the site updates in seconds. If it doesn't, the cron job catches it within 24 hours. The same update workflow runs either way, and it's idempotent, so it doesn't matter if both fire on the same day. The cron doesn't check whether the webhook already ran. It just fetches the latest data and embeds it. If nothing changed, no commit is created.</p>
<p>This is the same pattern you'd use in any distributed system: optimistic real-time updates with a periodic reconciliation loop as a backstop.</p>
<h4><strong>The webhook relay</strong></h4>
<p>GitHub Actions can't receive arbitrary webhooks directly. You need something in the middle. I used a Cloudflare Worker as a relay: it receives the Hashnode webhook, validates the HMAC signature using <code>crypto.subtle.verify()</code> for timing-safe comparison, and dispatches a <code>repository_dispatch</code> event to GitHub. The whole worker is about 40 lines. It validates the signature format before touching crypto, and error responses don't leak any details about the downstream GitHub integration.</p>
<p>Deploy is a single command (<code>npx wrangler deploy</code>), secrets are set in the Cloudflare dashboard.</p>
<h4><strong>The cron fallback</strong></h4>
<p>The GitHub Actions workflow accepts all three triggers in one file:</p>
<pre><code class="language-yaml">on:
  schedule:
    - cron: '0 6 * * *'    # Daily at 6 AM UTC
  workflow_dispatch:         # Manual trigger for debugging
  repository_dispatch:
    types: [hashnode-post-published]  # Webhook path
</code></pre>
<p>The job runs the same steps regardless of which trigger fired: run tests, fetch data, embed into HTML, commit if changed. The conditional commit (<code>git diff --staged --quiet</code>) is what makes the dual-trigger approach safe. If the webhook already ran and embedded the same data, the cron creates no commit. No harm, no noise.</p>
<p>The OSS stats workflow is similar but only has cron and manual triggers. There's no webhook equivalent for "you merged a PR on GitHub," so the daily cron is the only automated path there.</p>
<h2><strong>Testing</strong></h2>
<p>The pipeline generates HTML that gets served to real users, so I test it the same way I'd test production code.</p>
<p>The test suite has 27 tests across four layers: pure function correctness (escaping, date formatting, star display), marker replacement logic, structural integrity (verifying that every expected marker pair exists in the HTML files), and data schema validation (confirming the JSON files have the fields the embed script expects).</p>
<p>The structural tests are the most valuable. If someone accidentally deletes a marker comment while editing the HTML, the test suite catches it before the embed script silently skips that section. Both GitHub Actions workflows run the full test suite before fetching or embedding any data. If tests fail, the pipeline stops before touching anything.</p>
<h2><strong>Why not a framework?</strong></h2>
<p>I seriously considered Next.js or Astro. Both would have made the data integration more conventional. But I kept coming back to the same question: what does the framework actually give me here?</p>
<p>My site has no client-side interactivity beyond a hamburger menu. There's no routing (it's two pages). There's no state management. The "dynamic" parts update once a day, not on every page load.</p>
<p>A framework would add a build step, a <code>node_modules</code> folder, version upgrades to maintain, and a hosting dependency beyond static file serving. For a site that's fundamentally static content with periodic data refreshes, that's overhead in exchange for convenience I don't need.</p>
<p>The pipeline approach keeps the runtime dead simple (it's just files served by GitHub Pages) and moves the complexity into CI, where I have full Node.js, can test properly, and failures don't affect what's already deployed. If a cron job fails, the site still serves yesterday's data. If a Next.js build fails, you might have no site at all.</p>
<h2><strong>What I'd do differently</strong></h2>
<p><strong>Start with the data pipeline, not the design.</strong> I designed the site first and retrofitted the automation. It would have been cleaner to define the data shapes first and design the HTML around them. I ended up refactoring the marker positions multiple times as the data model evolved.</p>
<p><strong>The marker approach has limits.</strong> It works well for discrete sections, but if you needed the same data woven into multiple places (like a sidebar, a footer stat, and a header badge all showing the same PR count), you'd want a real templating engine. For my use case, each section is self-contained, so markers are the right tool.</p>
<p><strong>Seed your data first.</strong> The incremental update script assumes existing data exists. I wrote a separate <code>seed-merged-prs.mjs</code> that does a one-time full fetch to bootstrap the dataset. If you're adapting this pattern, plan for that bootstrapping step. Don't try to make your daily update script also handle the initial load.</p>
<h2><strong>How to build your own</strong></h2>
<p>If you want to try this approach, here's the pattern:</p>
<ol>
<li><p><strong>A</strong> <code>data/</code> <strong>directory</strong> with one JSON file per data source. This is your single source of truth.</p>
</li>
<li><p><strong>Fetch scripts</strong> (one per data source) that pull from APIs and write JSON. Make them incremental if you're near any API limits.</p>
</li>
<li><p><strong>An embed script</strong> with the <code>replaceBetweenMarkers</code> pattern. Every external string goes through <code>escapeHtml()</code>.</p>
</li>
<li><p><strong>Comment markers</strong> in your HTML wherever you want dynamic content.</p>
</li>
<li><p><strong>GitHub Actions</strong> with cron triggers. Run tests first. Only commit if something changed.</p>
</li>
<li><p><strong>Tests</strong> that validate your pure functions, data schemas, and marker integrity.</p>
</li>
<li><p><strong>A webhook relay</strong> if any of your data sources support webhooks, with a cron fallback because webhooks aren't reliable.</p>
</li>
</ol>
<p>The whole thing is <a href="https://github.com/costajohnt/costajohnt.github.io">open source</a>. Fork it, rip out my content, keep the pipeline.</p>
<p>The site is at <a href="http://jcosta.tech">jcosta.tech</a>. It looks like a simple static page. Under the hood, it's quietly keeping itself current. That's the whole point. The best infrastructure is the kind nobody notices.</p>
]]></content:encoded></item><item><title><![CDATA[Claude Code Tips and Tricks ]]></title><description><![CDATA[Plan First, Code Second
The single most impactful habit is planning before implementation.
Use Plan Mode
Press Shift+Tab twice to enter Plan Mode. This is where you should spend the most time.

Be spe]]></description><link>https://blog.jcosta.tech/claude-code-tips-and-tricks</link><guid isPermaLink="true">https://blog.jcosta.tech/claude-code-tips-and-tricks</guid><category><![CDATA[claude-code]]></category><category><![CDATA[engineering]]></category><category><![CDATA[AI-assisted development]]></category><category><![CDATA[Developer Tools]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[AI Coding Assistant]]></category><dc:creator><![CDATA[John Costa]]></dc:creator><pubDate>Sun, 08 Mar 2026 05:18:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/62fdd4ef1ebeafdc3d32f72e/c6d9dfdf-6bb6-4daa-86cf-42d51040ce3f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>Plan First, Code Second</strong></h2>
<p>The single most impactful habit is planning before implementation.</p>
<h3><strong>Use Plan Mode</strong></h3>
<p>Press <strong>Shift+Tab twice</strong> to enter Plan Mode. This is where you should spend the most time.</p>
<ul>
<li><p><strong>Be specific about the behavior, constraints, and edge cases you care about.</strong></p>
</li>
<li><p><strong>Instruct Claude to ask clarifying questions.</strong></p>
</li>
<li><p><strong>Read the plan carefully before approving and push back when you disagree.</strong></p>
</li>
</ul>
<h3><strong>Use Constraints to Keep Claude on Track</strong></h3>
<p>Being specific isn't just about what you say in prompts. It's about the structural constraints in your codebase. The more precisely you express your intent, the better the results.</p>
<ul>
<li><p><strong>Ask Claude to write tests.</strong></p>
</li>
<li><p><strong>Ask Claude to write documentation.</strong></p>
</li>
<li><p><strong>Use typed languages.</strong></p>
</li>
<li><p><strong>Use state machines.</strong></p>
</li>
</ul>
<h3><strong>Iterative Review Loops</strong></h3>
<p>Ask Claude to review its own code, fix issues, and review again, in a loop until no new issues are found.</p>
<blockquote>
<p>"Review this code. Fix any issues you find. Then review again. Keep going until you stop finding new problems."</p>
</blockquote>
<p>Each pass catches things the previous pass introduced or missed.</p>
<h2><strong>Set Up Your Project for Success</strong></h2>
<h3><strong>CLAUDE.md Files</strong></h3>
<p>Every project should have a <code>CLAUDE.md</code> file. It's Claude's persistent memory for your project: coding conventions, architecture decisions, common pitfalls.</p>
<ul>
<li><p><strong>Start with</strong> <code>/init</code><strong>.</strong> This generates an initial <code>CLAUDE.md</code> by analyzing your codebase.</p>
</li>
<li><p><strong>Update it when Claude makes a mistake.</strong> Wrong naming convention? Note it. Misunderstood a pattern? Note it.</p>
</li>
<li><p><strong>The better your</strong> <code>CLAUDE.md</code><strong>, the less time you spend correcting Claude.</strong></p>
</li>
</ul>
<p>Keep each file under 200 lines. If it gets longer, use sub-directory <code>CLAUDE.md</code> files to scope instructions. Claude loads the relevant ones automatically.</p>
<h3><strong>Commit Often</strong></h3>
<p>Commit after every completed task. Clean rollback point if Claude goes sideways, and it makes parallel work with sub-agents and worktrees easier.</p>
<h2><strong>Calibrate Your Level of Oversight</strong></h2>
<p>These models are getting better all the time. Push them, but calibrate how much autonomy you give Claude based on the stakes.</p>
<h3><strong>Tier 1: Maximum Autonomy. Private Experiments.</strong></h3>
<p>Personal projects, private repos, exploratory prototyping.</p>
<p>Longest leash. Let Claude work autonomously. Test the output, try things, skip detailed manual code review.</p>
<h3><strong>Tier 2: Moderate Oversight. Public Personal Work.</strong></h3>
<p>Public repos, open source contributions, side projects where your reputation is visible.</p>
<p>Claude still does most of the work. You may not review every line, but make sure things are tested before they go out.</p>
<h3><strong>Tier 3: Full Scrutiny. Professional Work.</strong></h3>
<p>Anything at your job, production systems, team codebases.</p>
<p>Shortest leash. Work slower. Work in smaller pieces. Fully review all code. Fully test before shipping.</p>
<p><strong>Watch what Claude is generating in real time.</strong> If it can't solve a problem cleanly, it will sometimes take a shortcut. It will say things like "let's just do this for now and come back to it later," or silently switch to a different approach.</p>
<p>When that happens, press <strong>Escape twice</strong> to interrupt and discuss. Don't let Claude wander.</p>
<p><strong>In all three tiers, Claude writes the code.</strong> What changes is the level of review, testing, and oversight. As models improve, gradually extend the leash.</p>
<h2><strong>Manage Your Context Window</strong></h2>
<p>Context rot is real. As your context window fills up, quality degrades. This matters most at Tier 3.</p>
<h3><strong>Monitor Your Context Usage</strong></h3>
<p>Ask Claude to set up a <strong>status line</strong> that shows your context usage in the bottom left of the terminal, color-coded green, yellow, and red. Green means plenty of room. Yellow means start wrapping up. Red means quality is about to degrade.</p>
<h3><strong>Clear Sessions, Not Your Progress</strong></h3>
<p>When your context window is filling up but you're mid-task, don't power through. Tell Claude: "I need to clear this session, but I want to continue where we left off. Set that up for me." Claude will write to its memory or generate a handoff prompt. Then run <code>/clear</code>, paste the prompt, and keep going with a fresh context window.</p>
<h2><strong>Built-in Features Worth Using</strong></h2>
<h3><strong>Sub-Agents and Agent Teams</strong></h3>
<p>Claude can spawn sub-agents. Separate Claude instances that work on tasks independently and report back. <strong>Use them liberally.</strong></p>
<p>They parallelize independent tasks. They specialize, so one agent researches while another implements and another reviews. They also protect your context window. Only the summary comes back.</p>
<p>Once you're comfortable with sub-agents, the next step is <strong>git worktrees</strong>. If you're running multiple Claude sessions on different features, they'll step on each other in the same working directory. Worktrees give each session its own isolated copy of the repo.</p>
<h3><strong>Hooks</strong></h3>
<p>Hooks let you run custom commands at specific points in Claude's workflow. Before a tool executes, after it completes, when a session starts, when Claude finishes responding.</p>
<ul>
<li><p><strong>Linting and formatting.</strong> Auto-run Prettier or ESLint after Claude writes code.</p>
</li>
<li><p><strong>Safety guardrails.</strong> Block dangerous commands like <code>rm -rf</code> before they execute.</p>
</li>
<li><p><strong>Custom validation.</strong> Run your own checks before Claude commits or deploys.</p>
</li>
</ul>
<h3><strong>Turn Repeated Tasks into Skills</strong></h3>
<p>If you find yourself asking Claude to do the same thing more than twice, ask it to create a <strong>skill</strong> instead.</p>
<blockquote>
<p>"I keep asking you to do X. Create a skill for this so I can just invoke it next time."</p>
</blockquote>
<h3><strong>Stay Up to Date</strong></h3>
<p>Claude Code ships updates frequently. Some are workflow-changing. Make it a habit to update and explore what's new. Features like <code>/voice</code>, <code>/remote-control</code> and agent teams were recent additions that changed how I work.</p>
<h2><strong>Plugins: Start Official, Then Expand</strong></h2>
<p>Start with the official plugins, then add third-party as needed.</p>
<h3><strong>Official Plugins</strong></h3>
<ul>
<li><p><strong>Code Simplifier</strong> simplifies and refines code for clarity and maintainability after you write it. Catches over-engineering.</p>
</li>
<li><p><strong>PR Review Toolkit</strong> is a suite of specialized review agents. Code reviewer, silent failure hunter, test analyzer, comment analyzer. Run these before committing.</p>
</li>
</ul>
<h3><strong>Third-Party Plugins</strong></h3>
<ul>
<li><p><strong>Context7</strong> fetches up-to-date documentation for external libraries. Instead of Claude relying on potentially outdated training data, Context7 pulls the actual current docs.</p>
</li>
<li><p><strong>Serena MCP Server</strong> provides semantic code search and navigation. It understands symbols, references, and code structure rather than just text.</p>
</li>
</ul>
<h3><strong>CLI Tools vs. MCP Servers</strong></h3>
<p><strong>Prefer CLI tools over MCP servers when both options exist.</strong> CLI tools are simpler and use less context. Use MCP servers when they offer capabilities CLI tools can't, like Serena's semantic code understanding. For things like Playwright and GitHub, use the CLI.</p>
<h2><strong>External Tools That Complement Claude Code</strong></h2>
<h3><strong>Obsidian as a Dev Brain</strong></h3>
<p><a href="https://obsidian.md/">Obsidian</a> is a markdown-based knowledge management tool. Ask Claude to set up an Obsidian vault as a "dev brain" for your work. It gives you structured, browsable project context across sessions and a place to track decisions and maintain living architecture docs. Useful for managing multiple projects or work that spans many sessions.</p>
<h3><strong>Wispr Flow</strong></h3>
<p><a href="https://wisprflow.com/">Wispr Flow</a> is a voice-to-text tool. Instead of typing long prompts, just talk. Useful for describing complex requirements naturally and staying in flow without switching to a keyboard.</p>
<h2><strong>Principles</strong></h2>
<ol>
<li><p><strong>Plan first, always.</strong> Time spent in Plan Mode is almost never wasted.</p>
</li>
<li><p><strong>Teach Claude, don't just correct it.</strong> Every mistake is an opportunity to update <code>CLAUDE.md</code>.</p>
</li>
<li><p><strong>Constrain with structure.</strong> Tests, types, docs, and state machines keep Claude focused.</p>
</li>
<li><p><strong>Iterate to convergence.</strong> Don't settle for a single review pass. Loop until stable.</p>
</li>
<li><p><strong>Protect your context window.</strong> Clear often, delegate research to sub-agents, monitor usage.</p>
</li>
<li><p><strong>Match scrutiny to stakes.</strong> More rope on experiments, less on professional work.</p>
</li>
</ol>
<p>These practices compound. The more of them you adopt, the more you can trust Claude to do. The faster you move.</p>
]]></content:encoded></item><item><title><![CDATA[The Biggest Bottleneck in Enterprise Software Isn't Technical]]></title><description><![CDATA[Enterprise projects don't move slowly because the engineering is hard. They move slowly because of coordination.
You know the pattern. You're building a feature that touches three systems. You own one]]></description><link>https://blog.jcosta.tech/the-coordination-tax</link><guid isPermaLink="true">https://blog.jcosta.tech/the-coordination-tax</guid><category><![CDATA[AI]]></category><category><![CDATA[software development]]></category><category><![CDATA[Enterprise AI]]></category><category><![CDATA[engineering-management]]></category><category><![CDATA[Developer]]></category><dc:creator><![CDATA[John Costa]]></dc:creator><pubDate>Thu, 05 Mar 2026 18:04:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/62fdd4ef1ebeafdc3d32f72e/a4d41c21-b62b-43d7-8e0d-c4edc82536d3.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Enterprise projects don't move slowly because the engineering is hard. They move slowly because of coordination.</p>
<p>You know the pattern. You're building a feature that touches three systems. You own one of them. The other two are managed by developers with their own priorities, their own sprints, their own backlogs. So you schedule a meeting. You explain what you need. They agree it's important but can't get to it for two weeks. You context-switch to something else. Two weeks later, that piece is done but now you need infrastructure changes, and that developer is mid-sprint on something else. Another meeting. Another wait.</p>
<p>The engineering work might take two weeks. The coordination adds months.</p>
<p>I think of it as the coordination tax. Every cross-functional project pays it. And for most enterprise teams, it's the single biggest reason features take as long as they do. Not complexity. Not technical debt. Coordination.</p>
<p>I'm in the middle of a project right now that would normally be drowning in it.</p>
<h2><strong>The System</strong></h2>
<p>I work at a B2B company that sells professional development content to organizations. We have hundreds of products in the catalog, and our clients serve them to their employees.</p>
<p>The sales cycle is long. Salespeople get on calls with prospects, ask about their needs, and figure out which products are the right fit. If we close the deal, it gets handed off to our client success team. Client success picks it up and naturally ends up covering a lot of the same ground, because the context from sales doesn't transfer cleanly. From there, either they manually set up the client's account, or they walk the client through our self-service tooling. Either way, there's a lot of repeated work and manual setup before the client is live.</p>
<p>I'm building a system to automate most of this.</p>
<p>We already use Gong to record sales calls, and those call summaries are stored in Salesforce. Every time a new call comes in, a Salesforce Flow identifies all the calls linked to that opportunity and sends them to an AWS Lambda function through an API Gateway. The Lambda function is a Python service that calls out to an LLM. It has access to our complete product catalog and metadata. The model analyzes the call summaries against the catalog, understands what the prospect actually needs, and generates a set of product recommendations. Those recommendations get written back to a custom object in Salesforce.</p>
<p>Every new call refines the recommendations. The system gets smarter about what the client needs as the sales process progresses. When the opportunity is marked closed-won, the recommendations are finalized.</p>
<p>From there, Salesforce syncs with our Rails monolith. The platform automatically creates a new organization for the client, builds a product package from the finalized recommendations, and grants access. Client success no longer needs to start from scratch. Instead of rebuilding context from the sales cycle and manually assembling everything, there's a tailored setup already waiting. The client will still customize things over time, but they're up and running on day one.</p>
<p>That's the system.</p>
<h2><strong>The Wall</strong></h2>
<p>I'm a full-stack developer. I work primarily in Rails and React with TypeScript. Python isn't even my main language, but I've been deploying production Python to Lambda functions for the past year with the help of AI coding tools. I can build the sync logic. I can handle the front-end work.</p>
<p>But this project also requires creating custom objects and triggers in Salesforce. I've never worked in Salesforce before. And it requires Terraform to stand up the infrastructure: a Docker container for the Lambda function, the API Gateway, all the AWS plumbing. I've worked with Docker, I've deployed Lambda functions, I've used API Gateway. But I've never written the Terraform to provision it all.</p>
<p>Normally, this means coordination. I'd ask our Salesforce developer to create the custom objects I need. He'd say yes, but he's in the middle of his own work. We'd need to loop in leadership to prioritize it. Same story with our platform engineer for the Terraform work. Meetings. Slack threads. Prioritization discussions. Context-switching on both sides.</p>
<p>I know what I need built. I understand the requirements completely. But I can't move forward because the work lives in someone else's domain.</p>
<p>This is the coordination tax.</p>
<h2><strong>What Changed</strong></h2>
<p>The Salesforce developer couldn't get to my work yet. So I got access to a Salesforce sandbox, opened Claude Code, and did it myself.</p>
<p>The work wasn't conceptually hard. I needed a custom object to store product recommendations. I understood the data model. I understood what fields it needed and how it related to the opportunity. I just hadn't worked in Salesforce's environment before. The barrier wasn't competence. It was familiarity. AI closed that gap in an afternoon.</p>
<p>Now I'm doing the same with the Terraform. I understand the infrastructure I need. I've worked with every component individually. I just haven't written the Terraform to wire it all together. Same pattern: I know what I need, I understand the architecture, and AI helps me work productively in a tool I haven't used before.</p>
<p>I'm not claiming to be a Salesforce developer or a Terraform expert. But adding a custom object to Salesforce isn't rocket science. Writing Terraform for a Lambda function behind an API Gateway isn't building Kubernetes clusters. These are straightforward tasks in unfamiliar tools. The kind of tasks that would normally require weeks of coordination but only hours of actual work.</p>
<h2><strong>The Bigger Picture</strong></h2>
<p>The coordination tax is one of the main reasons enterprise software moves slowly. When a single developer can work across stack boundaries, you don't just save that developer's time. You eliminate the meetings. You eliminate the prioritization discussions. You eliminate the context-switching for everyone involved. The Salesforce developer stays focused on his priorities. The platform engineer stays focused on hers. And the feature ships anyway.</p>
<p>This favors a certain kind of developer. Generalists who think in systems. Developers who are product-minded, who understand the full pipeline from sales call to user experience, and who are willing to step into unfamiliar territory. AI doesn't make you an expert in every tool. But it makes you productive enough in unfamiliar tools that you can stop waiting and start building.</p>
<p>The bottleneck in most organizations was never the engineering. It was the coordination. And that bottleneck is starting to disappear.</p>
]]></content:encoded></item><item><title><![CDATA[Why Functional Programming Is the Most Important Skill for the AI Era]]></title><description><![CDATA[Boris Cherny, the creator of Claude Code at Anthropic, recently appeared on Lenny's Podcast and said something that stopped me in my tracks. When asked whether coding skills still matter, he was unequ]]></description><link>https://blog.jcosta.tech/why-functional-programming-is-the-most-important-skill-for-the-ai-era</link><guid isPermaLink="true">https://blog.jcosta.tech/why-functional-programming-is-the-most-important-skill-for-the-ai-era</guid><category><![CDATA[Functional Programming]]></category><category><![CDATA[AI]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[claude-code]]></category><category><![CDATA[#Domain-Driven-Design]]></category><dc:creator><![CDATA[John Costa]]></dc:creator><pubDate>Tue, 24 Feb 2026 14:46:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/62fdd4ef1ebeafdc3d32f72e/60ed79c4-75d0-4c9b-92f3-582c40c3a050.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Boris Cherny, the creator of Claude Code at Anthropic, recently appeared on <a href="https://www.lennysnewsletter.com/p/head-of-claude-code-what-happens">Lenny's Podcast</a> and said something that stopped me in my tracks. When asked whether coding skills still matter, he was unequivocal: coding is "solved." Claude Code writes 100% of his code now. He doesn't miss the manual work, and he doesn't care if those skills atrophy.</p>
<p>I don't either. And I think Boris and I arrived at that conclusion through remarkably similar paths.</p>
<p>Boris studied economics, not computer science. He dropped out to start startups at 18. He got into coding because he wanted to build things, not because he loved the act of writing code itself. I came from philosophy. My first lines of code were JavaScript macros in Photoshop, automating the tedious parts of product photography. Neither of us set out to become software engineers. We set out to solve problems, and code was the tool we reached for.</p>
<p>Along the way, we both fell in love with functional programming. Boris discovered it after a motorcycle accident broke both his arms. He needed languages with fewer keystrokes, which led him from CoffeeScript to Haskell to Scala to TypeScript. He calls <em>Functional Programming in Scala</em> the most important technical book of his career. For me, the book that changed everything was <em>Domain Modeling Made Functional</em> by Scott Wlaschin. I came from an object-oriented background and genuinely didn't know another paradigm existed. When functional programming finally clicked, it fundamentally rewired how I think about building software.</p>
<p>Here's why that matters right now: the specific mental models that functional programming teaches you are exactly the skills you need to build effectively with AI. Not because AI writes functional code. It mostly doesn't. But because directing AI is itself a functional act. There are three reasons why.</p>
<h2>1. Strong Types Communicate Intent</h2>
<p>Boris says something in his Peterman Pod interview that I think is quietly profound: "I think in types when I code. The type signatures are more important than the code itself."</p>
<p>This is the first pillar. When you design a system by writing your types first, you're building a contract. You're defining what's possible, what's impossible, and what the boundaries of behavior look like. A well-designed type system is a set of constraints that eliminates entire categories of invalid states before a single line of implementation is written.</p>
<p>AI responds incredibly well to this. When you hand an AI a codebase with strong, expressive types, including well-defined state machines expressed through those types, you're giving it clear guardrails. The types communicate your intent in a way that's unambiguous. The AI doesn't have to guess what you want because the type system already constrains the universe of valid outcomes.</p>
<p>Think about it from the AI's perspective. If it's generating code in a loosely typed or untyped environment, it has enormous degrees of freedom. That means enormous potential to produce something that technically works but doesn't match your intent. Strong types collapse that space. They're a forcing function toward correctness.</p>
<p>This is why "thinking in types" isn't just a nice engineering practice anymore. It's becoming a prerequisite for effective AI-directed development. When you set up your type system and your state machines first, you're not just designing your software. You're designing the constraints that will guide the AI toward the right implementation.</p>
<h2>2. Declarative Thinking Describes Outcomes, Not Instructions</h2>
<p>The second reason functional programming prepares you for the AI era is more fundamental: functional programming is primarily declarative.</p>
<p>Consider SQL. When you write a SQL query, you don't tell the database engine how to search through the data. You don't specify which index to use, in what order to traverse the rows, or how to join the tables internally. You describe the outcome you want. Give me all records where this condition is true, grouped by this field, sorted by that one. The database engine figures out how to get there.</p>
<p>The underlying system is free to change and improve. The database might be rewritten tomorrow. None of that breaks your query, because your query never specified the <em>how</em>. It only specified the <em>what</em>.</p>
<p>Functional programming works the same way. When you compose pure functions, when you map and filter and reduce, you're describing transformations, not step-by-step procedures. You're saying "take this data and produce this shape" rather than "first do this, then check that, then loop through these."</p>
<p>Now apply this to how we work with AI. When you prompt an AI tool to build something, you are, at its core, performing a declarative act. You're describing the outcome you want. You're telling the AI <em>what</em> you need, and letting it figure out <em>how</em> to get there.</p>
<p>If you come from an imperative, object-oriented background, your instinct is to think in step-by-step instructions. You want to tell the computer exactly what to do and in what order. That instinct works against you when directing AI, because the AI might know a better way to reach the outcome. And as models improve, the <em>how</em> gets better. But only if you've left room for it. When you over-specify the implementation, you're constraining the AI in ways that will become increasingly counterproductive as the tools evolve.</p>
<p>Boris makes this point explicitly: build for the model six months from now, not the model you have today. Declarative specs are how you do that. The <em>what</em> you need doesn't change. The AI's ability to figure out the <em>how</em> only gets better.</p>
<p>Functional programmers have been training this muscle for years. We already think in terms of inputs, transformations, and outputs. We already describe outcomes rather than procedures. That's exactly the skill that AI-directed development demands.</p>
<h2>3. Domain Modeling Is the Real Skill</h2>
<p>The third piece is the one I think is most underappreciated, and it's the one that ties the whole argument together.</p>
<p><em>Domain Modeling Made Functional</em> taught me that code should model the domain so faithfully that a domain expert, the person the software is actually built for, should be able to read the code and understand what it means. The code and the business logic shouldn't speak different languages. Your types should map to real-world concepts. Your functions should describe real-world operations. If the domain expert says "a customer places an order" and your code says <code>processTransaction(entityRef, ctx)</code>, something has gone wrong.</p>
<p>This principle has always been good engineering practice. But now it's becoming essential for a different reason: English is becoming the primary programming language.</p>
<p>When you're working with AI, you're describing what you want to build in natural language. You're explaining the problem, the constraints, the user's needs, the desired behavior. You're doing domain modeling. In English. If you've spent years training yourself to think about software from the domain expert's perspective, to understand the problem space deeply and express it clearly, you're already fluent in the most important language of AI-directed development.</p>
<p>The engineers who struggle with AI tools are often the ones who think about code in terms of code. In terms of patterns, abstractions, and implementation details. The engineers who thrive are the ones who think about code in terms of the problem it solves. They can articulate what the software should do because they deeply understand <em>why</em> it exists and <em>who</em> it's for.</p>
<p>Boris talks about this instinct on the Lenny's Podcast episode. He describes "latent demand" as the most important principle in product. You can't get people to do something they don't already want to do. You find the intent they already have and build around it. That's product thinking, but it's also domain modeling. It's understanding the problem before you write a single line of code.</p>
<h2>The Punchline</h2>
<p>Types define the boundaries, declarative thinking describes the outcomes, and domain modeling ensures you're solving the right problem. None of these are about writing code. They're about thinking clearly, and then letting the AI do the writing.</p>
<p>Boris and I both came to engineering from liberal arts backgrounds. We both fell in love with functional programming. We both arrived at the same conclusion: the act of coding is being commoditized. But the skills that functional programming taught us are becoming more valuable, not less.</p>
<p>If you're an engineer wondering which skills to invest in right now, my advice is counterintuitive: don't practice writing code faster. Practice thinking about problems more clearly. The engineers who will thrive in the AI era aren't the ones who can write the most code. They're the ones who can think the most clearly about what needs to be built, and then let the machines do the building.</p>
<p>Shout out to Ryan Bell for introducing me to functional programming. It changed everything.</p>
<hr />
<h2>Resources</h2>
<ul>
<li><p><a href="https://www.lennysnewsletter.com/p/head-of-claude-code-what-happens">Lenny's Podcast: "Head of Claude Code: What happens after coding is solved | Boris Cherny"</a> (Feb 19, 2026)</p>
</li>
<li><p><a href="https://www.developing.dev/p/boris-cherny-creator-of-claude-code">The Peterman Pod: "Boris Cherny (Creator of Claude Code) On How His Career Grew"</a> (Dec 15, 2025)</p>
</li>
<li><p><em>Functional Programming in Scala</em> by Paul Chiusano and Runar Bjarnason</p>
</li>
<li><p><em>Programming TypeScript</em> by Boris Cherny</p>
</li>
<li><p><em>Domain Modeling Made Functional</em> by Scott Wlaschin</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[How to Have a Career in 2026]]></title><description><![CDATA[I've been evangelizing AI across our technology team for a while now. Recently, it's started to spill over into the rest of the organization. Colleagues and friends outside of engineering are asking me how they can use AI in their daily work, how the...]]></description><link>https://blog.jcosta.tech/how-to-have-a-career-in-2026</link><guid isPermaLink="true">https://blog.jcosta.tech/how-to-have-a-career-in-2026</guid><category><![CDATA[AI]]></category><category><![CDATA[Career Growth]]></category><category><![CDATA[Futureofwork]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[claude.ai]]></category><dc:creator><![CDATA[John Costa]]></dc:creator><pubDate>Sat, 14 Feb 2026 23:26:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1771112165436/f6d5f83c-045c-47e4-aa94-effe757b5f2a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I've been evangelizing AI across our technology team for a while now. Recently, it's started to spill over into the rest of the organization. Colleagues and friends outside of engineering are asking me how they can use AI in their daily work, how they can automate parts of their workflow, and how they can do things faster and better.</p>
<p>Here's the thing. When I get these questions, all I'm doing is feeding them back into Claude to help me generate responses. I don't have deep knowledge of sales, finance, or creative workflows. But Claude does. That's the whole point.</p>
<p>So instead of trying to give you function-specific advice, I'm going to teach you the process. This is a simple step-by-step guide for getting started with AI and figuring out how to leverage it in your job. This is exactly what I would do if I was starting from scratch.</p>
<h2 id="heading-step-1-invest-in-compute">Step 1: Invest in Compute</h2>
<p>You need access to a capable AI model. For me, that means Claude from Anthropic. Here's what I'd do:</p>
<ul>
<li>Go to <a target="_blank" href="https://claude.ai">claude.ai</a> and create an account</li>
<li>Download <a target="_blank" href="https://claude.ai/download">Claude Desktop</a></li>
</ul>
<p>Once you have Claude Desktop, set up <a target="_blank" href="https://claude.com/connectors">Connectors</a>. This is critical. Connectors let Claude access, search, and take actions inside the tools you already use for work. Anthropic has a directory of 50+ integrations, including Slack, Gmail, Google Drive, Notion, Asana, Figma, Canva, and many more. If your workflow lives in Google Suite, Slack, or any common productivity tool, there's likely a connector for it.</p>
<p>This is what makes Claude more than a chatbot. With connectors, it can read your documents, search your messages, pull data from your project management tools, and actually do things inside those tools on your behalf. All without leaving the Claude window.</p>
<h2 id="heading-step-2-let-claude-set-itself-up">Step 2: Let Claude Set Itself Up</h2>
<p>I'm not going to get into the exact specifics of how to configure everything. You know why? Because you're going to ask Claude how to do it. Not only can you ask Claude how to set things up, you're going to ask Claude to set things up for you.</p>
<p>This is an important habit to build early. Sometimes the AI will give you instructions for doing something manually when it could just do it for you. Ask it to try. It will sometimes say that it can't do something. Ask it to make sure, because it tends to undersell what it's actually capable of. You'd be surprised.</p>
<h2 id="heading-step-3-explain-your-job-in-extreme-detail">Step 3: Explain Your Job in Extreme Detail</h2>
<p>This is the most important step, and most people skip it or rush through it.</p>
<p>You need to long-form explain exactly what you do in your job function. Go into extreme detail. There is no detail too trivial. Try not to make assumptions about what the AI already knows.</p>
<p>Cover everything:</p>
<ul>
<li>What tools do you use?</li>
<li>What does your day-to-day look like?</li>
<li>What are your recurring tasks?</li>
<li>Who do you work with?</li>
<li>What are your inputs and outputs?</li>
<li>What takes you the most time?</li>
<li>What's tedious and repetitive?</li>
<li>What requires the most thought and judgment?</li>
</ul>
<p>You don't need to know what you want to automate yet. This step is purely about giving the AI a complete picture of your work. Tell it to ask you clarifying questions about anything that's unclear. Keep going until it has a thorough understanding of your role.</p>
<p>I can't emphasize this enough. The quality of everything that follows depends on how well you do this step.</p>
<p><strong>Pro tip:</strong> Use a speech-to-text tool for this. I use <a target="_blank" href="https://www.wispr.flow">Wispr Flow</a>. It lets you input three to five times as much information as you could by typing. When you're trying to dump your entire job description and daily workflow into the AI, speaking is dramatically faster.</p>
<h2 id="heading-step-4-ask-how-to-leverage-ai">Step 4: Ask How to Leverage AI</h2>
<p>Only once the AI has a solid understanding of your job function do you start asking:</p>
<ul>
<li>How can I leverage AI to make my life easier?</li>
<li>Which of my processes could be automated?</li>
<li>How can I be more productive?</li>
<li>How can I increase the quality of my output?</li>
<li>How can I ship more value in my function?</li>
</ul>
<p>It will give you ideas. Then you keep iterating and ideating. Ask follow-up questions. Go deeper on the ideas that interest you. There's nothing you can't at least try getting the AI to do.</p>
<p>When you're set up with Claude Desktop and the connectors you've configured, the AI has access to your file system, can search the internet, and can work directly inside your tools. It can do an incredible amount of work for you. It can figure out how to automate your workflows and how you can best leverage it.</p>
<h2 id="heading-tools-i-recommend">Tools I Recommend</h2>
<p><strong><a target="_blank" href="https://claude.ai">Claude</a></strong> for the AI itself. It's the best model available right now.</p>
<p><strong><a target="_blank" href="https://www.wispr.flow">Wispr Flow</a></strong> for speech-to-text. Speaking is the fastest way to get large amounts of context into the AI.</p>
<p><strong><a target="_blank" href="https://obsidian.md">Obsidian</a></strong> for note-taking. It stores everything in Markdown format, which is easy for you to read and, more importantly, easy for the AI to search and read as well. If you're going to be working with AI regularly, keeping your notes in a format the AI can consume is a force multiplier.</p>
<h2 id="heading-software-engineers-are-the-canary-in-the-coal-mine">Software Engineers Are the Canary in the Coal Mine</h2>
<p>I want to be direct about something.</p>
<p>Software engineers are the early adopters of AI. We've been leveraging these tools the most because AI development has been focused on automating software development first. It's highly technical, and we're the ones using AI to build AI. We've had a head start.</p>
<p>But here's the reality. If you have a desk job and you think your job is safe from AI, you're wrong. Your job is likely less technical than software engineering. Once AI gets specifically targeted at your function, it can be automated faster than you think. I'm not trying to be provocative. That's just the truth.</p>
<p>Matt Shumer's recent essay <a target="_blank" href="https://shumer.dev/something-big-is-happening">"Something Big Is Happening"</a> puts it plainly: "If your job happens on a screen, AI is coming for significant parts of it." He compares this moment to early February 2020, when most people dismissed warnings about COVID. The people paying attention acted early. The rest were caught off guard.</p>
<p>In order to stay ahead, you need to figure out how to increase your productivity and ship more value, better and faster. The steps above are how you start.</p>
<h2 id="heading-resources">Resources</h2>
<ul>
<li><a target="_blank" href="https://shumer.dev/something-big-is-happening">Something Big Is Happening</a> by Matt Shumer. The viral essay on why AI is about to change everything for knowledge workers.</li>
<li><a target="_blank" href="https://www.interconnects.ai/p/opus-46-vs-codex-53">Opus 4.6, Codex 5.3, and the Post-Benchmark Era</a> by Nathan Lambert. A technical comparison of the latest models from Anthropic and OpenAI.</li>
<li><a target="_blank" href="https://claude.ai/download">Claude Desktop</a>. Download and get started.</li>
<li><a target="_blank" href="https://www.wispr.flow">Wispr Flow</a>. Speech-to-text that actually works.</li>
<li><a target="_blank" href="https://obsidian.md">Obsidian</a>. Markdown-based note-taking.</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[What It Takes to Be a Software Engineer in 2026]]></title><description><![CDATA[It's no longer enough to be an individual contributor who pulls cards off the board, completes tasks, raises PRs, and merges them. AI has made implementation easier. What once took days now takes hours.
This means implementation is becoming commoditi...]]></description><link>https://blog.jcosta.tech/what-it-takes-to-be-a-software-engineer-in-2026</link><guid isPermaLink="true">https://blog.jcosta.tech/what-it-takes-to-be-a-software-engineer-in-2026</guid><category><![CDATA[Software Engineering]]></category><category><![CDATA[AI]]></category><category><![CDATA[agentic AI]]></category><category><![CDATA[tech careers]]></category><category><![CDATA[Future of work]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[engineering]]></category><dc:creator><![CDATA[John Costa]]></dc:creator><pubDate>Tue, 27 Jan 2026 16:17:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769530996481/51712556-a00a-477d-8504-cd282af98e3f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It's no longer enough to be an individual contributor who pulls cards off the board, completes tasks, raises PRs, and merges them. AI has made implementation easier. What once took days now takes hours.</p>
<p>This means implementation is becoming commoditized. To stay valuable, you need to focus on everything else. And you can't wait to be asked.</p>
<p>Here's what I'm doing.</p>
<h2 id="heading-1-use-and-evangelize-ai">1. Use and Evangelize AI</h2>
<p>Be the person who's forward-thinking and actually using these tools. Show your tech team and the broader organization what's possible. Lead others in adopting AI to streamline work and bring more value to the business.</p>
<p>Being the AI guy wasn't in my job description. I just started using the tools, talking about them, and showing people what was possible. Recently I presented at a company all-hands about Agentic AI. The productivity gains and value we're shipping are starting to get noticed at the C-suite level.</p>
<p>You get hands-on experience with what works. And as your colleagues start adopting these tools, they see you as the expert. That makes you more valuable.</p>
<h2 id="heading-2-become-a-generalist">2. Become a Generalist</h2>
<p>Specialization made sense when implementation was the bottleneck. If building anything took significant effort, you needed deep expertise just to be productive.</p>
<p>That's changed. I'm no longer just working on the core web app. I'm reaching into DevOps, writing Terraform. I'm looking at QA processes, evaluating whether Playwright could replace manual tools like Ghost Inspector.</p>
<p>The breadth of what a single engineer can contribute to has expanded. Take advantage of it.</p>
<h2 id="heading-3-automate-more">3. Automate More</h2>
<p>We've always automated the core application. But engineer time was expensive, so the cost-benefit analysis rarely justified going beyond that.</p>
<p>The value proposition is different now. It makes sense to apply engineering effort to places we used to ignore.</p>
<p><strong>Start by automating your own workflow.</strong> My interface is Claude Code. I manage my Jira cards and GitHub PRs through it. I respond to PR comments, address QA feedback, and incorporate PM input in real time.</p>
<p>Here's what this enables: when a PM finds an issue after delivery, I can address it immediately instead of creating a follow-up card for later. The implementation cycle is short enough now that this makes sense. The result is more complete features shipping faster.</p>
<p><strong>Then see what you can automate for your team.</strong> I automated our deploy process. It's now a simple <code>/deploy</code> command in Claude that finds the diff between main and production, posts to Slack showing the commits about to be released, and performs the deployment. The developer doesn't have to do any of it manually.</p>
<p>Another example: I saw QA asking in Slack to change the session timeout on staging. No interface for it, so they needed a developer to push a code change. Back and forth, waiting on availability, then again to revert it when done.</p>
<p>I built a feature that lets QA manage it themselves and posted to Slack asking if there was buy-in. There was. Now QA doesn't wait on anyone.</p>
<p><strong>Finally, work cross-functionally.</strong> Start looking at other teams in the company and what you can automate for them. This is where the huge value comes in. Developers adding automation across the business, not just within the tech team.</p>
<h2 id="heading-4-build-more-complete-features">4. Build More Complete Features</h2>
<p>Robust error handling. Full test coverage. Rate limiting. Extensive documentation.</p>
<p>These things weren't always worth the investment. Our team didn't prioritize a ton of tests. We definitely didn't prioritize documentation because there wasn't time to write it properly, let alone maintain it. We were applying the Pareto principle. Get 80% of the way there, accept diminishing returns after that.</p>
<p>That calculus has changed. These are trivial to add now, so add them. Tests and documentation also serve as guardrails for AI during implementation. They help ensure what gets built actually matches the requirements.</p>
<h2 id="heading-5-expand-your-product-knowledge">5. Expand Your Product Knowledge</h2>
<p>When you're not spending all your time on implementation, you have more cognitive bandwidth to focus on other parts of the software development life cycle. Use that bandwidth.</p>
<p>Stop relying on product managers to write detailed specs. Get comfortable operating with ambiguity. Focus on what the business really needs and how to ship the right value.</p>
<p>When you deeply understand the product domain, you make better judgment calls. You anticipate edge cases. You push back when requirements don't make sense. You become a partner in product development, not just an executor.</p>
<h2 id="heading-6-manage-more-work-in-progress">6. Manage More Work in Progress</h2>
<p>Before AI, it was smart to limit yourself to one or two code changes at a time. Context switching was expensive.</p>
<p>Now I have more changes in flight at once than I ever could before. AI tooling holds context for each workstream and helps me switch between tasks without losing momentum.</p>
<p>When you're waiting on code review or blocked on a dependency, you can pivot to something else without losing your place. The throughput increase is real.</p>
<h2 id="heading-the-point">The Point</h2>
<p>The engineers who thrive in 2026 won't be the ones who resist these tools or use them passively. They'll be the ones who rethink what it means to be an engineer when implementation is no longer the hard part.</p>
<p>You can't just be reactive, pulling cards off the board and completing them. Look at the business, find places to contribute.</p>
<p>These are the strategies I'm using. They're working.</p>
]]></content:encoded></item><item><title><![CDATA[AI Drives, You Direct]]></title><description><![CDATA[I've tried contributing to open source before. It's a lot of work, and when your PRs aren't getting traction, it's easy to give up.
Building oss-autopilot
I've written before about how AI-assisted development feels like engineering management. Direct...]]></description><link>https://blog.jcosta.tech/ai-drives-you-direct</link><guid isPermaLink="true">https://blog.jcosta.tech/ai-drives-you-direct</guid><category><![CDATA[AI]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[claude-code]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[Developer Tools]]></category><category><![CDATA[agentic AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[GitHub]]></category><dc:creator><![CDATA[John Costa]]></dc:creator><pubDate>Tue, 20 Jan 2026 01:14:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768870996369/e9b82e64-7f65-438e-9a59-3401bfa2831e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I've tried contributing to open source before. It's a lot of work, and when your PRs aren't getting traction, it's easy to give up.</p>
<h2 id="heading-building-oss-autopilot">Building oss-autopilot</h2>
<p>I've <a target="_blank" href="https://blog.jcosta.tech/why-ai-assisted-development-feels-like-engineering-management">written before</a> about how AI-assisted development feels like engineering management. Directing, reviewing, course correcting. That framing got me thinking: what if I could build tooling that handles the tedious parts of open source, and I just direct?</p>
<p>That's <a target="_blank" href="https://github.com/costajohnt/oss-autopilot">oss-autopilot</a>.</p>
<p>It tracks all my open PRs and fetches comments so I know when someone's waiting on me. It keeps a history of my contributions, which repos I've had success with, which ones have ignored me. When I'm looking for something new to work on, it searches for issues but filters them through that history. It steers me toward repos with active maintainers who've actually merged my stuff before.</p>
<p>When a maintainer leaves feedback on one of my PRs, it reads the comment and the relevant code and drafts a response. I review it, tweak it if needed, and approve it before anything gets posted. My reputation stays in my hands.</p>
<p>The whole thing runs through Claude Code. I type <code>/oss</code> and it goes to work. Checks my PRs, pulls in new comments, finds opportunities, drafts responses. I review what it surfaced and make decisions. That's it.</p>
<h2 id="heading-what-changed">What changed</h2>
<p>In the past few weeks I've had six PRs merged. I do most of this while playing Fortnite in the evenings.</p>
<p>The difference isn't that I got better at coding. It's that I stopped guessing and losing track of things. The tool remembers what I'd forget. I just make the calls.</p>
<h2 id="heading-the-agentic-shift">The agentic shift</h2>
<p>Tools like Claude Code are agentic now. They can take initiative, not just respond. They can check on things, fetch data, draft responses, keep track of state. That's a real capability shift.</p>
<p>The interface has changed too. You chat with Claude Code in plain English. Tell it what you want, and it goes off and does the work. That's it. No complex commands, no context switching between tools. Just conversation.</p>
<p>But most people still use AI the old way. You ask, it answers. You lead, it follows. That works, but it leaves a lot on the table.</p>
<p>The opportunity is building tooling that actually uses the agentic capabilities. Give Claude enough context and access to drive things forward on its own. Then you direct instead of do.</p>
<p>That's what oss-autopilot is for me. Yours might look different. But the principle is the same: if your AI can take initiative, build tooling that lets it.</p>
<hr />
<p><em><a target="_blank" href="https://github.com/costajohnt/oss-autopilot">oss-autopilot</a> is open source if you want to try it or build something similar.</em></p>
]]></content:encoded></item><item><title><![CDATA[Why AI-Assisted Development Feels Like Engineering Management]]></title><description><![CDATA[In 2021, I was grinding to hit a deadline. It was stressful. Multiple developers were working on the same parts of the codebase simultaneously, which meant constant merge conflicts that needed constant management. We took on a lot of tech debt and cu...]]></description><link>https://blog.jcosta.tech/why-ai-assisted-development-feels-like-engineering-management</link><guid isPermaLink="true">https://blog.jcosta.tech/why-ai-assisted-development-feels-like-engineering-management</guid><category><![CDATA[AI]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[Career]]></category><category><![CDATA[#ai-tools]]></category><category><![CDATA[engineering-management]]></category><category><![CDATA[code review]]></category><category><![CDATA[claude-code]]></category><category><![CDATA[llm]]></category><category><![CDATA[tech leadership]]></category><dc:creator><![CDATA[John Costa]]></dc:creator><pubDate>Tue, 06 Jan 2026 15:13:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767497971755/5a86f283-a861-498b-9448-f96d580b35cc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In 2021, I was grinding to hit a deadline. It was stressful. Multiple developers were working on the same parts of the codebase simultaneously, which meant constant merge conflicts that needed constant management. We took on a lot of tech debt and cut features just to ship. The result was a product no one was really happy with. The business was moving fast with limited developer resources, and we were on to the next project before we could clean up the mess.</p>
<p>I was unhappy with the quality of the work we were putting out despite the extra time and effort required to ship. I've always leaned toward reliability and maintainability. I understand the need to balance these tradeoffs based on business needs, but shipping janky code fast has never sat well with me. I was getting burned out on feature work and looking to pivot.</p>
<p>Luckily, I had a great manager who wanted to help. I joined our SRE subgroup and stepped away from features. One of the responsibilities of our small group was to handle all code review for the entire development team.</p>
<p>I spent the next two years diving into code review. I enforced standards, mentored developers, wrote code snippets to demonstrate what I was looking for, and paired when needed. I kept PRs moving because I didn't want the team blocked.</p>
<p>I started mentoring junior developers more formally and later took over leading a team of offshore developers working across the tech stack.</p>
<p>At this point it had been a while since I was really shipping my own code. I focused on planning, architecture, and delegation. I still personally reviewed code from about 30 developers. I was promoted to Associate Director of Engineering.</p>
<p>Soon after, the company (like many others at the time) began shrinking. Now the development team is quite small and I'm back in an IC role.</p>
<p>But things are different.</p>
<h2 id="heading-the-skills-transfer-directly">The Skills Transfer Directly</h2>
<p>I still do all the planning and high level architecture. But now instead of delegating to a team, I'm delegating to Claude.</p>
<p>The experience I gained focusing primarily on code review for years has become invaluable. I'm constantly reviewing AI generated code. There is a strong correlation between communicating requirements to junior and mid level developers and doing the same with an AI assistant. Clear requirements, well defined scope, and strong architectural guidance matter just as much.</p>
<p>By leveraging tools like git worktrees, I'm able to work on multiple tasks simultaneously. I'm not writing every line of code. I'm directing, reviewing, and course correcting. That's management.</p>
<h2 id="heading-the-point">The Point</h2>
<p>Real AI-driven development is more akin to a tech lead, staff engineer, or engineering manager role than traditional IC work. You're planning. You're reviewing. You're making architectural decisions. You're unblocking.</p>
<p>I used to think stepping away from code was a detour. Now it feels like preparation.</p>
]]></content:encoded></item><item><title><![CDATA[AI-Assisted Home Automation in 2026]]></title><description><![CDATA[Home automation is a lot of fun, but once it becomes a serious hobby, you start running into some challenging problems.
Device fragmentation, for one. Ring app for the alarm. Hue app for lights. Aqara app for the lock. SwitchBot app for curtains. My ...]]></description><link>https://blog.jcosta.tech/ai-assisted-home-automation-in-2026</link><guid isPermaLink="true">https://blog.jcosta.tech/ai-assisted-home-automation-in-2026</guid><category><![CDATA[Home Assistant]]></category><category><![CDATA[claude-code]]></category><category><![CDATA[claude.ai]]></category><category><![CDATA[proxmox]]></category><category><![CDATA[home automation]]></category><category><![CDATA[Software Engineering]]></category><dc:creator><![CDATA[John Costa]]></dc:creator><pubDate>Sat, 03 Jan 2026 19:10:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767467327244/e12d53b9-d246-482b-ae9e-ba4016830e9d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Home automation is a lot of fun, but once it becomes a serious hobby, you start running into some challenging problems.</p>
<p>Device fragmentation, for one. Ring app for the alarm. Hue app for lights. Aqara app for the lock. SwitchBot app for curtains. My wife's not going to download a dozen apps on her phone to control the house.</p>
<p>Then there's automation complexity. Most smart home platforms make simple things easy and complex things impossible. "Turn on lights at sunset" is fine. "Turn on lights at sunset, but only if someone's home, unless the alarm is armed, and also dim them if the TV is on" requires either a PhD in YAML or a tangled mess of node-red flows.</p>
<p>So here's my setup for 2026. I'll run through the hardware and services, but the part I really want to focus on is this: not editing YAML files by hand, not clicking through the Home Assistant UI, but building a strong connection between <a target="_blank" href="https://github.com/anthropics/claude-code">Claude Code</a> and Home Assistant so I can use natural language to build out my automations.</p>
<h2 id="heading-the-stack">The Stack</h2>
<h3 id="heading-hardware">Hardware</h3>
<ul>
<li><strong>Beelink Mini PC</strong> (~$200) - Low power, always-on server [1]</li>
<li><strong><a target="_blank" href="https://www.proxmox.com/en/">Proxmox</a></strong> - Free virtualization layer [2]</li>
<li><strong><a target="_blank" href="https://www.docker.com/">Docker</a></strong> - Container runtime inside LXC containers [3]</li>
</ul>
<h3 id="heading-services">Services</h3>
<p>Everything runs in Docker containers:</p>
<ul>
<li><strong><a target="_blank" href="https://www.home-assistant.io/">Home Assistant</a></strong> - Central hub for all devices</li>
<li><strong>Pi-hole</strong> - Network-wide ad blocking</li>
</ul>
<p>This is expandable. Jellyfin for media, Frigate for NVR, whatever you need.</p>
<h2 id="heading-the-key-part-claude-code-home-assistant">The Key Part: Claude Code + Home Assistant</h2>
<p>Here's where it gets interesting. Claude Code is Anthropic's CLI tool that gives Claude direct access to your filesystem and terminal. Point it at your home automation documentation, and it becomes an intelligent assistant that knows your setup.</p>
<h3 id="heading-documentation-folder">Documentation Folder</h3>
<p>Keep a local folder with markdown files that document your setup. This is the key to making Claude Code useful. Include:</p>
<ul>
<li><strong>Architecture notes</strong> - How your system is set up, what containers run where, common commands</li>
<li><strong>Project logs</strong> - What you've built, what broke, how you fixed it</li>
<li><strong>Config copies</strong> - Local copies of your configuration.yaml for reference</li>
</ul>
<p>This folder is context. When Claude Code reads these files, it stops being a generic assistant and becomes one that knows your specific setup.</p>
<h3 id="heading-connection-method">Connection Method</h3>
<p>Claude Code connects to Home Assistant through three methods, each serving a different purpose:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Method</td><td>What it's for</td></tr>
</thead>
<tbody>
<tr>
<td><strong>SSH</strong></td><td>File operations, container management</td></tr>
<tr>
<td><strong>REST API</strong></td><td>State queries, service calls</td></tr>
<tr>
<td><strong>WebSocket</strong></td><td>Registry operations</td></tr>
</tbody>
</table>
</div><p>In practice, SSH is the workhorse. Most of what we do is edit YAML, upload it, restart or reload. REST API is for quick checks and triggering actions. WebSocket is niche, only needed for certain registry operations like renaming entities.</p>
<p><strong>Setup:</strong></p>
<ol>
<li>SSH config entry pointing to your Home Assistant server with key-based auth</li>
<li>Long-lived access token created in Home Assistant UI (Profile → Long-lived access tokens)</li>
<li>Token stored in an environment variable (<code>HASS_TOKEN</code>) so Claude Code can use it</li>
</ol>
<p>How do you set this up? Ask Claude Code to do it. It can generate SSH keys, update your SSH config, and set up the environment variables.</p>
<p><strong>Bonus:</strong> Pair this with an AI voice-to-text tool [4] and you can just talk to Claude Code. Describe what you want out loud, it types for you, Claude Code does the rest.</p>
<h2 id="heading-the-payoff">The Payoff</h2>
<p>Instead of clicking through GUIs or memorizing YAML syntax, you describe what you want:</p>
<blockquote>
<p>"Create an automation that dims the kitchen lights when the Apple TV starts playing after sunset"</p>
</blockquote>
<p>Claude Code:</p>
<ol>
<li>Reads your configuration to understand existing entities</li>
<li>Writes the YAML automation</li>
<li>Uploads it to the server</li>
<li>Validates the config</li>
<li>Reloads Home Assistant</li>
</ol>
<p>When something breaks, you describe the problem:</p>
<blockquote>
<p>"The Ring sensors show unavailable"</p>
</blockquote>
<p>Claude Code investigates, finds that the ring-mqtt container stopped, restarts it, re-authenticates if needed, and documents the fix.</p>
<h2 id="heading-real-examples">Real Examples</h2>
<p><strong>Apple Home as unified interface.</strong> Home Assistant exposes a HomeKit bridge. All devices, regardless of manufacturer, appear in Apple Home with Siri support. Claude Code configured the bridge, filtered which entities to expose, and set friendly names.</p>
<p><strong>Conditional Ring alarm automation.</strong> "Lock all doors when the alarm is armed" sounds simple. Implementation requires knowing the entity IDs, the correct service calls, and handling both armed_home and armed_away states. Claude Code wrote it in seconds:</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">alias:</span> <span class="hljs-string">"Lock All Doors When Alarm Armed"</span>
  <span class="hljs-attr">trigger:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">platform:</span> <span class="hljs-string">state</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">alarm_control_panel.oakland_alarm</span>
      <span class="hljs-attr">to:</span> <span class="hljs-string">"armed_home"</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">platform:</span> <span class="hljs-string">state</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">alarm_control_panel.oakland_alarm</span>
      <span class="hljs-attr">to:</span> <span class="hljs-string">"armed_away"</span>
  <span class="hljs-attr">action:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">service:</span> <span class="hljs-string">lock.lock</span>
      <span class="hljs-attr">target:</span>
        <span class="hljs-attr">entity_id:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">lock.aqara_smart_lock_u100</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">lock.back_door</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">lock.side_gate</span>
</code></pre>
<h2 id="heading-what-this-changes">What This Changes</h2>
<p>The old workflow: SSH into the container, open a YAML file in Nano, make your edits, save, reload. If your spacing is off, it breaks. Fix it, try again. None of this is hard, it's just time-consuming. When building an automation takes 20 minutes of fiddling, it's harder to justify the time.</p>
<p>Claude Code compresses that. I describe what I want, it writes the YAML, uploads it, validates the config, reloads the integration. The documentation folder gives it context about my specific setup, so it knows my entity names, my server IPs, how my containers are organized. That's the difference.</p>
<p>That's the stack for 2026.</p>
<hr />
<p>[1] The hardware doesn't matter much. An old laptop, a Raspberry Pi, or any always-on computer will work. I went with a mini PC for the performance headroom, but Home Assistant runs fine on minimal hardware.</p>
<p>[2] Proxmox lets you run multiple isolated environments on one machine. I can run Home Assistant in one container, Pi-hole in another, and spin up test environments without affecting production. If something breaks, I can snapshot and restore. Overkill for some, but nice to have.</p>
<p>[3] Why containers instead of bare metal? I can run multiple services on the same hardware without them stepping on each other. Home Assistant, Pi-hole, a media server, whatever. Each runs in its own container with its own dependencies. Updates are clean, backups are simple, and if a container misbehaves, you restart it without touching anything else.</p>
<p>[4] Voice-to-text options: <a target="_blank" href="https://www.wispr.ai/">Wispr Flow</a> and <a target="_blank" href="https://www.voiceink.app/">VoiceInk</a> both work well. They transcribe in real-time as you speak, so you can dictate directly into the terminal.</p>
]]></content:encoded></item><item><title><![CDATA[Modeling React State as a Finite State Machine]]></title><description><![CDATA[Back in 2022, I wrote Building a Traffic Light React App exploring how to model UI state as a finite state machine. The key insight was naming states by their business meaning rather than their visual representation, like PriorityStraight instead of ...]]></description><link>https://blog.jcosta.tech/modeling-react-state-as-a-finite-state-machine</link><guid isPermaLink="true">https://blog.jcosta.tech/modeling-react-state-as-a-finite-state-machine</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[state-machines]]></category><category><![CDATA[State Management ]]></category><category><![CDATA[React]]></category><category><![CDATA[react hooks]]></category><category><![CDATA[Functional Programming]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Frontend Development]]></category><dc:creator><![CDATA[John Costa]]></dc:creator><pubDate>Tue, 30 Dec 2025 21:38:44 GMT</pubDate><content:encoded><![CDATA[<p>Back in 2022, I wrote <a target="_blank" href="https://johntcosta.hashnode.dev/building-a-traffic-light-react-app">Building a Traffic Light React App</a> exploring how to model UI state as a finite state machine. The key insight was naming states by their <strong>business meaning</strong> rather than their visual representation, like <code>PriorityStraight</code> instead of <code>red-light-green-arrow</code>.</p>
<p>That post used class components and MobX. This post takes the same core ideas (state machines and domain-driven naming) and shows how to apply them in modern React with <strong>function components, hooks, and TypeScript discriminated unions</strong>.</p>
<h2 id="heading-the-problem-boolean-soup">The Problem: Boolean Soup</h2>
<p>Here's a typical React component managing a blog post editor:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> [isLoading, setIsLoading] = useState(<span class="hljs-literal">false</span>);
<span class="hljs-keyword">const</span> [isSaving, setIsSaving] = useState(<span class="hljs-literal">false</span>);
<span class="hljs-keyword">const</span> [isPublishing, setIsPublishing] = useState(<span class="hljs-literal">false</span>);
<span class="hljs-keyword">const</span> [hasError, setHasError] = useState(<span class="hljs-literal">false</span>);
<span class="hljs-keyword">const</span> [errorMessage, setErrorMessage] = useState(<span class="hljs-string">''</span>);
<span class="hljs-keyword">const</span> [showPublishModal, setShowPublishModal] = useState(<span class="hljs-literal">false</span>);
<span class="hljs-keyword">const</span> [showDiscardModal, setShowDiscardModal] = useState(<span class="hljs-literal">false</span>);
<span class="hljs-keyword">const</span> [showSuccessMessage, setShowSuccessMessage] = useState(<span class="hljs-literal">false</span>);
<span class="hljs-keyword">const</span> [isDirty, setIsDirty] = useState(<span class="hljs-literal">false</span>);
</code></pre>
<p>Nine <code>useState</code> calls. Nine independent booleans.</p>
<p><strong>Here's the problem:</strong> With 9 booleans, there are 2⁹ = <strong>512 possible combinations</strong>. But how many are actually valid UI states? Maybe 9 or 10.</p>
<p>That means <strong>~97% of possible states are invalid</strong>. Can both modals be open? Can we be saving AND publishing? The type system allows it. Your UI doesn't. This gap is where bugs live.</p>
<h2 id="heading-the-solution-one-state-to-rule-them-all">The Solution: One State to Rule Them All</h2>
<p>What are the actual states our editor can be in?</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">type</span> EditorState =
  | { kind: <span class="hljs-string">'editing'</span>; draft: PostData; original: PostData }
  | { kind: <span class="hljs-string">'saving-draft'</span>; draft: PostData; original: PostData }
  | { kind: <span class="hljs-string">'draft-saved'</span>; draft: SavedPostData; original: PostData }
  | { kind: <span class="hljs-string">'save-error'</span>; draft: PostData; original: PostData; error: <span class="hljs-built_in">string</span> }
  | { kind: <span class="hljs-string">'confirming-publish'</span>; draft: PostData; original: PostData }
  | { kind: <span class="hljs-string">'publishing'</span>; draft: PostData; original: PostData }
  | { kind: <span class="hljs-string">'publish-error'</span>; draft: PostData; original: PostData; error: <span class="hljs-built_in">string</span> }
  | { kind: <span class="hljs-string">'confirming-discard'</span>; draft: PostData; original: PostData }
  | { kind: <span class="hljs-string">'published'</span>; post: SavedPostData };
</code></pre>
<p>Nine explicit states. One <code>useState</code> call:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> [state, setState] = useState&lt;EditorState&gt;({
  kind: <span class="hljs-string">'editing'</span>,
  draft: { title: <span class="hljs-string">''</span>, content: <span class="hljs-string">''</span> },
  original: { title: <span class="hljs-string">''</span>, content: <span class="hljs-string">''</span> },
});
</code></pre>
<h2 id="heading-why-this-works">Why This Works</h2>
<h3 id="heading-1-impossible-states-are-impossible">1. Impossible States Are Impossible</h3>
<p>With boolean soup, nothing prevents <code>showPublishModal</code> and <code>showDiscardModal</code> from both being <code>true</code>. With a discriminated union, you can only be in ONE state at a time. The type system enforces it.</p>
<h3 id="heading-2-domain-driven-state-names">2. Domain-Driven State Names</h3>
<p>Just like naming traffic light states <code>PriorityStraight</code> instead of <code>red-light-green-arrow</code>, we name our states by what they <strong>mean</strong>, not what they <strong>look like</strong>:</p>
<ul>
<li><code>'saving-draft'</code> instead of <code>isSaving &amp;&amp; !isPublishing &amp;&amp; !showModal</code></li>
<li><code>'confirming-publish'</code> instead of <code>showPublishModal &amp;&amp; !showDiscardModal</code></li>
</ul>
<p>When you read <code>state.kind === 'confirming-discard'</code>, you know exactly what's happening.</p>
<h3 id="heading-3-each-state-carries-its-context">3. Each State Carries Its Context</h3>
<p>Notice how <code>save-error</code> includes an <code>error</code> property, but <code>editing</code> doesn't? Each state carries only the data it needs. No stale <code>errorMessage</code> hanging around from a previous failed save.</p>
<h3 id="heading-4-exhaustive-switch-statements">4. Exhaustive Switch Statements</h3>
<p>TypeScript's exhaustive checking ensures you handle every state:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> assertNever = (x: <span class="hljs-built_in">never</span>): <span class="hljs-function"><span class="hljs-params">never</span> =&gt;</span> {
  <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Unexpected state: <span class="hljs-subst">${x}</span>`</span>);
};

<span class="hljs-keyword">switch</span> (state.kind) {
  <span class="hljs-keyword">case</span> <span class="hljs-string">'editing'</span>:
    <span class="hljs-comment">// ...</span>
  <span class="hljs-keyword">case</span> <span class="hljs-string">'saving-draft'</span>:
    <span class="hljs-comment">// ...</span>
  <span class="hljs-comment">// If you forget a case, TypeScript errors!</span>
  <span class="hljs-keyword">default</span>:
    <span class="hljs-keyword">return</span> assertNever(state);
}
</code></pre>
<p>Add a new state to the union? TypeScript shows errors everywhere you forgot to handle it.</p>
<h3 id="heading-5-explicit-transitions">5. Explicit Transitions</h3>
<p>State transitions become clear, intentional functions:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> openPublishConfirmation = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">switch</span> (state.kind) {
    <span class="hljs-keyword">case</span> <span class="hljs-string">'editing'</span>:
    <span class="hljs-keyword">case</span> <span class="hljs-string">'draft-saved'</span>:
      setState({
        kind: <span class="hljs-string">'confirming-publish'</span>,
        draft: state.draft,
        original: state.original,
      });
      <span class="hljs-keyword">return</span>;

    <span class="hljs-comment">// All other states: can't open publish modal</span>
    <span class="hljs-keyword">case</span> <span class="hljs-string">'saving-draft'</span>:
    <span class="hljs-keyword">case</span> <span class="hljs-string">'publishing'</span>:
    <span class="hljs-comment">// ...</span>
      <span class="hljs-keyword">return</span>;

    <span class="hljs-keyword">default</span>:
      assertNever(state);
  }
};
</code></pre>
<p>You can see exactly which states allow transitioning to <code>confirming-publish</code>. This is your state machine, defined in code.</p>
<h2 id="heading-try-it-yourself">Try It Yourself</h2>
<p>I've built a live demo with both approaches side-by-side:</p>
<p><strong><a target="_blank" href="https://costajohnt.github.io/state-machine-blog-post/">View Demo →</a></strong></p>
<p><strong><a target="_blank" href="https://github.com/costajohnt/state-machine-blog-post">View Source →</a></strong></p>
<p>Interact with both editors. Notice how the "After" version displays its current state, so you always know exactly what's happening.</p>
<h2 id="heading-when-to-use-this-pattern">When To Use This Pattern</h2>
<p>This pattern shines when:</p>
<ul>
<li>You have <strong>3+ boolean states</strong> that interact</li>
<li>States are <strong>mutually exclusive</strong> (one modal at a time, one async operation)</li>
<li>You're tracking <strong>loading/success/error cycles</strong></li>
<li>You find yourself writing conditions like <code>if (!isLoading &amp;&amp; !isSaving &amp;&amp; !hasError)</code></li>
</ul>
<p>For a single <code>isOpen</code> boolean? Overkill. For anything resembling "boolean soup"? Worth it.</p>
<h2 id="heading-what-about-xstate">What About XState?</h2>
<p><a target="_blank" href="https://xstate.js.org/">XState</a> is excellent for complex state machines with visualization tools and formal semantics. But this pattern requires:</p>
<ul>
<li>Zero dependencies</li>
<li>Just TypeScript + <code>useState</code></li>
<li>Knowledge you already have</li>
</ul>
<p>Start here. Graduate to XState when you need it.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>The next time you reach for a fourth <code>useState&lt;boolean&gt;</code>, stop and ask: <strong>what are the actual states my UI can be in?</strong></p>
<p>Model those states explicitly. Name them by their domain meaning. Let TypeScript enforce that impossible states are impossible.</p>
<hr />
<p><em>See also: <a target="_blank" href="https://johntcosta.hashnode.dev/building-a-traffic-light-react-app">Building a Traffic Light React App</a>, the original exploration of domain-driven state naming.</em></p>
]]></content:encoded></item><item><title><![CDATA[Building a Traffic Light React App]]></title><description><![CDATA[I set out to build an example that demonstrates how to manage state in a React TypeScript application using state machines. Traffic lights are commonly used as examples to describe how state machines work, so that seemed like a good place to start. T...]]></description><link>https://blog.jcosta.tech/building-a-traffic-light-react-app</link><guid isPermaLink="true">https://blog.jcosta.tech/building-a-traffic-light-react-app</guid><category><![CDATA[React]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[domain]]></category><category><![CDATA[Functional Programming]]></category><category><![CDATA[solution modeling]]></category><dc:creator><![CDATA[John Costa]]></dc:creator><pubDate>Mon, 26 Sep 2022 04:22:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/_U-x3_FYxfI/upload/v1664165420100/xrFgQC07B.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><img src="https://j.gifs.com/gpNkr6.gif" alt="GIF" /></p>
<p>I set out to build an example that demonstrates how to manage state in a React TypeScript application using state machines. Traffic lights are commonly used as examples to describe how state machines work, so that seemed like a good place to start. The repo is available <a target="_blank" href="https://github.com/costajohnt/stoplight-state-machine">here</a>.</p>
<h2 id="heading-getting-started">Getting Started</h2>
<p>The first step was to take a look at prior work, and a quick google search returned <a target="_blank" href="https://medium.com/well-red/state-machines-for-everyone-part-1-introduction-b7ac9aaf482e">this article</a>, which explains state machines pretty well and has good examples. I set out to recreate the author's design with React, TypeScript, and Functional Programming techniques. The states and transitions look like this (image from author article):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1664149792664/4a46D8s-4.png" alt="image.png" /></p>
<h2 id="heading-somethings-not-right">Something's Not Right</h2>
<p>The code was working as implemented, but parts of the design were concerning:</p>
<ul>
<li>The types may not represent a finite state machine for a traffic light. If the power is out, the light will be completely off. This is a valid, real world case that should be handled as a valid state. </li>
<li>Defining the power outage state with the current naming conventions is troublesome. <code>Off</code> does not fit with the color convention, and <code>black</code> doesn't seem appropriate in this context.</li>
<li>A newly installed light would be in a powered down or off state before being switched on for the first time. This state would be visually similar to the power outage state, but different in ability to transition to some other state. A light that is off because of a power outage would require fixing before being able to be powered back on, but a newly installed light would be able to go directly into a powered on state.</li>
</ul>
<p>At this point, I wasn't quite sure how to refactor. Maybe building the app out a bit more would bring the problems to light. I decided to extend the state machine by adding a left turn indicator.</p>
<h2 id="heading-coming-into-focus">Coming Into Focus</h2>
<p>Adding the left turn arrow meant adding a lot more states to the state machine. I started adding states like <code>red-light-green-arrow</code> and <code>flashing-red-arrow-off.</code> Yikes! Each new state exemplified the design issues. These names were getting long, and could continue to grow even longer as the complexity of the app develops. The fundamental flaw is that the <strong>states were defined by their visual representation</strong> in the user interface. This convention is not scalable and does not convey the business intent properly.  </p>
<h2 id="heading-solution">Solution</h2>
<p>First, it helps to think about what a traffic light really represents. Most of us learn how to read a stop light at a young age and take it for granted, but suppose you had to explain it to a small child.</p>
<p>You might say,\ "Green means go." This implies movement and priority to proceed through an intersection. Yellow is a warning that movement will be prohibited soon. Red means stop, stay immobile, movement is not allowed. These words describe the domain logic of a traffic light. <a target="_blank" href="https://en.wikipedia.org/wiki/Domain-driven_design">Domain Driven Design</a> can be a powerful tool for building systems using a language that both the domain expert and developer can share.</p>
<h2 id="heading-extension">Extension</h2>
<p>Now we have a <code>Warning</code> state and a <code>Prohibited</code> state for yellow and red lights. When the left turn signal is green, we represent this as <code>PriorityLeft</code> instead of <code>red-light-green-arrow</code>. <code>Green-light-flashing-yellow-arrow</code> is <code>PriorityStraight</code>. Here is what our type for <code>PriorityStraight</code> looks like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1664153482746/gOkPlK-5s.jpeg" alt="Screen Shot 2022-09-25 at 5.49.52 PM.jpeg" /></p>
<p>And here is how our finite state machine is represented in the types:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1664153593712/4Dqf-0cJd.jpeg" alt="PNG image.jpeg" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Modeling the domain in terms of the business function helps control complexity and aids maintainability/extendability as an application scales.</p>
]]></content:encoded></item></channel></rss>