<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Engineering on houdeshell.dev</title><link>https://houdeshell.dev/tags/engineering/</link><description>Recent content in Engineering on houdeshell.dev</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><copyright>© CRH</copyright><lastBuildDate>Wed, 27 May 2026 10:00:00 -0400</lastBuildDate><atom:link href="https://houdeshell.dev/tags/engineering/index.xml" rel="self" type="application/rss+xml"/><item><title>Software that I &lt;img class='title-icon' src='https://houdeshell.dev/static/images/lego-heart.svg' alt='heart' /></title><link>https://houdeshell.dev/software/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate><guid>https://houdeshell.dev/software/</guid><description>&lt;p>I bounce between Mac, Windows, and Linux daily. Honestly, the tools matter more than the OS at this point. Great software is great software, and I&amp;rsquo;ve been lucky enough to build a career on top of things other people built well.&lt;/p>
&lt;p>This is the stuff I actually reach for. Not a &amp;ldquo;best of&amp;rdquo; list, not sponsored, not comprehensive. Just software that makes me better at what I do. Or at least makes the work more fun.&lt;/p>
&lt;style>
.sw-grid {
display: flex;
flex-direction: column;
margin: 1.6em 0 2.6em;
counter-reset: sw;
border-top: 1px solid var(--border);
}
.sw-card {
display: grid;
grid-template-columns: 44px 130px 1fr;
grid-template-areas:
"num tag title"
"num tag desc";
column-gap: 1.5em;
row-gap: 0.3em;
align-items: baseline;
padding: 1em 6px 1em 4px;
border-bottom: 1px solid var(--border);
counter-increment: sw;
transition: background 0.2s ease, padding-left 0.2s ease;
}
.sw-card::before {
content: counter(sw, decimal-leading-zero);
grid-area: num;
align-self: baseline;
font-family: var(--font-mono);
font-size: 0.72em;
font-weight: 500;
letter-spacing: 0.08em;
color: var(--text-muted);
transition: color 0.2s ease;
}
.sw-card:hover {
background: rgba(251, 60, 0, 0.035);
padding-left: 12px;
}
.sw-card:hover::before {
color: #fb3c00;
}
.sw-card .sw-tag {
grid-area: tag;
align-self: baseline;
font-family: var(--font-mono);
font-size: 0.7em;
font-weight: 500;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-secondary);
background: none;
border: none;
padding: 0;
margin: 0;
transition: color 0.2s ease;
}
.sw-card:hover .sw-tag {
color: #fb3c00;
}
.sw-card h3 {
grid-area: title;
margin: 0 !important;
font-size: 1.02em !important;
font-weight: 600 !important;
letter-spacing: -0.005em !important;
border: none !important;
padding: 0 !important;
color: var(--text) !important;
text-decoration: none !important;
}
.sw-card h3::before {
content: none !important;
}
.sw-card h3 a {
color: var(--text);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: color 0.2s ease, border-color 0.2s ease;
}
.sw-card:hover h3 a {
color: #fb3c00;
border-bottom-color: rgba(251, 60, 0, 0.4);
}
.sw-card p {
grid-area: desc;
margin: 0 !important;
font-size: 0.88em;
color: var(--text-secondary);
line-height: 1.55;
}
.sw-card p a {
color: var(--text);
text-decoration: none;
border-bottom: 1px dashed var(--accent-line);
}
.sw-card p a:hover {
color: #fb3c00;
border-bottom-color: #fb3c00;
}
.sw-card code {
background: rgba(0, 0, 0, 0.04) !important;
color: var(--text) !important;
border-color: var(--border) !important;
}
@media (max-width: 640px) {
.sw-card {
grid-template-columns: 1fr;
grid-template-areas:
"num"
"tag"
"title"
"desc";
row-gap: 0.35em;
padding: 0.95em 4px;
}
.sw-card:hover {
padding-left: 4px;
}
}
&lt;/style>
&lt;h2 id="software-development" class="md-heading">
Software Development
&lt;a class="md-heading-anchor" href="#software-development" aria-label="Link to Software Development">#&lt;/a>
&lt;/h2>
&lt;div class="sw-grid">
&lt;div class="sw-card">
&lt;span class="sw-tag">IDE&lt;/span>
&lt;h3 id="jetbrains-rider" class="md-heading">
&lt;a href="https://www.jetbrains.com/rider/"target="_blank" rel="noopener noreferrer">JetBrains Rider&lt;/a>
&lt;a class="md-heading-anchor" href="#jetbrains-rider" aria-label="Link to JetBrains Rider">#&lt;/a>
&lt;/h3>
&lt;p>Cross-platform .NET IDE. Fast, smart, and doesn&amp;rsquo;t need Visual Studio&amp;rsquo;s weight to get the job done.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">IDE&lt;/span>
&lt;h3 id="visual-studio" class="md-heading">
&lt;a href="https://visualstudio.microsoft.com/"target="_blank" rel="noopener noreferrer">Visual Studio&lt;/a>
&lt;a class="md-heading-anchor" href="#visual-studio" aria-label="Link to Visual Studio">#&lt;/a>
&lt;/h3>
&lt;p>The OG. Heavy, but when you need the full .NET debugging experience, nothing else comes close.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">editor&lt;/span>
&lt;h3 id="vs-code" class="md-heading">
&lt;a href="https://code.visualstudio.com/"target="_blank" rel="noopener noreferrer">VS Code&lt;/a>
&lt;a class="md-heading-anchor" href="#vs-code" aria-label="Link to VS Code">#&lt;/a>
&lt;/h3>
&lt;p>Extension ecosystem is unmatched. Somehow Electron done right.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">editor&lt;/span>
&lt;h3 id="neovim" class="md-heading">
&lt;a href="https://neovim.io/"target="_blank" rel="noopener noreferrer">Neovim&lt;/a>
&lt;a class="md-heading-anchor" href="#neovim" aria-label="Link to Neovim">#&lt;/a>
&lt;/h3>
&lt;p>Vim reborn. Lua config, LSP native, and a plugin ecosystem that won&amp;rsquo;t quit. &lt;code>:wq&lt;/code> is a lifestyle.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">version control&lt;/span>
&lt;h3 id="git" class="md-heading">
&lt;a href="https://git-scm.com/"target="_blank" rel="noopener noreferrer">Git&lt;/a>
&lt;a class="md-heading-anchor" href="#git" aria-label="Link to Git">#&lt;/a>
&lt;/h3>
&lt;p>The version control system that won. Love it or hate it, you can&amp;rsquo;t ship without it.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">version control&lt;/span>
&lt;h3 id="lazygit" class="md-heading">
&lt;a href="https://github.com/jesseduffield/lazygit"target="_blank" rel="noopener noreferrer">lazygit&lt;/a>
&lt;a class="md-heading-anchor" href="#lazygit" aria-label="Link to lazygit">#&lt;/a>
&lt;/h3>
&lt;p>Terminal UI for git that makes interactive rebases feel like cheating.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">JSON&lt;/span>
&lt;h3 id="jq" class="md-heading">
&lt;a href="https://jqlang.github.io/jq/"target="_blank" rel="noopener noreferrer">jq&lt;/a>
&lt;a class="md-heading-anchor" href="#jq" aria-label="Link to jq">#&lt;/a>
&lt;/h3>
&lt;p>&lt;code>sed&lt;/code> for JSON. Once you learn the syntax, you&amp;rsquo;ll pipe everything through it.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">database&lt;/span>
&lt;h3 id="jetbrains-datagrip" class="md-heading">
&lt;a href="https://www.jetbrains.com/datagrip/"target="_blank" rel="noopener noreferrer">JetBrains DataGrip&lt;/a>
&lt;a class="md-heading-anchor" href="#jetbrains-datagrip" aria-label="Link to JetBrains DataGrip">#&lt;/a>
&lt;/h3>
&lt;p>SQL IDE that actually understands your schema. Autocomplete that works across joins.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">database&lt;/span>
&lt;h3 id="dbeaver" class="md-heading">
&lt;a href="https://dbeaver.io/"target="_blank" rel="noopener noreferrer">DBeaver&lt;/a>
&lt;a class="md-heading-anchor" href="#dbeaver" aria-label="Link to DBeaver">#&lt;/a>
&lt;/h3>
&lt;p>Universal database tool that actually works. Connect to anything, query everything.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">database&lt;/span>
&lt;h3 id="ssms" class="md-heading">
&lt;a href="https://learn.microsoft.com/en-us/sql/ssms/"target="_blank" rel="noopener noreferrer">SSMS&lt;/a>
&lt;a class="md-heading-anchor" href="#ssms" aria-label="Link to SSMS">#&lt;/a>
&lt;/h3>
&lt;p>Microsoft SQL Management Studio. If you&amp;rsquo;re in SQL Server land, you already know.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">database&lt;/span>
&lt;h3 id="postgresql" class="md-heading">
&lt;a href="https://www.postgresql.org/"target="_blank" rel="noopener noreferrer">PostgreSQL&lt;/a>
&lt;a class="md-heading-anchor" href="#postgresql" aria-label="Link to PostgreSQL">#&lt;/a>
&lt;/h3>
&lt;p>The database that keeps getting better. Extensions, JSON support, and rock-solid reliability.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">containers&lt;/span>
&lt;h3 id="docker" class="md-heading">
&lt;a href="https://www.docker.com/"target="_blank" rel="noopener noreferrer">Docker&lt;/a>
&lt;a class="md-heading-anchor" href="#docker" aria-label="Link to Docker">#&lt;/a>
&lt;/h3>
&lt;p>&amp;ldquo;Works on my machine&amp;rdquo; became &amp;ldquo;works on every machine.&amp;rdquo; Changed how we ship software.&lt;/p>
&lt;/div>
&lt;/div>
&lt;h2 id="ai" class="md-heading">
AI
&lt;a class="md-heading-anchor" href="#ai" aria-label="Link to AI">#&lt;/a>
&lt;/h2>
&lt;div class="sw-grid">
&lt;div class="sw-card">
&lt;span class="sw-tag">agent&lt;/span>
&lt;h3 id="claude-code" class="md-heading">
&lt;a href="https://docs.anthropic.com/en/docs/claude-code"target="_blank" rel="noopener noreferrer">Claude Code&lt;/a>
&lt;a class="md-heading-anchor" href="#claude-code" aria-label="Link to Claude Code">#&lt;/a>
&lt;/h3>
&lt;p>AI pair programmer in your terminal. It&amp;rsquo;s writing this page right now.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">agent&lt;/span>
&lt;h3 id="vs-code-agents" class="md-heading">
&lt;a href="https://code.visualstudio.com/"target="_blank" rel="noopener noreferrer">VS Code Agents&lt;/a>
&lt;a class="md-heading-anchor" href="#vs-code-agents" aria-label="Link to VS Code Agents">#&lt;/a>
&lt;/h3>
&lt;p>Native agent mode inside the editor. Let the assistant plan, edit files, and run commands without leaving VS Code or babysitting a separate CLI.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">agent&lt;/span>
&lt;h3 id="codex" class="md-heading">
&lt;a href="https://github.com/openai/codex"target="_blank" rel="noopener noreferrer">Codex&lt;/a>
&lt;a class="md-heading-anchor" href="#codex" aria-label="Link to Codex">#&lt;/a>
&lt;/h3>
&lt;p>OpenAI&amp;rsquo;s open-source coding agent. Terminal-native, sandboxed by default, and you can swap the model behind it whenever.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">platform&lt;/span>
&lt;h3 id="azure-ai-foundry" class="md-heading">
&lt;a href="https://azure.microsoft.com/en-us/products/ai-foundry"target="_blank" rel="noopener noreferrer">Azure AI Foundry&lt;/a>
&lt;a class="md-heading-anchor" href="#azure-ai-foundry" aria-label="Link to Azure AI Foundry">#&lt;/a>
&lt;/h3>
&lt;p>Microsoft&amp;rsquo;s AI app stack. Build agents, point them at your data, ship them on Azure. The enterprise wrapper for everything you&amp;rsquo;d otherwise glue together yourself.&lt;/p>
&lt;/div>
&lt;/div>
&lt;h2 id="infrastructure" class="md-heading">
Infrastructure
&lt;a class="md-heading-anchor" href="#infrastructure" aria-label="Link to Infrastructure">#&lt;/a>
&lt;/h2>
&lt;div class="sw-grid">
&lt;div class="sw-card">
&lt;span class="sw-tag">orchestration&lt;/span>
&lt;h3 id="kubernetes" class="md-heading">
&lt;a href="https://kubernetes.io/"target="_blank" rel="noopener noreferrer">Kubernetes&lt;/a>
&lt;a class="md-heading-anchor" href="#kubernetes" aria-label="Link to Kubernetes">#&lt;/a>
&lt;/h3>
&lt;p>Container orchestration that makes you mass produce YAML for a living.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">orchestration&lt;/span>
&lt;h3 id="k3s" class="md-heading">
&lt;a href="https://k3s.io/"target="_blank" rel="noopener noreferrer">K3s&lt;/a>
&lt;a class="md-heading-anchor" href="#k3s" aria-label="Link to K3s">#&lt;/a>
&lt;/h3>
&lt;p>All of Kubernetes in a single binary. Perfect for homelab, edge, and production deployments.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">IaC&lt;/span>
&lt;h3 id="terraform--opentofu" class="md-heading">
&lt;a href="https://www.terraform.io/"target="_blank" rel="noopener noreferrer">Terraform&lt;/a> / &lt;a href="https://opentofu.org/"target="_blank" rel="noopener noreferrer">OpenTofu&lt;/a>
&lt;a class="md-heading-anchor" href="#terraform--opentofu" aria-label="Link to Terraform / OpenTofu">#&lt;/a>
&lt;/h3>
&lt;p>Infrastructure as code. OpenTofu if you prefer the open-source fork without the license drama.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">networking&lt;/span>
&lt;h3 id="tailscale" class="md-heading">
&lt;a href="https://tailscale.com/"target="_blank" rel="noopener noreferrer">Tailscale&lt;/a>
&lt;a class="md-heading-anchor" href="#tailscale" aria-label="Link to Tailscale">#&lt;/a>
&lt;/h3>
&lt;p>WireGuard-based mesh VPN that just works. Connect everything without opening a single port.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">observability&lt;/span>
&lt;h3 id="grafana" class="md-heading">
&lt;a href="https://grafana.com/"target="_blank" rel="noopener noreferrer">Grafana&lt;/a>
&lt;a class="md-heading-anchor" href="#grafana" aria-label="Link to Grafana">#&lt;/a>
&lt;/h3>
&lt;p>Dashboards for everything. Pairs with Prometheus, Loki, and Tempo for the whole observability stack.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">Kubernetes&lt;/span>
&lt;h3 id="k9s" class="md-heading">
&lt;a href="https://k9scli.io/"target="_blank" rel="noopener noreferrer">k9s&lt;/a>
&lt;a class="md-heading-anchor" href="#k9s" aria-label="Link to k9s">#&lt;/a>
&lt;/h3>
&lt;p>Terminal UI for Kubernetes. Makes cluster management feel like a video game. Way faster than raw &lt;code>kubectl&lt;/code>.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">Kubernetes&lt;/span>
&lt;h3 id="lens" class="md-heading">
&lt;a href="https://k8slens.dev/"target="_blank" rel="noopener noreferrer">Lens&lt;/a>
&lt;a class="md-heading-anchor" href="#lens" aria-label="Link to Lens">#&lt;/a>
&lt;/h3>
&lt;p>The Kubernetes IDE. When you want to point and click your way through a cluster without shame.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">it's complicated&lt;/span>
&lt;h3 id="cloudflare" class="md-heading">
&lt;a href="https://www.cloudflare.com/"target="_blank" rel="noopener noreferrer">Cloudflare&lt;/a>
&lt;a class="md-heading-anchor" href="#cloudflare" aria-label="Link to Cloudflare">#&lt;/a>
&lt;/h3>
&lt;p>You love them. You hate them. Your DNS is already there. Their edge network is everywhere, their pricing is unbeatable, and their product sprawl is terrifying. Stockholm syndrome as a service.&lt;/p>
&lt;/div>
&lt;/div>
&lt;h2 id="shell--terminal" class="md-heading">
Shell &amp;amp; Terminal
&lt;a class="md-heading-anchor" href="#shell--terminal" aria-label="Link to Shell &amp;amp; Terminal">#&lt;/a>
&lt;/h2>
&lt;div class="sw-grid">
&lt;div class="sw-card">
&lt;span class="sw-tag">shell&lt;/span>
&lt;h3 id="bash" class="md-heading">
&lt;a href="https://www.gnu.org/software/bash/"target="_blank" rel="noopener noreferrer">Bash&lt;/a>
&lt;a class="md-heading-anchor" href="#bash" aria-label="Link to Bash">#&lt;/a>
&lt;/h3>
&lt;p>The shell that&amp;rsquo;s been there since before you were born. Simple, portable, everywhere.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">shell&lt;/span>
&lt;h3 id="zsh" class="md-heading">
&lt;a href="https://www.zsh.org/"target="_blank" rel="noopener noreferrer">Zsh&lt;/a>
&lt;a class="md-heading-anchor" href="#zsh" aria-label="Link to Zsh">#&lt;/a>
&lt;/h3>
&lt;p>Bash&amp;rsquo;s cooler sibling. Tab completion, globbing, and plugin support that actually makes the terminal enjoyable.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">prompt&lt;/span>
&lt;h3 id="starship" class="md-heading">
&lt;a href="https://github.com/starship/starship"target="_blank" rel="noopener noreferrer">Starship&lt;/a>
&lt;a class="md-heading-anchor" href="#starship" aria-label="Link to Starship">#&lt;/a>
&lt;/h3>
&lt;p>Cross-shell prompt written in Rust. Fast, pretty, and infinitely configurable.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">multiplexer&lt;/span>
&lt;h3 id="tmux" class="md-heading">
&lt;a href="https://github.com/tmux/tmux"target="_blank" rel="noopener noreferrer">tmux&lt;/a>
&lt;a class="md-heading-anchor" href="#tmux" aria-label="Link to tmux">#&lt;/a>
&lt;/h3>
&lt;p>Terminal multiplexer. SSH into a box, detach, come back tomorrow. Your session is still there.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">coreutils&lt;/span>
&lt;h3 id="eza" class="md-heading">
&lt;a href="https://github.com/eza-community/eza"target="_blank" rel="noopener noreferrer">eza&lt;/a>
&lt;a class="md-heading-anchor" href="#eza" aria-label="Link to eza">#&lt;/a>
&lt;/h3>
&lt;p>Modern replacement for &lt;code>ls&lt;/code>. Colors, git status, and tree view out of the box. Written in Rust, obviously.&lt;/p>
&lt;/div>
&lt;/div>
&lt;h2 id="utilities--apps" class="md-heading">
Utilities / Apps
&lt;a class="md-heading-anchor" href="#utilities--apps" aria-label="Link to Utilities / Apps">#&lt;/a>
&lt;/h2>
&lt;div class="sw-grid">
&lt;div class="sw-card">
&lt;span class="sw-tag">macOS&lt;/span>
&lt;h3 id="alttab" class="md-heading">
&lt;a href="https://alt-tab-macos.netlify.app/"target="_blank" rel="noopener noreferrer">AltTab&lt;/a>
&lt;a class="md-heading-anchor" href="#alttab" aria-label="Link to AltTab">#&lt;/a>
&lt;/h3>
&lt;p>Why does macOS think a minimized window is an inactive window and not let you switch to it? This fixes that.&lt;/p>
&lt;/div>
&lt;/div></description></item><item><title>About</title><link>https://houdeshell.dev/about/</link><pubDate>Sat, 11 Apr 2026 00:00:00 +0000</pubDate><guid>https://houdeshell.dev/about/</guid><description>&lt;div class="about-intro">
&lt;p class="about-tagline">&lt;span class="about-tagline-slash">//&lt;/span> bit herder. pointer wrecker. yaml apologist.&lt;/p>
&lt;/div>
&lt;p>I&amp;rsquo;m a bit herder, a pointer wrecker, and HTTP is my best friend. &lt;code>0xA3&lt;/code> &amp;amp; &lt;code>0x7C&lt;/code> are also my friends, but not &lt;code>0x08&lt;/code>.&lt;/p>
&lt;p>I spend my days somewhere between writing code and making sure other people can write better code. I love distributed systems, low-level embedded devices, and anything that has a &lt;code>content-type&lt;/code>. I also have an unhealthy relationship with YAML, but that&amp;rsquo;s a Kubernetes problem.&lt;/p>
&lt;h2 id="the-day-job" class="md-heading">
The day job
&lt;a class="md-heading-anchor" href="#the-day-job" aria-label="Link to The day job">#&lt;/a>
&lt;/h2>
&lt;p>During the day, I lead engineering and operations at &lt;a href="https://www.energycap.com"target="_blank" rel="noopener noreferrer">EnergyCAP&lt;/a>, a SaaS platform that helps organizations track utility spend, energy consumption, and sustainability goals across massive building portfolios. We&amp;rsquo;re talking &lt;strong>$100B+ in utility bill spend&lt;/strong> tracked across &lt;strong>350,000+ sites&lt;/strong>. Government, education, healthcare, commercial real estate. If it has a meter, we probably manage it.&lt;/p>
&lt;p>I started here as a developer in 2009 and have been building ever since: through senior roles, into leadership, and eventually into the VP seat where I get to shape both the tech and the teams behind it. Some days that means architecture decisions. Most days it means removing blockers so smart people can do their best work. And yes, sometimes it&amp;rsquo;s just meetings about meetings.&lt;/p>
&lt;p>My philosophy is simple: &lt;strong>technology should serve people&lt;/strong>. Build teams where people feel safe to be wrong, give them the tools to be great, and get out of the way.&lt;/p>
&lt;h3 id="things-i-geek-out-about" class="md-heading">
Things I geek out about
&lt;a class="md-heading-anchor" href="#things-i-geek-out-about" aria-label="Link to Things I geek out about">#&lt;/a>
&lt;/h3>
&lt;p>Distributed systems, container orchestration, database performance tuning, developer tooling, process dumps, leadership that doesn&amp;rsquo;t suck, making engineers&amp;rsquo; lives better, and the eternal quest for a perfect terminal setup.&lt;/p>
&lt;h2 id="i-also-talk-at-things" class="md-heading">
I also talk at things
&lt;a class="md-heading-anchor" href="#i-also-talk-at-things" aria-label="Link to I also talk at things">#&lt;/a>
&lt;/h2>
&lt;p>I like getting on stage and nerding out about the things I&amp;rsquo;ve learned the hard way. My talks tend to fall into a few buckets, and yes, I have strong opinions about all of them.&lt;/p>
&lt;h3 id="ai--the-developer-experience" class="md-heading">
AI &amp;amp; the developer experience
&lt;a class="md-heading-anchor" href="#ai--the-developer-experience" aria-label="Link to AI &amp;amp; the developer experience">#&lt;/a>
&lt;/h3>
&lt;p>The AI wave hit and I leaned in. I&amp;rsquo;ve talked about using LLMs as debugging partners, building RAG pipelines that actually know your business context, and the ethical lines we should be drawing as we hand more work to machines.&lt;/p>
&lt;ul class="talk-list">
&lt;li>My Rubber Duck is a Large Language Model&lt;/li>
&lt;li>Unleashing the Power of the AI Wizards: Retrieval-Augmented Generation Spells&lt;/li>
&lt;/ul>
&lt;h3 id="kubernetes--cloud-native" class="md-heading">
Kubernetes &amp;amp; cloud native
&lt;a class="md-heading-anchor" href="#kubernetes--cloud-native" aria-label="Link to Kubernetes &amp;amp; cloud native">#&lt;/a>
&lt;/h3>
&lt;p>I was late to the Kubernetes party, and I&amp;rsquo;ll tell you all about it. From lightweight K3s workshops to navigating the 900+ services in the CNCF landscape, to making the case that cloud-native principles work even when you&amp;rsquo;re not in the cloud.&lt;/p>
&lt;ul class="talk-list">
&lt;li>Kubernetes Chronicles: Late to the Party, Big on Adventure!&lt;/li>
&lt;li>K3s: Half the Size, Twice as Awesome (workshop)&lt;/li>
&lt;li>Orchestrating Machine Learning Workloads with Kubernetes&lt;/li>
&lt;li>Exploring the Cloud Native Landscape&lt;/li>
&lt;li>Cloud Native is Only for the Cloud, Right?&lt;/li>
&lt;li>Nomad: Orchestration Doesn't Start with a K&lt;/li>
&lt;/ul>
&lt;h3 id="performance--databases" class="md-heading">
Performance &amp;amp; databases
&lt;a class="md-heading-anchor" href="#performance--databases" aria-label="Link to Performance &amp;amp; databases">#&lt;/a>
&lt;/h3>
&lt;p>I&amp;rsquo;ve spent an unreasonable amount of time staring at query plans. These talks cover squeezing performance out of SQL Server, using DMVs like cheat codes, and that one time we took a 10-hour process down to 10 minutes.&lt;/p>
&lt;ul class="talk-list">
&lt;li>ReArchitecting Data: 10 Hours to 10 Minutes&lt;/li>
&lt;li>Mastering SQL Server Performance Optimization (workshop)&lt;/li>
&lt;li>SQL Server DMVs That Give Me Superpowers&lt;/li>
&lt;li>Achieving Continuous High Performance with Query Store&lt;/li>
&lt;li>Practical High Performance: C# Edition&lt;/li>
&lt;li>Intrinsics in .NET: Start Somewhere&lt;/li>
&lt;/ul>
&lt;h3 id="production-war-stories--leadership" class="md-heading">
Production war stories &amp;amp; leadership
&lt;a class="md-heading-anchor" href="#production-war-stories--leadership" aria-label="Link to Production war stories &amp;amp; leadership">#&lt;/a>
&lt;/h3>
&lt;p>Production breaks. Systems fail. The interesting part is what you do next. I also talk about the human side: what I&amp;rsquo;ve learned (and gotten wrong) leading engineering teams.&lt;/p>
&lt;ul class="talk-list">
&lt;li>Bug Squashing with Process Dumps&lt;/li>
&lt;li>Recovery by Design: A Postmortem Adventure&lt;/li>
&lt;li>Joining the Cloud: Our Journey&lt;/li>
&lt;li>TIL as a CTO&lt;/li>
&lt;/ul>
&lt;h3 id="the-wildcard" class="md-heading">
The wildcard
&lt;a class="md-heading-anchor" href="#the-wildcard" aria-label="Link to The wildcard">#&lt;/a>
&lt;/h3>
&lt;p>Sometimes you just want to build something fun with your hands.&lt;/p>
&lt;ul class="talk-list">
&lt;li>Paper Circuits: Origami for a New Generation&lt;/li>
&lt;/ul>
&lt;h2 id="lets-talk" class="md-heading">
Let&amp;rsquo;s talk
&lt;a class="md-heading-anchor" href="#lets-talk" aria-label="Link to Let’s talk">#&lt;/a>
&lt;/h2>
&lt;p>I drink a bit too much coffee while rambling on about technology. If you&amp;rsquo;re willing to have a conversation, I&amp;rsquo;m ready to buy you a cup.&lt;/p>
&lt;dl class="contact-list">
&lt;dt>email&lt;/dt>&lt;dd>&lt;a href="mailto:chris@houdeshell.dev">chris@houdeshell.dev&lt;/a>&lt;/dd>
&lt;dt>github&lt;/dt>&lt;dd>&lt;a href="https://github.com/choudeshell">choudeshell&lt;/a>&lt;/dd>
&lt;dt>linkedin&lt;/dt>&lt;dd>&lt;a href="https://www.linkedin.com/in/choudeshell/">choudeshell&lt;/a>&lt;/dd>
&lt;dt>bluesky&lt;/dt>&lt;dd>&lt;a href="https://bsky.app/profile/choudeshell.bsky.social">@choudeshell.bsky.social&lt;/a>&lt;/dd>
&lt;/dl></description></item><item><title>Now</title><link>https://houdeshell.dev/now/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate><guid>https://houdeshell.dev/now/</guid><description>&lt;h2 id="focus" class="md-heading">
Focus
&lt;a class="md-heading-anchor" href="#focus" aria-label="Link to Focus">#&lt;/a>
&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>AI at &lt;code>/\$DAYJOB/&lt;/code>.&lt;/strong> Production agents on real customer data. Compliance edges, evals, on-call playbooks.&lt;/li>
&lt;li>&lt;strong>Upgrading Azure infrastructure.&lt;/strong> Modernizing the stack we run customer workloads on.&lt;/li>
&lt;/ul>
&lt;h2 id="thinking" class="md-heading">
Thinking
&lt;a class="md-heading-anchor" href="#thinking" aria-label="Link to Thinking">#&lt;/a>
&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>AI.&lt;/strong> Where it pays off and where it just adds latency.&lt;/li>
&lt;li>&lt;strong>Team.&lt;/strong> Shape, growth, what each person&amp;rsquo;s pulling toward.&lt;/li>
&lt;li>&lt;strong>The price of the cloud.&lt;/strong> Watching cluster spend while AI workloads scale up unpredictably.&lt;/li>
&lt;/ul>
&lt;h2 id="reading" class="md-heading">
Reading
&lt;a class="md-heading-anchor" href="#reading" aria-label="Link to Reading">#&lt;/a>
&lt;/h2>
&lt;ul>
&lt;li>TODO — drop in current books, longreads, or RSS finds here. The shelf changes monthly.&lt;/li>
&lt;/ul></description></item><item><title>You're the Typist</title><link>https://houdeshell.dev/post/2026-05-27_youre-the-typist/</link><pubDate>Wed, 27 May 2026 10:00:00 -0400</pubDate><guid>https://houdeshell.dev/post/2026-05-27_youre-the-typist/</guid><description>&lt;p>There&amp;rsquo;s a question I ask engineers when they hand me an AI generated PR: &amp;ldquo;walk me through what this does.&amp;rdquo;&lt;/p>
&lt;p>If they can, the PR is usually fine. If they can&amp;rsquo;t, the PR is almost always broken. Not visibly broken. Not the kind of broken that fails a test. The kind of broken that ships, sits in production for six months, and then takes down a service at 2am because nobody knew the cache invalidation only ran on the leader.&lt;/p>
&lt;p>AI doesn&amp;rsquo;t fix that. AI makes it worse.&lt;/p>
&lt;p>&lt;strong>AI is a multiplier.&lt;/strong> It multiplies whatever you bring to it. If you understand the problem, AI gets you to a working solution faster than any tool we&amp;rsquo;ve ever had. If you don&amp;rsquo;t, it gets you to something that &lt;em>looks&lt;/em> like a working solution faster than any tool we&amp;rsquo;ve ever had. Those are very different things, and the difference doesn&amp;rsquo;t show up until later.&lt;/p>
&lt;p>The engineers I see thriving with AI right now read every line. They push back when the model takes a shortcut they don&amp;rsquo;t like. They ask &amp;ldquo;why&amp;rdquo; three times. They notice when the generated code uses a pattern they&amp;rsquo;ve never seen, and they stop to learn it before they accept it. AI doubles their throughput because they were already good. It also doubles their learning rate, because every prompt is a chance to see how an experienced engineer (which is what the model is impersonating) would have approached the same problem.&lt;/p>
&lt;p>The engineers I see drowning treat the model like an oracle. They accept the diff. They run the tests. The tests pass, so they ship. They have no mental model of what they just merged. Six months in, they&amp;rsquo;ve shipped a hundred PRs and learned nothing. The codebase has grown faster than their understanding of it, and the gap is widening.&lt;/p>
&lt;p>Here&amp;rsquo;s the test I&amp;rsquo;d give anyone using AI heavily. Pick a PR from last week. Open it without the model. Explain every block out loud. Why this data structure? Why this error path? What happens if this call times out? What does this loop do on the empty case? If you can&amp;rsquo;t answer those questions about your own merged code, AI isn&amp;rsquo;t helping &lt;em>you&lt;/em>. It&amp;rsquo;s helping the model, and you&amp;rsquo;re the typist.&lt;/p>
&lt;p>This isn&amp;rsquo;t just an engineering problem. Engineering managers and product managers fall into the same trap, one rung up. If you used AI to write a JIRA ticket and you can&amp;rsquo;t explain it five minutes later, your problem is different than you think. The ticket isn&amp;rsquo;t the artifact. The thinking behind the ticket is. If the model did the thinking and you hit save, you&amp;rsquo;ve handed your team a task that nobody on your side actually owns. The first time an engineer pushes back with a real question, the whole thing falls apart, and the engineer figures out fast that there&amp;rsquo;s no one home behind the requirements.&lt;/p>
&lt;p>&lt;strong>Use AI. Lean into it. Just don&amp;rsquo;t outsource the part of the job that was ever actually yours.&lt;/strong>&lt;/p></description></item><item><title>KQL Reads Like a Sentence</title><link>https://houdeshell.dev/post/2026-05-23_kql-reads-like-a-sentence/</link><pubDate>Sat, 23 May 2026 10:00:00 -0400</pubDate><guid>https://houdeshell.dev/post/2026-05-23_kql-reads-like-a-sentence/</guid><description>&lt;p>Watch a smart, curious person try to learn SQL. They get through &lt;code>SELECT * FROM x WHERE y&lt;/code> and they&amp;rsquo;re fine. Then they hit &lt;code>JOIN&lt;/code>, or &lt;code>GROUP BY&lt;/code>, or a correlated subquery, and the floor opens up. Most never come back.&lt;/p>
&lt;p>KQL is the opposite. It reads left to right, the way the human brain wants to think about data.&lt;/p>
&lt;div class="code-block" data-lang="kusto">&lt;span class="code-lang" aria-hidden="true">kusto&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">PageViews
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">| where TimeGenerated &amp;gt; ago(7d)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">| where ClientCountryOrRegion == &amp;#34;United States&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">| summarize count() by bin(TimeGenerated, 1d), Page
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">| top 10 by count_&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;p>Read it out loud. &amp;ldquo;Take page views, where time is in the last seven days, where the country is the US, summarize by day and page, give me the top ten.&amp;rdquo; That sentence is the query. Nothing is hidden, nothing is out of order.&lt;/p>
&lt;p>The verbs are English. &lt;code>where&lt;/code>. &lt;code>summarize&lt;/code>. &lt;code>project&lt;/code>. &lt;code>extend&lt;/code>. &lt;code>take&lt;/code>. &lt;code>top&lt;/code>. There&amp;rsquo;s no &lt;code>CROSS APPLY&lt;/code> lurking three chapters in.&lt;/p>
&lt;p>&lt;strong>You can do useful work on day one without ever learning a join.&lt;/strong> That matters more than engineers remember. Every join is a cliff for a non-engineer, and SQL puts that cliff in front of trivial questions. KQL puts the cliff somewhere on chapter five.&lt;/p>
&lt;p>Add to that:&lt;/p>
&lt;ul>
&lt;li>Sane defaults — &lt;code>take 100&lt;/code> to peek, results are always tabular&lt;/li>
&lt;li>Time-series operators baked in — &lt;code>ago&lt;/code>, &lt;code>bin&lt;/code>, &lt;code>series_decompose&lt;/code>&lt;/li>
&lt;li>Autocomplete that actually understands your schema&lt;/li>
&lt;li>A query pane that gives you instant feedback the moment you stop typing&lt;/li>
&lt;/ul>
&lt;p>KQL isn&amp;rsquo;t the right tool for everything. I&amp;rsquo;d never reach for it to build a transactional system. But for letting smart, curious people answer their own questions about their own data, I haven&amp;rsquo;t seen anything better.&lt;/p>
&lt;p>&lt;em>Take all that with a grain of salt — I wrote a KQL-to-SQL parser and generator, so I&amp;rsquo;m clearly not sure what any of this means.&lt;/em>&lt;/p></description></item><item><title>Inverse AI Consulting</title><link>https://houdeshell.dev/post/2026-05-15_inverse-ai-consulting/</link><pubDate>Fri, 15 May 2026 15:00:00 -0400</pubDate><guid>https://houdeshell.dev/post/2026-05-15_inverse-ai-consulting/</guid><description>&lt;p>&lt;a href="https://x.com/mitchellh/status/2055380239711457578"target="_blank" rel="noopener noreferrer">Mitchell Hashimoto wrote a post on X&lt;/a> that I haven&amp;rsquo;t stopped thinking about.&lt;/p>
&lt;blockquote>
&lt;p>It&amp;rsquo;s frightening, because the psychosis folks operate under an almost absolute &amp;ldquo;MTTR is all you need&amp;rdquo; mentality: &amp;ldquo;its fine to ship bugs because the agents will fix them so quickly and at a scale humans can&amp;rsquo;t do!&amp;rdquo; We learned in infrastructure that MTTR is great but you can&amp;rsquo;t yeet resilient systems entirely.&lt;/p>&lt;/blockquote>
&lt;p>I love AI. I lean into it harder than most people I work with and know. Engineering is genuinely better because of AI. Daily and categorically.&lt;/p>
&lt;p>Purely AI written systems will scale to a point of complexity no human can ever understand. For a while it looks fine. Defects close, bug reports trend down, dashboards stay green. Then the defect close rate tapers. Token burn per defect climbs. Eventually, AI changes cause on average more defects than they close, and the whole system becomes unstable. &lt;strong>The decay.&lt;/strong>&lt;/p>
&lt;p>AI amplifies good thinking. AI amplifies good engineering. AI also amplifies bad thinking and bad engineering.&lt;/p>
&lt;p>When that happens, I expect a new type of consulting to emerge. Call it &lt;strong>Inverse AI&lt;/strong>. An involution. The same specialists who get called for an active breach, or when production data is gone and the backups failed. People who walk into a doomed AI generated codebase, distill out the core design principles nobody bothered to write down, and rebuild from scratch with good thinking and good engineering &amp;ndash; using AI.&lt;/p>
&lt;p>This time with with humans in the seat where it counts.&lt;/p>
&lt;p>The decay isn&amp;rsquo;t a reason to be against AI. It&amp;rsquo;s a reason to be honest about what comes next.&lt;/p></description></item><item><title>How I Run 1:1s</title><link>https://houdeshell.dev/post/2026-05-02_how-i-run-1on1s/</link><pubDate>Tue, 05 May 2026 09:00:00 -0400</pubDate><guid>https://houdeshell.dev/post/2026-05-02_how-i-run-1on1s/</guid><description>&lt;p>I&amp;rsquo;ve been running 1:1s for years and I still get them wrong. The version of &amp;ldquo;wrong&amp;rdquo; has changed. Early on, my 1:1s were status meetings with the lights off. Manager asks what you&amp;rsquo;re working on. Engineer recites their pile. Manager says &amp;ldquo;anything blocking you?&amp;rdquo;. Engineer says no, even when they mean yes. Forty-five minutes evaporate.&lt;/p>
&lt;p>That isn&amp;rsquo;t a 1:1. It&amp;rsquo;s a standup with a longer time slot.&lt;/p>
&lt;p>I figured out it was wrong because of one engineer. Smart, quiet, on a team I&amp;rsquo;d inherited. We&amp;rsquo;d had eight or nine &amp;ldquo;great&amp;rdquo; 1:1s in a row. He left to go work somewhere else and on his way out told me, in different words, that he didn&amp;rsquo;t think I knew anything about him. He was right. I knew what he was working on, what his blockers were, what he&amp;rsquo;d shipped that quarter. I didn&amp;rsquo;t know what he wanted out of his career, what he was sick of, what his last manager had broken in him that I was busy perpetuating. Everything I knew about him was project-shaped.&lt;/p>
&lt;p>That cost me an engineer, and it cost me about three months of figuring out how to run these meetings differently.&lt;/p>
&lt;h2 id="the-frame" class="md-heading">
The frame
&lt;a class="md-heading-anchor" href="#the-frame" aria-label="Link to The frame">#&lt;/a>
&lt;/h2>
&lt;p>This is your meeting. You set the agenda. If you want to talk through a tricky design problem, talk it through. If you want to vent about a teammate, vent. If you want to plan the next two years of your career, plan. If you want to talk about your kid&amp;rsquo;s soccer team, fine. I have a notes doc I never make you open.&lt;/p>
&lt;p>I do bring things. Three or four every week. Coaching I want to give, feedback I owe, context you&amp;rsquo;d want, a decision I want your read on. Mine goes after yours. If we run out of time and I never got to my list, that&amp;rsquo;s okay. My list keeps. Yours might not.&lt;/p>
&lt;h2 id="one-question-that-does-most-of-the-work" class="md-heading">
One question that does most of the work
&lt;a class="md-heading-anchor" href="#one-question-that-does-most-of-the-work" aria-label="Link to One question that does most of the work">#&lt;/a>
&lt;/h2>
&lt;p>I used to have a list. Three questions, same every week, the kind of thing that lands in a leadership newsletter. They worked, mostly. They also made me predictable, which made the answers predictable.&lt;/p>
&lt;p>Now I mostly ask one thing and let it breathe:&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;What&amp;rsquo;s draining you right now?&amp;rdquo;&lt;/strong>&lt;/p>
&lt;p>The honest answer is rarely the project. It&amp;rsquo;s a person, a process, or something they don&amp;rsquo;t want to admit they&amp;rsquo;re avoiding. Half the time the first answer is &amp;ldquo;nothing, I&amp;rsquo;m good,&amp;rdquo; and then five minutes later something real comes out because the question planted a seed. Naming the drain doesn&amp;rsquo;t fix it. It just lets us start.&lt;/p>
&lt;p>If they bring me energy instead, fine. I roll with it. The point of the question is to give them permission to say something they probably haven&amp;rsquo;t said out loud yet.&lt;/p>
&lt;h2 id="what-i-stopped-doing" class="md-heading">
What I stopped doing
&lt;a class="md-heading-anchor" href="#what-i-stopped-doing" aria-label="Link to What I stopped doing">#&lt;/a>
&lt;/h2>
&lt;p>I&amp;rsquo;d love to write you a clean bullet list of antipatterns. The truth is messier, and most of them are mistakes I made before I learned to stop.&lt;/p>
&lt;p>I used to ask for a status update at the top of every 1:1. I told myself I was &amp;ldquo;checking in.&amp;rdquo; I was filling silence. Now if I&amp;rsquo;m asking what they shipped, it&amp;rsquo;s because I genuinely don&amp;rsquo;t know, which means I&amp;rsquo;m not paying attention to the team. JIRA exists. Standups exist. The 1:1 isn&amp;rsquo;t where that goes.&lt;/p>
&lt;p>I used to ambush. The &amp;ldquo;by the way, we&amp;rsquo;re concerned about your output&amp;rdquo; drop, slipped into a 1:1 because I didn&amp;rsquo;t have the spine to schedule it as its own meeting. Every time I did it, I blew up trust for a quarter. Performance feedback gets its own slot, its own framing, its own heads-up. Not a sneak attack between &amp;ldquo;how was your weekend&amp;rdquo; and &amp;ldquo;what&amp;rsquo;s on your mind?&amp;rdquo;.&lt;/p>
&lt;p>I used to solve for them. They&amp;rsquo;d bring me a problem and I&amp;rsquo;d hand them the answer thirty seconds later because it felt helpful and made me feel useful. It made them dependent. &amp;ldquo;What would you do?&amp;rdquo; is the better question, even when I already know what I&amp;rsquo;d do. Especially then.&lt;/p>
&lt;h2 id="cadence" class="md-heading">
Cadence
&lt;a class="md-heading-anchor" href="#cadence" aria-label="Link to Cadence">#&lt;/a>
&lt;/h2>
&lt;p>Weekly, 30 minutes, on the calendar. Not &amp;ldquo;ad-hoc when we both have time.&amp;rdquo; Ad-hoc never happens. Ad-hoc is what people say when they don&amp;rsquo;t actually want the meeting.&lt;/p>
&lt;p>I cancel mine more than I&amp;rsquo;d like. Sprint demo collides, customer call drops in, an outage at 2am. I always reschedule. I never delete. The person sees that a 1:1 with me is a slot that survives the rest of the calendar. That matters more than they&amp;rsquo;ll ever tell me.&lt;/p>
&lt;p>Skip one, fine. Skip two in a row, that&amp;rsquo;s a signal you&amp;rsquo;re sending whether you mean to or not. Skip three and you&amp;rsquo;ve made the problem for yourself.&lt;/p>
&lt;h2 id="the-unsexy-part" class="md-heading">
The unsexy part
&lt;a class="md-heading-anchor" href="#the-unsexy-part" aria-label="Link to The unsexy part">#&lt;/a>
&lt;/h2>
&lt;p>Nobody puts this on a leadership thread because it doesn&amp;rsquo;t tweet well. The actual work of 1:1s is being there, in this meeting, every week, for years. Not the clever question. Not the great coaching moment that lives rent-free in their head five years later, though those happen and you&amp;rsquo;ll remember them. The week-after-week of showing up, paying attention, and remembering the things they told you last month so you can ask about them this month.&lt;/p>
&lt;p>Most of it is just that. Show up. Pay attention. Remember.&lt;/p>
&lt;p>The engineer who left taught me one more thing on his way out, without meaning to. He&amp;rsquo;d told me about his daughter&amp;rsquo;s blights in passing about six weeks earlier, and I never asked about it again. Not because I didn&amp;rsquo;t care. Because I forgot, and he noticed.&lt;/p>
&lt;p>I write things down now.&lt;/p></description></item><item><title>Flue, Part 3: Self-Hosting Agents with Knative</title><link>https://houdeshell.dev/post/2026-05-02_flue-knative-kubernetes/</link><pubDate>Sat, 02 May 2026 18:00:00 -0400</pubDate><guid>https://houdeshell.dev/post/2026-05-02_flue-knative-kubernetes/</guid><description>&lt;p>&lt;a href="https://houdeshell.dev/post/2026-05-02_flue-mastra-typescript-agents/">Part 1&lt;/a> was the framework pitch. &lt;a href="https://houdeshell.dev/post/2026-05-09_flue-jira-triage/">Part 2&lt;/a> was the agent I wanted to build. This one is for the people who can&amp;rsquo;t (or won&amp;rsquo;t) ship that agent to Cloudflare Workers.&lt;/p>
&lt;p>The reasons vary. Compliance says all customer data stays in our VPC. Security says no third-party serverless on the data path. Procurement won&amp;rsquo;t sign another vendor contract this fiscal year. We already run Kubernetes for everything else, so why are we paying someone to schedule containers for us? All valid.&lt;/p>
&lt;p>Self-hosting a Flue agent on Kubernetes is straightforward. The trick is doing it without rebuilding the things managed serverless gives you for free: HTTP routing, autoscaling, scale-to-zero, ingress, TLS. That&amp;rsquo;s where Knative earns its keep.&lt;/p>
&lt;h2 id="why-knative" class="md-heading">
Why Knative
&lt;a class="md-heading-anchor" href="#why-knative" aria-label="Link to Why Knative">#&lt;/a>
&lt;/h2>
&lt;p>Knative is the K8s-native answer to serverless. You hand it a container image and a port. It hands you back a public URL, request-based autoscaling, scale-to-zero when nothing is calling, and TLS through cert-manager. The whole &amp;ldquo;deploy a webhook handler&amp;rdquo; thing collapses into a single Kubernetes resource.&lt;/p>
&lt;p>For a Flue agent specifically:&lt;/p>
&lt;ul>
&lt;li>The agent boots in a few hundred milliseconds. Cold start is fine.&lt;/li>
&lt;li>Webhook traffic is bursty. Scale-to-zero saves real money.&lt;/li>
&lt;li>Inbound traffic is HTTP only. No need for a service mesh.&lt;/li>
&lt;/ul>
&lt;p>OpenFAAS works too. So does spinning up a Deployment plus Service plus Ingress plus HPA by hand. I keep coming back to Knative because the Service resource is one file and the operational story (events, observability, autoscaling) is built in.&lt;/p>
&lt;h2 id="the-dockerfile" class="md-heading">
The Dockerfile
&lt;a class="md-heading-anchor" href="#the-dockerfile" aria-label="Link to The Dockerfile">#&lt;/a>
&lt;/h2>
&lt;p>Flue ships as a Node CLI plus a function bundle. For deployment, build a small image with the function code and the SDK&amp;rsquo;s HTTP server in front:&lt;/p>
&lt;div class="code-block" data-lang="dockerfile">&lt;span class="code-lang" aria-hidden="true">dockerfile&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span class="line">&lt;span class="cl">&lt;span class="k">FROM&lt;/span>&lt;span class="s"> node:22-alpine AS deps&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">WORKDIR&lt;/span>&lt;span class="s"> /app&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> package.json package-lock.json ./&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> npm ci --omit&lt;span class="o">=&lt;/span>dev&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="s"> node:22-alpine&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">WORKDIR&lt;/span>&lt;span class="s"> /app&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> --from&lt;span class="o">=&lt;/span>deps /app/node_modules ./node_modules&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> agent.ts jira.ts ./&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> skills ./skills&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">ENV&lt;/span> &lt;span class="nv">NODE_ENV&lt;/span>&lt;span class="o">=&lt;/span>production
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">EXPOSE&lt;/span>&lt;span class="s"> 8080&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">CMD&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;npx&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;flue&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;serve&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;--host&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;0.0.0.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;--port&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;8080&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;agent.ts&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;p>Two-stage build to keep the runtime image small. The skills directory comes along since the agent reads context files at runtime. &lt;code>flue serve&lt;/code> is the SDK&amp;rsquo;s HTTP entrypoint that turns &lt;code>export default&lt;/code> into a webhook handler at &lt;code>/&lt;/code>.&lt;/p>
&lt;p>If you&amp;rsquo;re running a Bun-flavored Flue agent, swap the base image for &lt;code>oven/bun:1-alpine&lt;/code> and the CMD for &lt;code>bun run agent.ts&lt;/code>. Same shape, fewer layers.&lt;/p>
&lt;p>Push to whatever registry you&amp;rsquo;re using:&lt;/p>
&lt;div class="code-block" data-lang="bash">&lt;span class="code-lang" aria-hidden="true">bash&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker build -t registry.internal/agents/jira-triage:0.1.0 .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker push registry.internal/agents/jira-triage:0.1.0&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;h2 id="the-knative-service" class="md-heading">
The Knative Service
&lt;a class="md-heading-anchor" href="#the-knative-service" aria-label="Link to The Knative Service">#&lt;/a>
&lt;/h2>
&lt;p>The Service resource is the whole thing.&lt;/p>
&lt;div class="code-block" data-lang="yaml">&lt;span class="code-lang" aria-hidden="true">yaml&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">serving.knative.dev/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">jira-triage&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agents&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">autoscaling.knative.dev/min-scale&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;0&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">autoscaling.knative.dev/max-scale&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;10&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">autoscaling.knative.dev/target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;5&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">timeoutSeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">90&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containerConcurrency&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">registry.internal/agents/jira-triage:0.1.0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">envFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">jira-triage-secrets&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">100m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">256Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1000m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1Gi&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;p>The annotations are doing real work:&lt;/p>
&lt;ul>
&lt;li>&lt;code>min-scale: 0&lt;/code> lets the agent drop to zero replicas when nothing is calling it. That is the entire point of running this on Knative instead of a plain Deployment.&lt;/li>
&lt;li>&lt;code>max-scale: 10&lt;/code> caps blast radius. If a JIRA automation rule misfires and floods the webhook, it won&amp;rsquo;t take down the cluster.&lt;/li>
&lt;li>&lt;code>target: 5&lt;/code> plus &lt;code>containerConcurrency: 1&lt;/code> tell Knative each pod handles one in-flight request at a time, and to scale up once five concurrent requests are queued.&lt;/li>
&lt;/ul>
&lt;p>&lt;code>timeoutSeconds: 90&lt;/code> is the upper bound for a single request. Most agent runs finish in 10 to 30 seconds. Ninety gives a comfortable cushion for the slow case where the model is grinding through a long classification context. If you regularly need longer than that, you&amp;rsquo;ve left &amp;ldquo;webhook handler&amp;rdquo; territory and want a workflow runner like Argo or a queue.&lt;/p>
&lt;p>&lt;code>containerConcurrency: 1&lt;/code> is the safe default. Flue agents tend to hold an open session and a model connection per request. Sharing one Node process across multiple in-flight model calls works, but the concurrency math gets complicated and you trade simplicity for a small CPU win. Start at 1, tune later if cost matters.&lt;/p>
&lt;h2 id="secrets" class="md-heading">
Secrets
&lt;a class="md-heading-anchor" href="#secrets" aria-label="Link to Secrets">#&lt;/a>
&lt;/h2>
&lt;p>Secrets stay out of the Service manifest. Drop them in a Secret and &lt;code>envFrom&lt;/code> them in:&lt;/p>
&lt;div class="code-block" data-lang="yaml">&lt;span class="code-lang" aria-hidden="true">yaml&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">jira-triage-secrets&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agents&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Opaque&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">stringData&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ANTHROPIC_API_KEY&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;sk-ant-...&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">JIRA_HOST&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;yourorg.atlassian.net&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">JIRA_EMAIL&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;triage-bot@yourorg.com&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">JIRA_TOKEN&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;p>If you&amp;rsquo;re running External Secrets Operator or Vault Secrets Operator, point at the upstream store instead of stuffing keys into a YAML you&amp;rsquo;ll commit by accident. The Flue agent doesn&amp;rsquo;t care where the env vars come from. It just reads them.&lt;/p>
&lt;h2 id="the-webhook-url" class="md-heading">
The webhook URL
&lt;a class="md-heading-anchor" href="#the-webhook-url" aria-label="Link to The webhook URL">#&lt;/a>
&lt;/h2>
&lt;p>Knative gives the Service a public URL on whatever domain your cluster operator has configured. You&amp;rsquo;ll see it in &lt;code>kubectl get ksvc -n agents&lt;/code>:&lt;/p>
&lt;div class="code-block" data-lang="bash">&lt;span class="code-lang" aria-hidden="true">bash&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get ksvc jira-triage -n agents
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME URL READY
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">jira-triage https://jira-triage.agents.example.internal True&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;p>Point your JIRA automation rule at that URL with whatever auth you want on it. Knative supports request authentication through the standard ingress controllers, so if you want HMAC verification, JWT, or a static bearer token, the Service is just an HTTP server and you wire it in upstream.&lt;/p>
&lt;p>For HMAC (JIRA can sign webhooks), I add a small middleware layer in the Flue handler itself rather than at the ingress. That way the Service stays portable and the auth lives next to the agent code.&lt;/p>
&lt;h2 id="scaling-the-operators-view" class="md-heading">
Scaling, the operator&amp;rsquo;s view
&lt;a class="md-heading-anchor" href="#scaling-the-operators-view" aria-label="Link to Scaling, the operator’s view">#&lt;/a>
&lt;/h2>
&lt;p>Knative does its own pod scaling, but the autoscaler still has to live somewhere. The defaults are reasonable, and if your team is already running KPA (the Knative Pod Autoscaler), you don&amp;rsquo;t need to do anything. If you&amp;rsquo;d rather use HPA, swap the annotation:&lt;/p>
&lt;div class="code-block" data-lang="yaml">&lt;span class="code-lang" aria-hidden="true">yaml&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">autoscaling.knative.dev/class&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;hpa.autoscaling.knative.dev&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;p>KPA reacts faster to bursts (sub-second). HPA is steadier and integrates with your existing custom metrics pipeline. For agent workloads I&amp;rsquo;d default to KPA: bursts of webhook traffic show up first as concurrency, which KPA scales on natively.&lt;/p>
&lt;h2 id="what-you-give-up" class="md-heading">
What you give up
&lt;a class="md-heading-anchor" href="#what-you-give-up" aria-label="Link to What you give up">#&lt;/a>
&lt;/h2>
&lt;p>&lt;strong>Cold starts.&lt;/strong> The first request after a quiet period will pay 1 to 3 seconds for the Node process to come up. That&amp;rsquo;s fine for JIRA webhooks. It would be painful for a chatbot facing humans. If you can&amp;rsquo;t tolerate cold starts, set &lt;code>min-scale: 1&lt;/code> and pay for one always-warm pod.&lt;/p>
&lt;p>&lt;strong>Operational ownership.&lt;/strong> Cloudflare hands you &amp;ldquo;the function ran, here&amp;rsquo;s the log, here&amp;rsquo;s the trace.&amp;rdquo; With Knative you own the autoscaler tuning, the ingress, the cert rotation, the registry, and the K8s upgrade cycle. If your team already runs that, the marginal cost is small. If not, you might be reaching for the wrong tool.&lt;/p>
&lt;p>&lt;strong>Egress quotas.&lt;/strong> Knative defaults to no egress filtering. If you&amp;rsquo;re in a regulated environment, you&amp;rsquo;ll want NetworkPolicies in front of the agent&amp;rsquo;s namespace so it can only reach the model API and your JIRA host. The default is &amp;ldquo;open egress,&amp;rdquo; which is probably fine for the prototype and definitely not fine for prod.&lt;/p>
&lt;h2 id="observability" class="md-heading">
Observability
&lt;a class="md-heading-anchor" href="#observability" aria-label="Link to Observability">#&lt;/a>
&lt;/h2>
&lt;p>The agent already returns structured JSON for each invocation (the action it took, the reasoning summary, the issue key). On Knative, every request shows up in &lt;code>kubectl logs&lt;/code> and the queue-proxy sidecar exposes Prometheus metrics:&lt;/p>
&lt;ul>
&lt;li>&lt;code>revision_request_count&lt;/code> per Service revision&lt;/li>
&lt;li>&lt;code>request_latency&lt;/code> histogram&lt;/li>
&lt;li>&lt;code>concurrent_requests&lt;/code> gauge&lt;/li>
&lt;/ul>
&lt;p>Pair that with a ServiceMonitor and you get a dashboard for free. Add a structured logger inside the agent (pino, &lt;code>console.log&lt;/code> of structured objects, whatever your cluster&amp;rsquo;s log pipeline can parse) and you&amp;rsquo;ve got correlated logs and metrics without any custom infra.&lt;/p>
&lt;p>For traces: enable Knative&amp;rsquo;s OpenTelemetry support, or bolt on the OpenTelemetry SDK inside the Flue handler. Whichever your tracing stack already prefers.&lt;/p>
&lt;h2 id="when-this-is-the-right-tool" class="md-heading">
When this is the right tool
&lt;a class="md-heading-anchor" href="#when-this-is-the-right-tool" aria-label="Link to When this is the right tool">#&lt;/a>
&lt;/h2>
&lt;p>Self-host the Flue agent on Knative when:&lt;/p>
&lt;ul>
&lt;li>Your data plane has to stay in your VPC.&lt;/li>
&lt;li>You already run Kubernetes and have an SRE team that knows it.&lt;/li>
&lt;li>You have multiple agents and want a uniform deployment story across them all.&lt;/li>
&lt;li>Webhook traffic is bursty enough that &amp;ldquo;always running on a VM&amp;rdquo; is overkill, but consistent enough that you&amp;rsquo;d rather not pay per-request to a third party.&lt;/li>
&lt;/ul>
&lt;p>Reach for managed serverless when:&lt;/p>
&lt;ul>
&lt;li>You don&amp;rsquo;t have a K8s team, or you have one and they&amp;rsquo;re already at capacity.&lt;/li>
&lt;li>You want one less thing to babysit.&lt;/li>
&lt;li>The data is fine to leave the building.&lt;/li>
&lt;/ul>
&lt;p>Both shapes work. Both are real. The Flue agent code doesn&amp;rsquo;t change between them, which is the part I keep finding satisfying: you can develop on Workers, prove the agent out in the small, and self-host the same code in your cluster the day someone in security says &amp;ldquo;no, that data stays put.&amp;rdquo;&lt;/p></description></item><item><title>Flue, Part 2: Stop Triaging JIRA By Hand</title><link>https://houdeshell.dev/post/2026-05-09_flue-jira-triage/</link><pubDate>Sat, 02 May 2026 15:00:00 -0400</pubDate><guid>https://houdeshell.dev/post/2026-05-09_flue-jira-triage/</guid><description>&lt;p>&lt;a href="https://houdeshell.dev/post/2026-05-02_flue-mastra-typescript-agents/">Part 1&lt;/a> was the framework pitch. This is the agent I actually wanted to build with it.&lt;/p>
&lt;p>The setup is something I keep seeing across teams: JIRA triage is brutal. Tickets pile up in the new-issues queue. An EM or tech lead spends an hour every morning sorting them. Someone classifies, someone re-classifies, someone routes, someone re-routes. Half of them get assigned to the wrong team and bounce around for two days before landing.&lt;/p>
&lt;p>The agent I&amp;rsquo;m going to walk through does that triage work. It reads each new ticket, classifies it, picks a team and a person, and either assigns it or comments on the ticket asking for human eyes. It never guesses. If it isn&amp;rsquo;t confident, the human gets a structured handoff instead of a wrong assignee.&lt;/p>
&lt;h2 id="the-plan" class="md-heading">
The plan
&lt;a class="md-heading-anchor" href="#the-plan" aria-label="Link to The plan">#&lt;/a>
&lt;/h2>
&lt;p>Two skills:&lt;/p>
&lt;ol>
&lt;li>&lt;code>classify-ticket&lt;/code>: reads the ticket and picks a category, domain, and priority. Self-reports confidence.&lt;/li>
&lt;li>&lt;code>route-ticket&lt;/code>: given that classification, picks a team and a specific assignee. Also self-reports confidence.&lt;/li>
&lt;/ol>
&lt;p>Then a tiny piece of glue:&lt;/p>
&lt;ul>
&lt;li>If both skills come back high-confidence, assign + label, log, done.&lt;/li>
&lt;li>Otherwise, comment on the ticket with the agent&amp;rsquo;s reasoning, label &lt;code>needs-triage&lt;/code>, leave the assignee blank.&lt;/li>
&lt;/ul>
&lt;p>The whole thing runs on a JIRA webhook. New issue created? Webhook fires, Flue runs, two API calls back to JIRA, response sent. Total wall-clock time: a few seconds.&lt;/p>
&lt;h2 id="the-agent" class="md-heading">
The agent
&lt;a class="md-heading-anchor" href="#the-agent" aria-label="Link to The agent">#&lt;/a>
&lt;/h2>
&lt;div class="code-block" data-lang="typescript">&lt;span class="code-lang" aria-hidden="true">typescript&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="kr">type&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">FlueContext&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;@flue/sdk/client&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="kr">as&lt;/span> &lt;span class="nx">v&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;valibot&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="kr">as&lt;/span> &lt;span class="nx">jira&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;./jira&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">export&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">triggers&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">webhook&lt;/span>: &lt;span class="kt">true&lt;/span> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">Confidence&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">picklist&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s1">&amp;#39;high&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;medium&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;low&amp;#39;&lt;/span>&lt;span class="p">]);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">Classification&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="kt">object&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">category&lt;/span>: &lt;span class="kt">v.picklist&lt;/span>&lt;span class="p">([&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;bug&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;feature-request&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;support&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;security&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;infra&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;docs&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;duplicate&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;spam&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">domain&lt;/span>: &lt;span class="kt">v.picklist&lt;/span>&lt;span class="p">([&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;frontend&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;backend&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;database&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;auth&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;billing&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;reporting&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;kubernetes&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;unknown&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">priority&lt;/span>: &lt;span class="kt">v.picklist&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s1">&amp;#39;low&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;medium&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;high&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;critical&amp;#39;&lt;/span>&lt;span class="p">]),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">reasoning&lt;/span>: &lt;span class="kt">v.string&lt;/span>&lt;span class="p">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">confidence&lt;/span>: &lt;span class="kt">Confidence&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">Routing&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="kt">object&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">team&lt;/span>: &lt;span class="kt">v.string&lt;/span>&lt;span class="p">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">assignee&lt;/span>: &lt;span class="kt">v.union&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="nx">v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="kt">string&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="nx">v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">()]),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">reasoning&lt;/span>: &lt;span class="kt">v.string&lt;/span>&lt;span class="p">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">confidence&lt;/span>: &lt;span class="kt">Confidence&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">({&lt;/span> &lt;span class="nx">init&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">payload&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">env&lt;/span> &lt;span class="p">}&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">FlueContext&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">issue&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">payload&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">agent&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="nx">init&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">model&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;anthropic/claude-opus-4-7&amp;#39;&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">session&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="nx">agent&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">session&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">classification&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="nx">session&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">skill&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;classify-ticket&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">args&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">summary&lt;/span>: &lt;span class="kt">issue.fields.summary&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">description&lt;/span>: &lt;span class="kt">issue.fields.description&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">reporter&lt;/span>: &lt;span class="kt">issue.fields.reporter.displayName&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">labels&lt;/span>: &lt;span class="kt">issue.fields.labels&lt;/span> &lt;span class="o">??&lt;/span> &lt;span class="p">[],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">result&lt;/span>: &lt;span class="kt">Classification&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">routing&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="nx">session&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">skill&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;route-ticket&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">args&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">classification&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">summary&lt;/span>: &lt;span class="kt">issue.fields.summary&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">description&lt;/span>: &lt;span class="kt">issue.fields.description&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">result&lt;/span>: &lt;span class="kt">Routing&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">confident&lt;/span> &lt;span class="o">=&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">classification&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">confidence&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;high&amp;#39;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">routing&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">confidence&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;high&amp;#39;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">routing&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">assignee&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">confident&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">jira&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">assignIssue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">issue&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">key&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">routing&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">assignee&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">jira&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">applyLabels&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">issue&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">key&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sb">`category/&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">classification&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">category&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sb">`domain/&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">classification&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">domain&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sb">`priority/&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">classification&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">priority&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sb">`team/&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">routing&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">team&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">action&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;assigned&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">issue&lt;/span>: &lt;span class="kt">issue.key&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">...&lt;/span>&lt;span class="nx">routing&lt;/span> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="nx">escalate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">issue&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">key&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">classification&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">routing&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">escalate&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">env&lt;/span>: &lt;span class="kt">any&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">key&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">c&lt;/span>: &lt;span class="kt">v.InferOutput&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">typeof&lt;/span> &lt;span class="na">Classification&lt;/span>&lt;span class="p">&amp;gt;,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">r&lt;/span>: &lt;span class="kt">v.InferOutput&lt;/span>&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">typeof&lt;/span> &lt;span class="na">Routing&lt;/span>&lt;span class="p">&amp;gt;,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">body&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sb">`Triage agent escalated this ticket for human review.`&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sb">``&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sb">`*Classification (&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">confidence&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">):*`&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sb">`- Category: &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">category&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sb">`- Domain: &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">domain&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sb">`- Priority: &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">priority&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sb">`- Reasoning: &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">reasoning&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sb">``&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sb">`*Suggested routing (&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">confidence&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">):*`&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sb">`- Team: &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">team&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sb">`- Assignee: &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">assignee&lt;/span> &lt;span class="o">??&lt;/span> &lt;span class="s1">&amp;#39;unsure&amp;#39;&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sb">`- Reasoning: &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">reasoning&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">].&lt;/span>&lt;span class="nx">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;\n&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">jira&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addComment&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">key&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">body&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">jira&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">applyLabels&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">key&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;needs-triage&amp;#39;&lt;/span>&lt;span class="p">]);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">action&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;escalated&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">issue&lt;/span>: &lt;span class="kt">key&lt;/span> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;p>Two skill calls, one branch. The whole file fits on a single screen.&lt;/p>
&lt;p>The JIRA helper is small enough to inline. Three calls: assign, comment, label.&lt;/p>
&lt;div class="code-block" data-lang="typescript">&lt;span class="code-lang" aria-hidden="true">typescript&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// jira.ts
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">base&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">env&lt;/span>: &lt;span class="kt">any&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="sb">`https://&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">JIRA_HOST&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">/rest/api/3`&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">headers&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">env&lt;/span>: &lt;span class="kt">any&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Authorization&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="sb">`Basic &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">btoa&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">JIRA_EMAIL&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">:&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">JIRA_TOKEN&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;Content-Type&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;application/json&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">export&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">assignIssue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">env&lt;/span>: &lt;span class="kt">any&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">key&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">accountId&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">fetch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">base&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">/issue/&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">key&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">/assignee`&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">method&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;PUT&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">headers&lt;/span>: &lt;span class="kt">headers&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">body&lt;/span>: &lt;span class="kt">JSON.stringify&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">accountId&lt;/span> &lt;span class="p">}),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">export&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">addComment&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">env&lt;/span>: &lt;span class="kt">any&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">key&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">text&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">fetch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">base&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">/issue/&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">key&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">/comment`&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">method&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;POST&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">headers&lt;/span>: &lt;span class="kt">headers&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">body&lt;/span>: &lt;span class="kt">JSON.stringify&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">body&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;doc&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">version&lt;/span>: &lt;span class="kt">1&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">content&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span> &lt;span class="kr">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;paragraph&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">content&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[{&lt;/span> &lt;span class="kr">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;text&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">text&lt;/span> &lt;span class="p">}]&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">export&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">applyLabels&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">env&lt;/span>: &lt;span class="kt">any&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">key&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">labels&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">[])&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="nx">fetch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">base&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">/issue/&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">key&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">method&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;PUT&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">headers&lt;/span>: &lt;span class="kt">headers&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">body&lt;/span>: &lt;span class="kt">JSON.stringify&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">update&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">labels&lt;/span>: &lt;span class="kt">labels.map&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="nx">l&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">({&lt;/span> &lt;span class="nx">add&lt;/span>: &lt;span class="kt">l&lt;/span> &lt;span class="p">}))&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;h2 id="the-skills" class="md-heading">
The skills
&lt;a class="md-heading-anchor" href="#the-skills" aria-label="Link to The skills">#&lt;/a>
&lt;/h2>
&lt;p>A Flue skill is a directory. It has a prompt template and zero or more context files the model reads before answering. That&amp;rsquo;s where the &amp;ldquo;advanced reasoning&amp;rdquo; actually lives, in the context the model gets to look at before it commits to a structured output.&lt;/p>
&lt;p>Here is &lt;code>skills/classify-ticket/prompt.md&lt;/code>:&lt;/p>
&lt;div class="code-block" data-lang="text">&lt;span class="code-lang" aria-hidden="true">text&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">You are a triage assistant for our engineering org. Classify the JIRA ticket
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">below.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Use these context files to understand the categories, domains, and our
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">priority rubric:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- /context/categories.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- /context/domains.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- /context/priority.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- /context/historical-examples.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Process:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">1. Read the ticket and the context files.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">2. Pick the single most accurate category and domain.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">3. Pick a priority based on /context/priority.md, not based on how loudly
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> the reporter complains.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">4. Set confidence based on how clean the match is. If you have to choose
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> between two domains, that is &amp;#34;medium&amp;#34; at best. If the ticket is too
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> vague to map confidently, that is &amp;#34;low&amp;#34;.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">5. In `reasoning`, write 2 to 3 sentences explaining the call. This is
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> what a human will read if you escalate.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Never guess. We would rather have a human triage an ambiguous ticket than
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">auto-assign a wrong one.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">The ticket:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- Summary: {{ args.summary }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- Description: {{ args.description }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- Reporter: {{ args.reporter }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- Labels: {{ args.labels }}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;p>Three things matter in that prompt. The structured output schema enforces that the model picks from a finite set of categories. The &amp;ldquo;never guess&amp;rdquo; instruction backed by the &lt;code>confidence&lt;/code> field gives it permission to bail out. The context files give it the org-specific knowledge that the base model can&amp;rsquo;t have.&lt;/p>
&lt;p>&lt;code>skills/route-ticket/&lt;/code> is the same shape. Its own prompt, its own context:&lt;/p>
&lt;ul>
&lt;li>&lt;code>/context/teams.md&lt;/code>: what each team owns, with examples.&lt;/li>
&lt;li>&lt;code>/context/people.md&lt;/code>: who&amp;rsquo;s on each team, what they specialize in, and who&amp;rsquo;s currently OOO.&lt;/li>
&lt;li>&lt;code>/context/ownership-by-domain.md&lt;/code>: table of domain to primary team, with edge cases.&lt;/li>
&lt;/ul>
&lt;p>The OOO file is the under-rated piece. If the model&amp;rsquo;s first pick is on vacation, it should fall back to the team&amp;rsquo;s secondary, not barrel through with a stale assignment that nobody picks up for a week.&lt;/p>
&lt;h2 id="what-confident-actually-means-here" class="md-heading">
What &amp;ldquo;confident&amp;rdquo; actually means here
&lt;a class="md-heading-anchor" href="#what-confident-actually-means-here" aria-label="Link to What “confident” actually means here">#&lt;/a>
&lt;/h2>
&lt;p>The confidence field isn&amp;rsquo;t magic. It&amp;rsquo;s the model self-reporting after reading the prompt and the context files. It&amp;rsquo;s wrong sometimes. Two things make it less wrong:&lt;/p>
&lt;p>&lt;strong>Two paths, one decision.&lt;/strong> I run both skills before deciding. A &amp;ldquo;high&amp;rdquo; on classification but &amp;ldquo;medium&amp;rdquo; on routing means we ask a human, even though the model thinks it has half the answer. In practice, &amp;ldquo;the routing is shaky&amp;rdquo; is the most common reason to escalate, and I want that signal preserved instead of averaged away.&lt;/p>
&lt;p>&lt;strong>No null assignees on the happy path.&lt;/strong> If the routing skill returns &lt;code>assignee: null&lt;/code> (it knows the team but not the person), I treat that as not-confident even if the model self-reports &amp;ldquo;high&amp;rdquo;. The shape of the result is doing some of the gating work for me.&lt;/p>
&lt;p>You could tighten this further. Numeric confidence with explicit thresholds, an explicit &amp;ldquo;needs more info&amp;rdquo; exit code from the skill, retries with deeper context. I haven&amp;rsquo;t needed those yet. The two-skill, two-confidence pattern catches the bulk of bad routings before they happen.&lt;/p>
&lt;h2 id="the-escalation-comment-is-the-product" class="md-heading">
The escalation comment is the product
&lt;a class="md-heading-anchor" href="#the-escalation-comment-is-the-product" aria-label="Link to The escalation comment is the product">#&lt;/a>
&lt;/h2>
&lt;p>When the agent isn&amp;rsquo;t sure, the JIRA comment is where it earns its keep. Look at what it writes:&lt;/p>
&lt;div class="code-block" data-lang="text">&lt;span class="code-lang" aria-hidden="true">text&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">Triage agent escalated this ticket for human review.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">*Classification (medium):*
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- Category: support
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- Domain: billing
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- Priority: medium
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- Reasoning: Customer reports a charge they don&amp;#39;t recognize. Could be
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> a duplicate from the recent retry storm or a real billing bug. Treating
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> as support until billing confirms which one.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">*Suggested routing (low):*
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- Team: billing
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- Assignee: unsure
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">- Reasoning: Billing team owns this domain, but they&amp;#39;re rotating an
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> on-call this week and I can&amp;#39;t tell from /context/people.md who is
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> actually picking things up today. Recommend an EM eyeball this and
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> route.&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;p>A human reading that has all the work pre-loaded. The category guess. The domain guess. The reasoning. The reason it stopped short. They&amp;rsquo;re not triaging from scratch. They&amp;rsquo;re auditing a draft.&lt;/p>
&lt;p>That&amp;rsquo;s the actual UX win. Even when the agent fails, it fails in a way that saves the human time.&lt;/p>
&lt;h2 id="deploying-it" class="md-heading">
Deploying it
&lt;a class="md-heading-anchor" href="#deploying-it" aria-label="Link to Deploying it">#&lt;/a>
&lt;/h2>
&lt;p>Flue runs in a few places. For something this small I&amp;rsquo;d put it on Cloudflare Workers. The request flow is &amp;ldquo;JIRA webhook, 30 seconds of model calls, JIRA REST API.&amp;rdquo; No long-running compute, no filesystem, just a function that eats a webhook.&lt;/p>
&lt;p>Point JIRA&amp;rsquo;s automation rule at your worker URL, configure secrets in Cloudflare for the JIRA token and the Anthropic key, push with &lt;code>wrangler deploy&lt;/code>. The whole thing has fewer moving parts than the legacy &amp;ldquo;triage Slack channel&amp;rdquo; my team used to run.&lt;/p>
&lt;h2 id="where-this-stops-working" class="md-heading">
Where this stops working
&lt;a class="md-heading-anchor" href="#where-this-stops-working" aria-label="Link to Where this stops working">#&lt;/a>
&lt;/h2>
&lt;p>It is not a replacement for an EM. It is not a replacement for a tech lead with context. The agent is good at the volume case: routine tickets, clear ownership, well-documented domains. It will struggle at the long tail:&lt;/p>
&lt;ul>
&lt;li>Cross-team tickets that legitimately span two domains.&lt;/li>
&lt;li>Tickets where the customer is using the wrong vocabulary for the system.&lt;/li>
&lt;li>Anything political. Reorgs, ownership disputes, &amp;ldquo;you broke our launch.&amp;rdquo;&lt;/li>
&lt;/ul>
&lt;p>The escalation path is what catches those. As long as the agent is willing to stop and ask, the long tail goes to a human anyway. The agent&amp;rsquo;s job isn&amp;rsquo;t to be right about everything. It&amp;rsquo;s to clear the boring 70% so humans can focus on the interesting 30%.&lt;/p>
&lt;p>That&amp;rsquo;s the same pattern I keep finding for these harness-based agents in general. They don&amp;rsquo;t replace the senior person. They eat the toil that the senior person hates doing, and they hand back the genuinely hard cases with a useful first draft. On net, an excellent trade.&lt;/p></description></item><item><title>Flue vs Mastra: TypeScript Agents in 2026</title><link>https://houdeshell.dev/post/2026-05-02_flue-mastra-typescript-agents/</link><pubDate>Sat, 02 May 2026 09:00:00 -0400</pubDate><guid>https://houdeshell.dev/post/2026-05-02_flue-mastra-typescript-agents/</guid><description>&lt;p>I&amp;rsquo;ve been on a kick lately, building small agents in TypeScript instead of Python. Two frameworks keep showing up in my browser tabs: &lt;a href="https://flueframework.com/"target="_blank" rel="noopener noreferrer">Flue&lt;/a> and &lt;a href="https://mastra.ai/"target="_blank" rel="noopener noreferrer">Mastra&lt;/a>. They&amp;rsquo;re both betting on the same thing: &lt;strong>TypeScript is the right language to ship AI in&lt;/strong>. They just disagree about almost everything else.&lt;/p>
&lt;p>That disagreement is the interesting part.&lt;/p>
&lt;h2 id="the-harness-vs-the-workflow" class="md-heading">
The harness vs the workflow
&lt;a class="md-heading-anchor" href="#the-harness-vs-the-workflow" aria-label="Link to The harness vs the workflow">#&lt;/a>
&lt;/h2>
&lt;p>Flue&amp;rsquo;s pitch is one line: &lt;code>Agent = Model + Harness&lt;/code>. The framework gives you a programmable harness. Sandbox, filesystem, bash, sessions, skills with structured output. You drop in a model. You get something that looks a lot like Claude Code or Codex, except &lt;em>you&lt;/em> wrote it and &lt;em>you&lt;/em> deploy it.&lt;/p>
&lt;p>Here&amp;rsquo;s a Flue agent that triages a GitHub issue:&lt;/p>
&lt;div class="code-block" data-lang="typescript">&lt;span class="code-lang" aria-hidden="true">typescript&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="kr">type&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">FlueContext&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;@flue/sdk/client&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="kr">as&lt;/span> &lt;span class="nx">v&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;valibot&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">({&lt;/span> &lt;span class="nx">init&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">payload&lt;/span> &lt;span class="p">}&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">FlueContext&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">agent&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="nx">init&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">model&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;anthropic/claude-opus-4-7&amp;#39;&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">session&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="nx">agent&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">session&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">triage&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="nx">session&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">skill&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;triage&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">args&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">issueNumber&lt;/span>: &lt;span class="kt">payload.issueNumber&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">result&lt;/span>: &lt;span class="kt">v.object&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">severity&lt;/span>: &lt;span class="kt">v.picklist&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s1">&amp;#39;low&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;medium&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;high&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;critical&amp;#39;&lt;/span>&lt;span class="p">]),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">reproducible&lt;/span>: &lt;span class="kt">v.boolean&lt;/span>&lt;span class="p">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">summary&lt;/span>: &lt;span class="kt">v.string&lt;/span>&lt;span class="p">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">triage&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;p>The whole shape is: init the agent, open a session, call a skill, get a typed result back. The skill is a directory of prompts and tools that the harness orchestrates. Your code never has to babysit a tool-call loop. The framework is doing it.&lt;/p>
&lt;p>Mastra goes the other way. The core abstractions are &lt;strong>agents&lt;/strong>, &lt;strong>workflows&lt;/strong>, and &lt;strong>RAG&lt;/strong>. You compose them, evaluate them, and deploy them as APIs next to your Next.js or Hono app.&lt;/p>
&lt;p>A roughly equivalent Mastra agent:&lt;/p>
&lt;div class="code-block" data-lang="typescript">&lt;span class="code-lang" aria-hidden="true">typescript&lt;/span>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">Agent&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;@mastra/core&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">z&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="kr">from&lt;/span> &lt;span class="s1">&amp;#39;zod&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">export&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">triageAgent&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Agent&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;issue-triage&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">instructions&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;You triage GitHub issues. Be concise.&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">model&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">provider&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;ANTHROPIC&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;claude-opus-4-7&amp;#39;&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">export&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">triage&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">issueNumber&lt;/span>: &lt;span class="kt">number&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="nx">triageAgent&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">generate&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sb">`Triage issue #&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">issueNumber&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">output&lt;/span>: &lt;span class="kt">z.object&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">severity&lt;/span>: &lt;span class="kt">z.enum&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s1">&amp;#39;low&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;medium&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;high&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;critical&amp;#39;&lt;/span>&lt;span class="p">]),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">reproducible&lt;/span>: &lt;span class="kt">z.boolean&lt;/span>&lt;span class="p">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">summary&lt;/span>: &lt;span class="kt">z.string&lt;/span>&lt;span class="p">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">result&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="kt">object&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/div>
&lt;p>Different shape entirely. You declare the agent up front, you call &lt;code>.generate()&lt;/code> with a structured output schema, you get an object back. There&amp;rsquo;s no harness loop driving sessions and skills. There&amp;rsquo;s just a model with tools, and you wire it into your app like any other service.&lt;/p>
&lt;h2 id="where-they-diverge" class="md-heading">
Where they diverge
&lt;a class="md-heading-anchor" href="#where-they-diverge" aria-label="Link to Where they diverge">#&lt;/a>
&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>Flue&lt;/th>
&lt;th>Mastra&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Mental model&lt;/td>
&lt;td>Agent = Model + Harness&lt;/td>
&lt;td>Agent = LLM with tools&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Primitives&lt;/td>
&lt;td>Sessions, skills, sandboxes&lt;/td>
&lt;td>Agents, workflows, RAG&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>What you bring&lt;/td>
&lt;td>Skill prompts and tools&lt;/td>
&lt;td>Instructions, tools, eval suites&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Deploy targets&lt;/td>
&lt;td>Node, Cloudflare Workers, GitHub Actions&lt;/td>
&lt;td>Anywhere TypeScript runs, plus Mastra Cloud&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Replaces&lt;/td>
&lt;td>Dosu, Greptile, CodeRabbit&lt;/td>
&lt;td>Honestly, half of LangChain&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tagline&lt;/td>
&lt;td>&amp;ldquo;Stop renting someone else&amp;rsquo;s agent&amp;rdquo;&lt;/td>
&lt;td>&amp;ldquo;Python trains, TypeScript ships&amp;rdquo;&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>If you&amp;rsquo;re building something that &lt;em>acts&lt;/em> (writes files, runs commands, reads a repo, works through a task across many tool calls), Flue&amp;rsquo;s harness model is doing more for you. If you&amp;rsquo;re embedding an agent into an existing app and the bigger problem is glue, eval, and observability, Mastra has more of the production scaffolding ready to go.&lt;/p>
&lt;p>I don&amp;rsquo;t think one is winning yet. They might both be right for different jobs.&lt;/p>
&lt;h2 id="the-thing-nobodys-saying-out-loud" class="md-heading">
The thing nobody&amp;rsquo;s saying out loud
&lt;a class="md-heading-anchor" href="#the-thing-nobodys-saying-out-loud" aria-label="Link to The thing nobody’s saying out loud">#&lt;/a>
&lt;/h2>
&lt;p>Both of these projects assume something I would have laughed at five years ago: that TypeScript is a serious language for building AI infrastructure. Not a frontend duct tape. Not a glue layer. The actual substrate.&lt;/p>
&lt;p>A couple weeks back, &lt;a href="https://devblogs.microsoft.com/typescript/announcing-typescript-7-0-beta/"target="_blank" rel="noopener noreferrer">Microsoft shipped the TypeScript 7.0 beta&lt;/a>, and the punchline is that the compiler is no longer written in TypeScript. It&amp;rsquo;s written in Go. Native code, shared memory parallelism, the whole thing. They&amp;rsquo;re claiming &lt;strong>about 10x faster&lt;/strong> than 6.0 on real codebases. The language server, too. The editing experience is reportedly night and day.&lt;/p>
&lt;p>Read that twice. The TypeScript team looked at the language they invented, decided it wasn&amp;rsquo;t fast enough to compile itself, and rewrote the toolchain in Go. That&amp;rsquo;s a remarkable thing to do.&lt;/p>
&lt;p>It also reframes everything. If &lt;code>tsc&lt;/code> is fast enough that incremental compiles feel like Go&amp;rsquo;s build times, the cost-of-iteration argument against TypeScript for serious systems work mostly evaporates. The other arguments (no real concurrency, GC pauses, V8 quirks) are runtime arguments, and most agent code is I/O bound on a model API anyway.&lt;/p>
&lt;p>So you get a typed, npm-friendly, JIT&amp;rsquo;d runtime with a now-fast toolchain, and the LLM ecosystem has decided to publish first-class SDKs for it. That&amp;rsquo;s a pretty good place for a framework to land.&lt;/p>
&lt;p>Flue and Mastra are both betting on it. They might both be right.&lt;/p>
&lt;h2 id="what-id-actually-pick" class="md-heading">
What I&amp;rsquo;d actually pick
&lt;a class="md-heading-anchor" href="#what-id-actually-pick" aria-label="Link to What I’d actually pick">#&lt;/a>
&lt;/h2>
&lt;p>For a coding agent, an issue triage bot, anything that needs to &lt;em>do&lt;/em> work in a sandbox: Flue. The harness model is the right level of abstraction for that kind of work, and &amp;ldquo;drop your code in a Cloudflare Worker, point a webhook at it&amp;rdquo; is a stupidly nice deployment story.&lt;/p>
&lt;p>For a chat-shaped assistant living inside a real product, with eval suites and traces and the whole observability story: Mastra. The framework gets out of your way and the tooling around it is more mature.&lt;/p>
&lt;p>For everything else: keep an eye on both. The TypeScript-as-AI-substrate bet is going to play out fast, and a 10x faster compiler landing the same year is not a coincidence I would have predicted.&lt;/p></description></item></channel></rss>