<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community</title>
    <description>The most recent home feed on DEV Community.</description>
    <link>https://dev.to</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/rss"/>
    <language>en</language>
    <item>
      <title>Hermes Agent Burned 603M Tokens Behind My Back — I Cut Background Costs by Up to 125x</title>
      <dc:creator>Azamat Safarov</dc:creator>
      <pubDate>Tue, 02 Jun 2026 00:03:08 +0000</pubDate>
      <link>https://dev.to/azamat_safarov/hermes-agent-burned-603m-tokens-behind-my-back-i-cut-background-costs-by-up-to-125x-43oi</link>
      <guid>https://dev.to/azamat_safarov/hermes-agent-burned-603m-tokens-behind-my-back-i-cut-background-costs-by-up-to-125x-43oi</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fomw79knqbhrxwxwaqz80.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fomw79knqbhrxwxwaqz80.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Last Tuesday I noticed my &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama Cloud Pro&lt;/a&gt; quota draining faster than usual. Way faster. I had burned through &lt;strong&gt;603 million tokens&lt;/strong&gt; in seven days without understanding where they went.&lt;/p&gt;

&lt;p&gt;I opened my &lt;a href="https://github.com/nousresearch/hermes-agent" rel="noopener noreferrer"&gt;Hermes Agent&lt;/a&gt; logs and found something I did not know existed: an &lt;code&gt;auxiliary:&lt;/code&gt; block with twelve background tasks. Compression, web extraction, vision, session search, skills matching — all running silently every time I typed a message. Every task was set to &lt;code&gt;provider: auto&lt;/code&gt;. And because I had no API keys for the fallback chain, every one silently fell back to &lt;code&gt;kimi-k2.6&lt;/code&gt;, my one-trillion-parameter main model.&lt;/p&gt;

&lt;p&gt;I had no idea this was happening. The agent was sending eleven background prompts to the same model I was actively chatting with, through the same quota, without showing me the prompts. Compression alone fired 10–20 times per long session, each pass sending the full conversation history.&lt;/p&gt;

&lt;p&gt;This is what I fixed immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Here is what I changed in the &lt;code&gt;auxiliary&lt;/code&gt; block of my &lt;code&gt;~/.hermes/config.yaml&lt;/code&gt;. The complete YAML is in the &lt;strong&gt;Full Config&lt;/strong&gt; section below.&lt;/p&gt;

&lt;p&gt;Apply with &lt;code&gt;/reset&lt;/code&gt; or restart Hermes. Config changes only take effect on new sessions.&lt;/p&gt;

&lt;p&gt;If you just want the config and don't care about the story, jump to &lt;strong&gt;Full Config&lt;/strong&gt; below, copy it, &lt;code&gt;/reset&lt;/code&gt;, done.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Routing Works
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshxvazrv64ggackzpnzo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshxvazrv64ggackzpnzo.png" alt="Before and after: default provider auto vs optimized explicit routing" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Twelve tasks used to collapse into one trillion-parameter model. Now they are distributed across six models, from 8B to 1T.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;provider: auto&lt;/code&gt; Actually Does
&lt;/h2&gt;

&lt;p&gt;I searched dozens of Hermes guides online. Not a single one mentions the &lt;code&gt;auxiliary&lt;/code&gt; block. The official docs describe the YAML structure, but there's no warning that &lt;code&gt;provider: auto&lt;/code&gt; silently falls back to your main model. I only found one &lt;a href="https://www.youtube.com/watch?v=NoF-YajElIM&amp;amp;list=PLmpUb_PWAkDxewld5ZYyKifuHxgIbiq2d&amp;amp;index=14" rel="noopener noreferrer"&gt;video by AI Garage&lt;/a&gt; discussing this — nothing else. No blog posts, no Discord threads, no Reddit discussions.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;auto&lt;/code&gt; chain is: &lt;code&gt;openrouter → new portal → codex → gemini flash&lt;/code&gt;. If none of these backends have an API key configured, it falls back to your &lt;strong&gt;main chat model&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So while I was typing one message to k2.6, the agent was sending eleven others to the same model through the same quota, without showing me the prompt. Compression alone was firing 10–20 times per long session, sending the full conversation history every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Ollama Cloud Pro Catalog
&lt;/h2&gt;

&lt;p&gt;I have an Ollama Cloud Pro subscription. Here are the models available in my catalog that matter for routing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Strength&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kimi-k2.6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~1T params, 256K context&lt;/td&gt;
&lt;td&gt;Reasoning, architecture, debugging&lt;/td&gt;
&lt;td&gt;Main chat only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kimi-k2.5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~1T params&lt;/td&gt;
&lt;td&gt;Same family, optimized for long context&lt;/td&gt;
&lt;td&gt;Summarization, compression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen3-vl:235b-instruct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;235B params&lt;/td&gt;
&lt;td&gt;Multimodal (vision + text)&lt;/td&gt;
&lt;td&gt;Screenshots, image analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deepseek-v4-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~20B params&lt;/td&gt;
&lt;td&gt;Fast, good at structured output&lt;/td&gt;
&lt;td&gt;Safety checks, classification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemma3:12b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12B params&lt;/td&gt;
&lt;td&gt;Lightweight, fast&lt;/td&gt;
&lt;td&gt;Triage, profile tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rnj-1:8b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8B params&lt;/td&gt;
&lt;td&gt;Cheapest in catalog&lt;/td&gt;
&lt;td&gt;Titles, search, skills matching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemma4:e2b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2B params&lt;/td&gt;
&lt;td&gt;Smallest&lt;/td&gt;
&lt;td&gt;Not used — too weak for any auxiliary task&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I pulled all of these and tested them against the twelve auxiliary tasks. The routing below is the result of that testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Twelve Background Tasks
&lt;/h2&gt;

&lt;p&gt;My Hermes version has twelve auxiliary tasks. I ordered them by how much they cost in practice:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Why it costs money&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;compression&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Summarizes conversation when context exceeds limits&lt;/td&gt;
&lt;td&gt;Fires 10–20 times per long session. Each pass sends the full conversation history.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;web_extract&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Strips HTML boilerplate after &lt;code&gt;web_search&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Fires every time you search the web.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vision&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Processes screenshots and images&lt;/td&gt;
&lt;td&gt;Multimodal tokens are expensive.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;flush_memories&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Writes facts to memory files on &lt;code&gt;/new&lt;/code&gt; or &lt;code&gt;/exit&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Runs at every session end.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kanban_decomposer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Breaks Kanban tasks into steps&lt;/td&gt;
&lt;td&gt;Medium complexity, runs on board operations.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;code&gt;curator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Analyzes skill quality and redundancy&lt;/td&gt;
&lt;td&gt;Heavy analysis task.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;code&gt;session_search&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Searches past sessions and summarizes matches&lt;/td&gt;
&lt;td&gt;Runs when you look up old conversations.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;code&gt;skills_hub&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Matches your query to installed skills&lt;/td&gt;
&lt;td&gt;Runs on most questions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;code&gt;triage_specifier&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Classifies incoming messages&lt;/td&gt;
&lt;td&gt;Binary classification.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;code&gt;approval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Binary safety check before terminal commands&lt;/td&gt;
&lt;td&gt;Simple yes/no on safety.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;&lt;code&gt;profile_describer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Generates profile bio&lt;/td&gt;
&lt;td&gt;Rare, lightweight.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;&lt;code&gt;title_generation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Auto-names new sessions&lt;/td&gt;
&lt;td&gt;Trivial, runs constantly.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note: older Hermes versions (like the one in the AI Garage video below) show eight tasks. My version has twelve — four were added in recent updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Mapped Models to Tasks
&lt;/h2&gt;

&lt;p&gt;The logic is dead simple: put the lightest model that doesn't break on each task, and keep k2.6 for actual conversations.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Why this one&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Main chat&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kimi-k2.6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Architecture, debugging, discussion. The only task that actually needs a trillion parameters.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compression, web_extract, kanban, curator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kimi-k2.5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same &lt;a href="https://www.moonshot.cn" rel="noopener noreferrer"&gt;Kimi&lt;/a&gt; family, optimized for long context. Summarization quality stays high. Using k2.6 for compression was burning quota for no quality gain.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vision&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qwen3-vl:235b-instruct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only multimodal model in the catalog at this level. No alternative exists for image analysis.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Triage, profile&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemma3:12b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12B params vs 1T. Classification and bio generation do not need reasoning depth.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Approval&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;deepseek-v4-flash&lt;/code&gt; (~20B)&lt;/td&gt;
&lt;td&gt;Binary safety check. Fast response time matters more than reasoning quality.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Titles, search, skills, MCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rnj-1:8b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8B params. 125 times lighter than k2.6. The bulk of the savings are here — these tasks run constantly but need minimal intelligence.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I Tried First: Local Models
&lt;/h2&gt;

&lt;p&gt;Before settling on cloud routing, I tried running auxiliary tasks locally. I already had &lt;code&gt;gemma4:e2b&lt;/code&gt; pulled via Ollama on my machine.&lt;/p&gt;

&lt;p&gt;RTX 5070 Ti, 8 GB VRAM. One 6B-parameter model fits. Two is already borderline. Every time Hermes switched from compression to approval, Ollama unloaded one model and loaded another. Five to ten seconds of dead air. The GPU fan kicked in. I lost more time waiting for model swaps than I saved on tokens. I abandoned local auxiliary models the same day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;p&gt;Here is what the routing actually means in terms of model size:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Before (default)&lt;/th&gt;
&lt;th&gt;After (routed)&lt;/th&gt;
&lt;th&gt;Reduction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Titles, search, skills, MCP&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kimi-k2.6&lt;/code&gt; (~1T)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;rnj-1:8b&lt;/code&gt; (8B)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;125x lighter&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Triage, profile&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kimi-k2.6&lt;/code&gt; (~1T)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gemma3:12b&lt;/code&gt; (12B)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;83x lighter&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approval&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kimi-k2.6&lt;/code&gt; (~1T)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;deepseek-v4-flash&lt;/code&gt; (~20B)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;50x lighter&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compression, web_extract&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kimi-k2.6&lt;/code&gt; (~1T)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kimi-k2.5&lt;/code&gt; (~1T)&lt;/td&gt;
&lt;td&gt;Same family, frees k2.6 for chat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vision&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kimi-k2.6&lt;/code&gt; (~1T)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;qwen3-vl&lt;/code&gt; (235B)&lt;/td&gt;
&lt;td&gt;Dedicated multimodal model&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=NoF-YajElIM&amp;amp;list=PLmpUb_PWAkDxewld5ZYyKifuHxgIbiq2d&amp;amp;index=14" rel="noopener noreferrer"&gt;The video by AI Garage&lt;/a&gt; measured compression cost directly: Claude Opus at 50K context = 13 cents per pass. Kimi K2 for the same task = 1.9 cents. That is an 85% reduction for a single compression pass. Compression fires 10–20 times per day for heavy users. The author estimated that with default settings, compression alone can cost $60/month with Claude Opus. Routed to a cheaper model, it drops to $9/month.&lt;/p&gt;

&lt;p&gt;I cannot confirm exact dollar savings for Ollama Cloud — they do not expose per-call pricing. But the scale difference is unambiguous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current Status
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Heavy auxiliary on k2.5&lt;/td&gt;
&lt;td&gt;Working&lt;/td&gt;
&lt;td&gt;Compression and web_extract no longer block the main model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vision on qwen3-vl&lt;/td&gt;
&lt;td&gt;Working&lt;/td&gt;
&lt;td&gt;Only multimodal option available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium tasks on gemma3:12b&lt;/td&gt;
&lt;td&gt;Working&lt;/td&gt;
&lt;td&gt;Triage and profile classification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approval on deepseek-v4-flash&lt;/td&gt;
&lt;td&gt;Working&lt;/td&gt;
&lt;td&gt;Fast binary decisions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Light tasks on rnj-1:8b&lt;/td&gt;
&lt;td&gt;Working&lt;/td&gt;
&lt;td&gt;Titles, search, skills, MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;provider: auto&lt;/code&gt; removed&lt;/td&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;td&gt;Explicit provider on every task&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local Ollama auxiliary&lt;/td&gt;
&lt;td&gt;Abandoned&lt;/td&gt;
&lt;td&gt;VRAM contention on 8 GB laptop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost tracking per task&lt;/td&gt;
&lt;td&gt;Not possible&lt;/td&gt;
&lt;td&gt;Ollama Cloud does not expose per-call pricing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sessions no longer pause for &lt;code&gt;Compressing messages&lt;/code&gt;. The token counter stopped monopolizing k2.6. Quota exhaustion mid-session is gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Config
&lt;/h2&gt;

&lt;p&gt;Here is the complete &lt;code&gt;auxiliary:&lt;/code&gt; block from my &lt;code&gt;~/.hermes/config.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;auxiliary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;compression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kimi-k2.5&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;120&lt;/span&gt;
  &lt;span class="na"&gt;web_extract&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kimi-k2.5&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;360&lt;/span&gt;
  &lt;span class="na"&gt;kanban_decomposer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kimi-k2.5&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;180&lt;/span&gt;
  &lt;span class="na"&gt;curator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kimi-k2.5&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;600&lt;/span&gt;
  &lt;span class="na"&gt;vision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;qwen3-vl:235b-instruct&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;120&lt;/span&gt;
    &lt;span class="na"&gt;download_timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
  &lt;span class="na"&gt;triage_specifier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemma3:12b&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;120&lt;/span&gt;
  &lt;span class="na"&gt;profile_describer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemma3:12b&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
  &lt;span class="na"&gt;approval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deepseek-v4-flash&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
  &lt;span class="na"&gt;title_generation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rnj-1:8b&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
  &lt;span class="na"&gt;session_search&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rnj-1:8b&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
    &lt;span class="na"&gt;max_concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;skills_hub&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rnj-1:8b&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
  &lt;span class="na"&gt;mcp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama-cloud&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rnj-1:8b&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;If you run Hermes Agent and have never touched the auxiliary block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hermes config edit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find &lt;code&gt;auxiliary:&lt;/code&gt;. Set explicit &lt;code&gt;provider&lt;/code&gt; and &lt;code&gt;model&lt;/code&gt; for every task — the one that handles it without dragging extra parameters. Save. Run &lt;code&gt;/reset&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then tomorrow run &lt;code&gt;hermes insights --days 1&lt;/code&gt;. Your main model should stop eating the entire token budget.&lt;/p&gt;

&lt;p&gt;If you use Claude or another frontier model as your main provider, the default config is even more expensive — every background task inherits that model. Route them to smaller models. Or go local if your hardware handles it.&lt;/p&gt;

&lt;p&gt;What auxiliary routing are you using? Drop it in the comments.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt; — &lt;a href="https://www.linkedin.com/in/azamat-safarov-4a37b93a7/" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/azamat-safarov-4a37b93a7/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;X / Twitter&lt;/strong&gt; — &lt;a href="https://x.com/Azamat__Safarov" rel="noopener noreferrer"&gt;https://x.com/Azamat__Safarov&lt;/a&gt;&lt;/p&gt;

</description>
      <category>hermes</category>
      <category>ollama</category>
      <category>optimization</category>
      <category>tokencost</category>
    </item>
    <item>
      <title>PostgreSQL 0L000 Error: Causes and Solutions Complete Guide</title>
      <dc:creator>umzzil nng</dc:creator>
      <pubDate>Tue, 02 Jun 2026 00:03:05 +0000</pubDate>
      <link>https://dev.to/dbmserror/postgresql-0l000-error-causes-and-solutions-complete-guide-4cbb</link>
      <guid>https://dev.to/dbmserror/postgresql-0l000-error-causes-and-solutions-complete-guide-4cbb</guid>
      <description>&lt;h2&gt;
  
  
  PostgreSQL Error 0L000: invalid grantor
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;0L000 invalid grantor&lt;/code&gt; error in PostgreSQL occurs when a user attempts to grant privileges on a database object without having the proper authority to do so. Specifically, this happens when the grantor does not own the object and does not hold the relevant privilege &lt;code&gt;WITH GRANT OPTION&lt;/code&gt;. This error is commonly encountered in complex multi-tenant environments or role-based access control systems where privilege delegation chains are involved.&lt;/p&gt;




&lt;h2&gt;
  
  
  Top 3 Causes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Attempting to Re-Grant a Privilege Without GRANT OPTION
&lt;/h3&gt;

&lt;p&gt;The most frequent cause: a user received a privilege but &lt;strong&gt;without&lt;/strong&gt; &lt;code&gt;WITH GRANT OPTION&lt;/code&gt;, and then tries to pass that privilege on to another user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- This works: superuser grants SELECT without GRANT OPTION&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;middle_user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- This FAILS: middle_user tries to grant to another user&lt;/span&gt;
&lt;span class="c1"&gt;-- ERROR: 0L000 invalid grantor&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;middle_user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;end_user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- ERROR here&lt;/span&gt;
&lt;span class="k"&gt;RESET&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Fix: Re-grant WITH GRANT OPTION&lt;/span&gt;
&lt;span class="k"&gt;RESET&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- back to superuser&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;middle_user&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;OPTION&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Now this works&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;middle_user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;end_user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- SUCCESS&lt;/span&gt;
&lt;span class="k"&gt;RESET&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Non-Owner Granting Privileges on an Object
&lt;/h3&gt;

&lt;p&gt;In PostgreSQL, only the object owner or a superuser can freely grant privileges. A non-owner user without &lt;code&gt;GRANT OPTION&lt;/code&gt; cannot issue &lt;code&gt;GRANT&lt;/code&gt; statements on that object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Check object ownership&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tableowner&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_tables&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'sales'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Non-owner attempting to grant (FAILS)&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;non_owner_user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;analyst&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- ERROR: 0L000&lt;/span&gt;
&lt;span class="k"&gt;RESET&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Fix: Change ownership or have the owner grant WITH GRANT OPTION&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;OWNER&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;correct_owner&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Or, have the owner delegate with grant option&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;correct_owner&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;non_owner_user&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;OPTION&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;RESET&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Broken Privilege Chain in Role Hierarchies
&lt;/h3&gt;

&lt;p&gt;When roles are nested, &lt;code&gt;WITH GRANT OPTION&lt;/code&gt; is &lt;strong&gt;not&lt;/strong&gt; automatically inherited through the role hierarchy. Each link in the chain must explicitly carry the grant option.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Create role hierarchy&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;top_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;mid_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;end_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Superuser grants to top_role WITHOUT grant option&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reports&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;top_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- mid_role is a member of top_role but still cannot re-grant&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="n"&gt;top_role&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;mid_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;mid_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reports&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;end_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- ERROR: 0L000&lt;/span&gt;
&lt;span class="k"&gt;RESET&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Fix: Rebuild the chain with GRANT OPTION at each step&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reports&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;top_role&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;OPTION&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;top_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reports&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;mid_role&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;OPTION&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;RESET&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;mid_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reports&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;end_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- SUCCESS&lt;/span&gt;
&lt;span class="k"&gt;RESET&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Quick Fix Solutions
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1. Check current grant options on a table&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;grantor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grantee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;privilege_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_grantable&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role_table_grants&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'your_table'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. Revoke and re-grant with proper GRANT OPTION (as superuser)&lt;/span&gt;
&lt;span class="k"&gt;REVOKE&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;your_table&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;problem_user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;your_table&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;problem_user&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;OPTION&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 3. Grant on all tables in schema with delegation rights&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;reporting_role&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;OPTION&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 4. Set default privileges for future tables&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;
    &lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;reporting_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Prevention Tips
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Audit grant options regularly.&lt;/strong&gt; Schedule a periodic query to review which roles hold &lt;code&gt;WITH GRANT OPTION&lt;/code&gt; and ensure the privilege delegation structure matches your intended design.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Regular audit query&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;table_schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grantor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grantee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;privilege_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_grantable&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role_table_grants&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_grantable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'YES'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;table_schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Apply the Principle of Least Privilege.&lt;/strong&gt; Only issue &lt;code&gt;WITH GRANT OPTION&lt;/code&gt; when absolutely necessary. Design a clear role hierarchy upfront — document which roles can delegate privileges and to whom — and avoid ad-hoc &lt;code&gt;GRANT&lt;/code&gt; statements in production without a review process. Use intermediate roles to keep the permission chain clean and auditable.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;📖 &lt;strong&gt;Want a more detailed guide?&lt;/strong&gt;&lt;br&gt;
Check out the full in-depth version (Korean) on &lt;a href="https://oraerror.com/postgresql-0l000-%ec%98%a4%eb%a5%98-%ec%9b%90%ec%9d%b8%ea%b3%bc-%ed%95%b4%ea%b2%b0-%eb%b0%a9%eb%b2%95-%ec%99%84%eb%b2%bd-%ea%b0%80%ec%9d%b4%eb%93%9c/" rel="noopener noreferrer"&gt;oraerror.com&lt;/a&gt; — includes detailed analysis, additional SQL examples, and prevention tips.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>dba</category>
      <category>sql</category>
    </item>
    <item>
      <title>Oracle ORA-00206 Error: Causes and Solutions Complete Guide</title>
      <dc:creator>umzzil nng</dc:creator>
      <pubDate>Tue, 02 Jun 2026 00:01:40 +0000</pubDate>
      <link>https://dev.to/dbmserror/oracle-ora-00206-error-causes-and-solutions-complete-guide-2o0g</link>
      <guid>https://dev.to/dbmserror/oracle-ora-00206-error-causes-and-solutions-complete-guide-2o0g</guid>
      <description>&lt;h2&gt;
  
  
  ORA-00206: Error in Writing Control File – Causes, Fixes &amp;amp; Prevention
&lt;/h2&gt;

&lt;p&gt;ORA-00206 is a critical Oracle database error that occurs when the database fails to write to one or more control files during normal operation. The control file is the most vital binary file in an Oracle database, storing the physical structure metadata including datafile locations, redo log file locations, checkpoint information, and SCN history. Because Oracle continuously updates the control file during database activity, any write failure immediately triggers this error and can lead to a database crash.&lt;/p&gt;




&lt;h2&gt;
  
  
  Top 3 Causes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Disk Space Exhaustion
&lt;/h3&gt;

&lt;p&gt;The most common cause is the filesystem or ASM disk group running out of free space where the control file resides. Oracle's checkpoint process (CKPT) and other background processes continuously write to the control file, so the moment available space is gone, writes fail instantly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Check control file location and size&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;BLOCK_SIZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;FILE_SIZE_BLKS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BLOCK_SIZE&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;FILE_SIZE_BLKS&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;SIZE_MB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;STATUS&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;CONTROLFILE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Check Fast Recovery Area usage (if applicable)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;SPACE_LIMIT&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;LIMIT_GB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;SPACE_USED&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;USED_GB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;SPACE_USED&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;SPACE_LIMIT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;USED_PCT&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;RECOVERY_FILE_DEST&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. OS-Level Permission or I/O Errors
&lt;/h3&gt;

&lt;p&gt;If the OS file permissions for the control file are changed (e.g., after infrastructure maintenance), or if the underlying storage device experiences hardware I/O errors, Oracle background processes can no longer write to the file. Always check OS-level logs (&lt;code&gt;/var/log/messages&lt;/code&gt;) alongside the Oracle alert log.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Verify current control file parameter settings&lt;/span&gt;
&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="k"&gt;PARAMETER&lt;/span&gt; &lt;span class="n"&gt;CONTROL_FILES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Check instance and database status&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;INSTANCE_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;STATUS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DATABASE_STATUS&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;INSTANCE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Control File Corruption or Accidental Deletion
&lt;/h3&gt;

&lt;p&gt;Accidental deletion via OS commands or physical media failure can corrupt or destroy the control file entirely. Without multiplexing in place, losing a single control file can be catastrophic. This scenario always appears alongside ORA-00202 in the alert log.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Query alert log location for investigation&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;DIAG_INFO&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Diag Trace'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Check all control file statuses&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;STATUS&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;CONTROLFILE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Quick Fix Solutions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Fix 1 – Free up disk space, then restart&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- After reclaiming OS disk space, restart the database&lt;/span&gt;
&lt;span class="n"&gt;SHUTDOWN&lt;/span&gt; &lt;span class="k"&gt;ABORT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;STARTUP&lt;/span&gt; &lt;span class="n"&gt;MOUNT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Confirm control files are accessible&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;STATUS&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;CONTROLFILE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="k"&gt;OPEN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix 2 – Restore from multiplexed copy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If one control file is damaged but another copy exists, copy the healthy file to replace the damaged one at the OS level, then restart.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- After OS-level file copy, mount and open&lt;/span&gt;
&lt;span class="n"&gt;STARTUP&lt;/span&gt; &lt;span class="n"&gt;MOUNT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="k"&gt;OPEN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix 3 – RMAN restore (when no valid copy exists)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Run in RMAN after connecting to target database&lt;/span&gt;
&lt;span class="n"&gt;RESTORE&lt;/span&gt; &lt;span class="n"&gt;CONTROLFILE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;AUTOBACKUP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;MOUNT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;RECOVER&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="k"&gt;OPEN&lt;/span&gt; &lt;span class="n"&gt;RESETLOGS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix 4 – Recreate from trace backup&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Recreate control file using a previously generated trace&lt;/span&gt;
&lt;span class="c1"&gt;-- First generate trace (for future use — run this NOW as prevention):&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;BACKUP&lt;/span&gt; &lt;span class="n"&gt;CONTROLFILE&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;TRACE&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="s1"&gt;'/backup/ctrl_trace.sql'&lt;/span&gt; &lt;span class="n"&gt;REUSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Prevention Tips
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Multiplex control files and enable RMAN autobackup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Always maintain at least 3 copies of the control file on separate physical disks. Enable automatic control file backups via RMAN so every structural change is captured.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Enable RMAN control file autobackup&lt;/span&gt;
&lt;span class="n"&gt;CONFIGURE&lt;/span&gt; &lt;span class="n"&gt;CONTROLFILE&lt;/span&gt; &lt;span class="n"&gt;AUTOBACKUP&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;CONFIGURE&lt;/span&gt; &lt;span class="n"&gt;CONTROLFILE&lt;/span&gt; &lt;span class="n"&gt;AUTOBACKUP&lt;/span&gt; &lt;span class="n"&gt;FORMAT&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;DEVICE&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;DISK&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'/backup/rman/%F'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Add multiplexed control file location (requires restart)&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;SYSTEM&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;CONTROL_FILES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="s1"&gt;'/u01/oradata/orcl/control01.ctl'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'/u02/oradata/orcl/control02.ctl'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'/u03/oradata/orcl/control03.ctl'&lt;/span&gt;
&lt;span class="k"&gt;SCOPE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SPFILE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Proactive disk monitoring&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Set up automated alerts when the filesystem hosting control files exceeds 80% usage. Use Oracle Enterprise Manager, Zabbix, or a scheduled script to catch space issues before they become outages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Include this in your monitoring scripts&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;STATUS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BLOCK_SIZE&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;FILE_SIZE_BLKS&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;SIZE_MB&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;CONTROLFILE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Related Oracle Errors
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error Code&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ORA-00202&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Identifies the exact control file path causing the write failure — always appears with ORA-00206&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ORA-00205&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Error identifying control file during STARTUP MOUNT phase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ORA-00210&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cannot open the specified control file (file missing or path mismatch)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ORA-27061&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Async I/O failure at the OS level, often accompanying storage-related ORA-00206&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ORA-00257&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Archiver error due to archive log space full, which can indirectly trigger control file write issues&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;blockquote&gt;
&lt;p&gt;📖 &lt;strong&gt;Want a more detailed guide?&lt;/strong&gt;&lt;br&gt;
Check out the full in-depth version (Korean) on &lt;a href="https://oraerror.com/oracle-ora-00206-%ec%98%a4%eb%a5%98-%ec%9b%90%ec%9d%b8%ea%b3%bc-%ed%95%b4%ea%b2%b0-%eb%b0%a9%eb%b2%95-%ec%99%84%eb%b2%bd-%ea%b0%80%ec%9d%b4%eb%93%9c/" rel="noopener noreferrer"&gt;oraerror.com&lt;/a&gt; — includes detailed analysis, additional SQL examples, and prevention tips.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>oracle</category>
      <category>database</category>
      <category>dba</category>
      <category>sql</category>
    </item>
    <item>
      <title>View Transitions API: Frontend Animasyonlarında Yeni Standart mı Geliyor?</title>
      <dc:creator>Erhan AKKAYA</dc:creator>
      <pubDate>Tue, 02 Jun 2026 00:01:37 +0000</pubDate>
      <link>https://dev.to/erhanaky/view-transitions-api-frontend-animasyonlarinda-yeni-standart-mi-geliyor-1p3i</link>
      <guid>https://dev.to/erhanaky/view-transitions-api-frontend-animasyonlarinda-yeni-standart-mi-geliyor-1p3i</guid>
      <description>&lt;p&gt;&lt;strong&gt;View Transitions API, sayfa geçişlerini daha akıcı ve performanslı hale getiriyor. Modern web uygulamalarında neden öne çıktığını keşfedin.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  View Transitions API: Frontend Animasyonlarında Yeni Standart mı Geliyor?
&lt;/h2&gt;

&lt;p&gt;Uzun yıllardır web uygulamalarında sayfa geçişleri ve animasyonlar, kullanıcı deneyimini iyileştirmek için kullanılan önemli araçlardan biri oldu. Ancak bu animasyonları oluşturmak çoğu zaman ek kütüphaneler, karmaşık state yönetimi ve performans optimizasyonları gerektiriyordu. Özellikle React, Next.js ve benzeri framework'lerde sayfa geçişlerini kusursuz şekilde uygulamak her zaman kolay olmadı.&lt;/p&gt;

&lt;p&gt;View Transitions API ise bu durumu değiştirmeyi hedefleyen yeni nesil bir web standardı olarak karşımıza çıkıyor. Tarayıcı seviyesinde çalışan bu API sayesinde geliştiriciler, ek animasyon kütüphanelerine daha az ihtiyaç duyarak daha akıcı ve performanslı geçişler oluşturabiliyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  View Transitions API Nedir?
&lt;/h2&gt;

&lt;p&gt;View Transitions API, bir sayfanın veya kullanıcı arayüzünün mevcut görünümünü yakalayarak yeni görünümle arasında animasyonlu bir geçiş oluşturabilen modern bir tarayıcı API'sidir.&lt;/p&gt;

&lt;p&gt;Geleneksel yöntemlerde bir bileşenin eski ve yeni durumları arasında animasyon oluşturmak için JavaScript kodları veya harici kütüphaneler kullanılırken, View Transitions API bu işlemi doğrudan tarayıcı motoruna devreder.&lt;/p&gt;

&lt;p&gt;Bu yaklaşımın en önemli avantajı performanstır. Çünkü animasyon süreci tarayıcı tarafından optimize edilir ve geliştiricinin ekstra DOM manipülasyonları yapmasına gerek kalmaz.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Neden Önemli?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Modern web uygulamalarında kullanıcı beklentileri giderek yükseliyor. Kullanıcılar artık yalnızca hızlı çalışan uygulamalar değil, aynı zamanda doğal hissettiren geçişler de bekliyor.&lt;/p&gt;

&lt;p&gt;Mobil uygulamalarda yıllardır gördüğümüz:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kart büyütme animasyonları&lt;/li&gt;
&lt;li&gt;Sayfa geçiş efektleri&lt;/li&gt;
&lt;li&gt;Paylaşılan öğe animasyonları&lt;/li&gt;
&lt;li&gt;Akıcı navigasyon hareketleri&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;gibi deneyimler artık web tarafında da daha kolay uygulanabiliyor.&lt;/p&gt;

&lt;p&gt;View Transitions API'nin öne çıkan avantajları şunlardır:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Daha az JavaScript kodu&lt;/li&gt;
&lt;li&gt;Daha yüksek performans&lt;/li&gt;
&lt;li&gt;Daha düşük bakım maliyeti&lt;/li&gt;
&lt;li&gt;Framework bağımsız kullanım&lt;/li&gt;
&lt;li&gt;Tarayıcı optimizasyonlarından doğrudan faydalanma&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Geleneksel Yaklaşımın Sorunları&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;React veya Next.js projelerinde animasyon denildiğinde akla genellikle Framer Motion gibi çözümler geliyor.&lt;/p&gt;

&lt;p&gt;Bu araçlar oldukça güçlü olsalar da bazı dezavantajlara sahipler:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uygulama boyutunu artırırlar.&lt;/li&gt;
&lt;li&gt;Karmaşık geçişlerde ek kod gerektirirler.&lt;/li&gt;
&lt;li&gt;Navigasyon sırasında state yönetimi zorlaşabilir.&lt;/li&gt;
&lt;li&gt;Performans maliyeti oluşturabilirler.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;View Transitions API ise birçok temel senaryoda bu ihtiyacı azaltabilecek potansiyele sahip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nasıl Çalışıyor?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;API'nin temel mantığı oldukça basittir.&lt;/p&gt;

&lt;p&gt;Tarayıcı önce mevcut görünümün bir anlık görüntüsünü alır. Daha sonra arayüz güncellenir ve yeni görünüm oluşturulur. Son aşamada ise iki görünüm arasında animasyon uygulanır.&lt;/p&gt;

&lt;p&gt;Bu işlem geliştiricinin manuel olarak eski ve yeni DOM durumlarını takip etmesine gerek bırakmaz.&lt;/p&gt;

&lt;p&gt;Özellikle liste sayfalarından detay sayfalarına geçişlerde oldukça etkileyici sonuçlar elde edilebilir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;React ve Next.js İçin Ne Anlama Geliyor?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;React ekosistemi son yıllarda performans ve kullanıcı deneyimi odaklı önemli değişimler yaşadı.&lt;/p&gt;

&lt;p&gt;Server Components, React Compiler ve Actions API gibi yeniliklerin ardından View Transitions API de kullanıcı deneyimi tarafında dikkat çekici bir adım olarak görülüyor.&lt;/p&gt;

&lt;p&gt;Next.js kullanan geliştiriciler açısından bakıldığında:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Daha doğal sayfa geçişleri&lt;/li&gt;
&lt;li&gt;Daha az üçüncü parti bağımlılık&lt;/li&gt;
&lt;li&gt;Daha düşük bundle boyutu&lt;/li&gt;
&lt;li&gt;Daha yüksek Lighthouse skorları&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;gibi avantajlar elde etmek mümkün olabilir.&lt;/p&gt;

&lt;p&gt;Özellikle içerik siteleri, portfolyo projeleri, dashboard uygulamaları ve e-ticaret siteleri için önemli fırsatlar sunuyor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Her Şey Çözüldü mü?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Henüz değil.&lt;/p&gt;

&lt;p&gt;View Transitions API oldukça umut verici olsa da tüm animasyon senaryolarını tek başına çözebilecek noktada değil.&lt;/p&gt;

&lt;p&gt;Karmaşık etkileşimler, sürükle-bırak sistemleri veya ileri seviye mikro animasyonlar için hâlâ Framer Motion gibi araçlara ihtiyaç duyulabilir.&lt;/p&gt;

&lt;p&gt;Ancak birçok günlük kullanım senaryosunda geliştiricilerin daha sade çözümler üretmesine yardımcı olacağı açık.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sonuç&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Web platformu son yıllarda önemli bir dönüşüm geçiriyor. Eskiden yalnızca JavaScript kütüphaneleriyle mümkün olan birçok özellik artık doğrudan tarayıcılar tarafından destekleniyor.&lt;/p&gt;

&lt;p&gt;View Transitions API de bu dönüşümün en dikkat çekici örneklerinden biri. Daha akıcı kullanıcı deneyimleri oluşturmayı kolaylaştırırken aynı zamanda performans avantajları sunuyor.&lt;/p&gt;

&lt;p&gt;Önümüzdeki birkaç yıl içerisinde React, Next.js ve diğer modern frontend teknolojilerinin bu API'yi daha yoğun şekilde kullanacağını görmek sürpriz olmayacaktır. Geliştiriciler için ise bugünden öğrenmeye değer teknolojilerden biri olduğu kesin.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>nextjs</category>
      <category>react</category>
      <category>web</category>
    </item>
    <item>
      <title>AI code review is now a cloud workload</title>
      <dc:creator>Paulo Victor Leite Lima Gomes</dc:creator>
      <pubDate>Tue, 02 Jun 2026 00:01:22 +0000</pubDate>
      <link>https://dev.to/pvgomes/ai-code-review-is-now-a-cloud-workload-237j</link>
      <guid>https://dev.to/pvgomes/ai-code-review-is-now-a-cloud-workload-237j</guid>
      <description>&lt;p&gt;Code review used to have a very human scaling problem.&lt;/p&gt;

&lt;p&gt;The pull request was ready. The tests were green. Then it sat there waiting for someone with enough context, enough patience, and maybe enough coffee to read it properly.&lt;/p&gt;

&lt;p&gt;AI code review changes that queue. GitHub Copilot can review pull requests automatically, and since June 1, those reviews consume GitHub AI Credits. For private repositories, they can also consume GitHub Actions minutes while Copilot prepares the environment and analyzes the code.&lt;/p&gt;

&lt;p&gt;This is not surprising. Models cost money. Runners cost money.&lt;/p&gt;

&lt;p&gt;But it does make the product category much clearer.&lt;/p&gt;

&lt;p&gt;AI code review is not merely a helpful comment bot anymore. It is a cloud workload triggered by your development process.&lt;/p&gt;

&lt;p&gt;And cloud workloads need budgets.&lt;/p&gt;

&lt;h2&gt;
  
  
  the review button is now an infrastructure decision
&lt;/h2&gt;

&lt;p&gt;There is a familiar progression in developer tooling.&lt;/p&gt;

&lt;p&gt;At first, a feature feels small and local. Someone enables it. A few people try it. The cost is invisible because the usage is limited and the invoice is mixed into something larger.&lt;/p&gt;

&lt;p&gt;Then adoption grows.&lt;/p&gt;

&lt;p&gt;Soon, the same feature runs across hundreds of repositories, thousands of pull requests, and multiple review passes per change. A product that felt like an editor convenience becomes an operating expense.&lt;/p&gt;

&lt;p&gt;We already learned this with CI.&lt;/p&gt;

&lt;p&gt;The first pipeline is cheap. Then every commit runs unit tests, integration tests, security scans, preview deployments, browser tests, and half a dozen checks nobody wants to remove because nobody remembers why they were added.&lt;/p&gt;

&lt;p&gt;The pipeline is useful. It is also a bill.&lt;/p&gt;

&lt;p&gt;AI review is entering the same phase.&lt;/p&gt;

&lt;p&gt;GitHub's new billing model is explicit: code review consumes AI Credits, and private repositories use Actions minutes during the analysis. Organizations can configure a default runner and apply budgets at the user level.&lt;/p&gt;

&lt;p&gt;Those controls sound boring.&lt;/p&gt;

&lt;p&gt;They are also the part engineering leaders should care about.&lt;/p&gt;

&lt;h2&gt;
  
  
  review everything is not a strategy
&lt;/h2&gt;

&lt;p&gt;It is tempting to enable AI review everywhere.&lt;/p&gt;

&lt;p&gt;Why not? More review sounds better than less review. If the bot catches one security bug, one missing test, or one questionable API change, the entire month might pay for itself.&lt;/p&gt;

&lt;p&gt;That is probably true in some teams.&lt;/p&gt;

&lt;p&gt;It does not follow that every review is equally valuable.&lt;/p&gt;

&lt;p&gt;A ten-line dependency bump and a large authentication refactor should not necessarily receive the same treatment. A generated documentation change may not need an AI reviewer. A risky database migration probably deserves more than one automated pass and a quick human approval.&lt;/p&gt;

&lt;p&gt;Once reviews are metered, the policy questions become unavoidable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which repositories should request AI review by default?&lt;/li&gt;
&lt;li&gt;Which pull requests should trigger it automatically?&lt;/li&gt;
&lt;li&gt;Should generated changes receive the same review budget as human-written changes?&lt;/li&gt;
&lt;li&gt;How many review rounds are useful before the comments become noise?&lt;/li&gt;
&lt;li&gt;What runner size is actually necessary for each repository?&lt;/li&gt;
&lt;li&gt;Which teams are spending more because their codebase is harder to analyze?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not finance-only questions.&lt;/p&gt;

&lt;p&gt;They are architecture and engineering-management questions expressed through a bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  cost attribution is a useful forcing function
&lt;/h2&gt;

&lt;p&gt;I do not think metering is automatically bad.&lt;/p&gt;

&lt;p&gt;Free-looking infrastructure has a way of hiding bad habits. A budget can force a team to decide what it actually values.&lt;/p&gt;

&lt;p&gt;If one repository uses far more AI review credits and runner minutes than another, there may be a good reason. Maybe it is a critical service. Maybe the diffs are complex. Maybe the team is using the reviewer as an extra security layer.&lt;/p&gt;

&lt;p&gt;Or maybe every tiny pull request is triggering an expensive workflow because nobody looked at the defaults.&lt;/p&gt;

&lt;p&gt;The useful outcome is not minimizing the bill at all costs. The useful outcome is understanding what the bill represents.&lt;/p&gt;

&lt;p&gt;CI minutes can tell you that your tests are too slow, too broad, or too flaky. Cloud spend can tell you that an architecture is chatty, over-provisioned, or poorly cached. AI review spend can reveal that your workflow is asking a model to examine a lot of low-value changes.&lt;/p&gt;

&lt;p&gt;Cost is architecture feedback.&lt;/p&gt;

&lt;h2&gt;
  
  
  the other bill is reviewer attention
&lt;/h2&gt;

&lt;p&gt;There is a second cost that will not appear on the GitHub invoice.&lt;/p&gt;

&lt;p&gt;Every AI review comment asks a human to spend attention.&lt;/p&gt;

&lt;p&gt;Some comments will be useful. Some will be technically correct but irrelevant. Some will identify a real concern without understanding why the code looks strange. Some will confidently suggest a cleaner implementation that quietly breaks an ugly but important edge case.&lt;/p&gt;

&lt;p&gt;That means the cheapest AI review is not always the best one.&lt;/p&gt;

&lt;p&gt;A review that costs a few credits but creates ten minutes of distraction for two engineers is expensive in a different way. Multiply that across a large organization and the bigger problem may be signal quality, not model usage.&lt;/p&gt;

&lt;p&gt;This is why "number of AI review comments" is a terrible success metric.&lt;/p&gt;

&lt;p&gt;The metrics I would want are more practical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How often does an AI comment lead to a code change?&lt;/li&gt;
&lt;li&gt;Which categories of comments are consistently useful?&lt;/li&gt;
&lt;li&gt;How often are comments dismissed as noise?&lt;/li&gt;
&lt;li&gt;Does AI review reduce human review time or add another review queue?&lt;/li&gt;
&lt;li&gt;Does it catch defects that tests and static analysis missed?&lt;/li&gt;
&lt;li&gt;Are teams starting to rubber-stamp reviews because a bot already looked at the diff?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not more comments.&lt;/p&gt;

&lt;p&gt;The goal is better changes reaching production with less wasted attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  keep humans in the uncomfortable parts
&lt;/h2&gt;

&lt;p&gt;AI review is good at broad, patient inspection.&lt;/p&gt;

&lt;p&gt;It does not get bored reading the fifth similar file. It can notice missing error handling, suspicious patterns, inconsistent tests, and changes that deserve a second look. It can give a pull request author feedback before a human reviewer arrives.&lt;/p&gt;

&lt;p&gt;That is useful.&lt;/p&gt;

&lt;p&gt;But the highest-value review questions are usually uncomfortable and contextual:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this the right change for the system?&lt;/li&gt;
&lt;li&gt;Does the abstraction make future incidents easier or harder to debug?&lt;/li&gt;
&lt;li&gt;Are we preserving an API contract that is not documented anywhere?&lt;/li&gt;
&lt;li&gt;Is the migration plan realistic under production load?&lt;/li&gt;
&lt;li&gt;Is this complexity necessary, or did we create it because the generated code looked plausible?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those questions need judgment.&lt;/p&gt;

&lt;p&gt;The AI reviewer can help surface evidence. It should not become a reason to skip the conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  what i would do this week
&lt;/h2&gt;

&lt;p&gt;If your organization uses Copilot code review, I would treat the June 1 billing change as a good excuse to inspect the workflow before the invoice does it for you.&lt;/p&gt;

&lt;p&gt;Start with visibility. Check which repositories use automatic reviews, which runners they use, and how frequently reviews run. Put a budget in place, even if it is generous. A budget is an alerting mechanism before it is a restriction.&lt;/p&gt;

&lt;p&gt;Then sample the output.&lt;/p&gt;

&lt;p&gt;Take a few weeks of review comments and classify them. Useful defect catch. Helpful suggestion. Style preference. Duplicate of an existing check. Wrong. Ignored.&lt;/p&gt;

&lt;p&gt;You do not need a complicated dashboard on day one. A spreadsheet and an honest conversation will tell you more than a vanity metric.&lt;/p&gt;

&lt;p&gt;Finally, decide where AI review belongs in the pipeline.&lt;/p&gt;

&lt;p&gt;Maybe it runs automatically on critical services. Maybe authors request it before asking for human review. Maybe small dependency bumps skip it. Maybe security-sensitive changes get a more deliberate policy.&lt;/p&gt;

&lt;p&gt;The correct answer will depend on the codebase.&lt;/p&gt;

&lt;p&gt;That is the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  the punchline
&lt;/h2&gt;

&lt;p&gt;AI code review is becoming infrastructure.&lt;/p&gt;

&lt;p&gt;It consumes model credits. It can consume runner minutes. It generates operational data. It needs defaults, budgets, and a reason to exist in each workflow.&lt;/p&gt;

&lt;p&gt;That does not make it less useful.&lt;/p&gt;

&lt;p&gt;It makes the engineering conversation more honest.&lt;/p&gt;

&lt;p&gt;"Let the AI review everything" sounds like a productivity strategy until every pull request spends compute and every comment spends attention.&lt;/p&gt;

&lt;p&gt;The teams that get value from AI review will not be the teams with the most automated comments.&lt;/p&gt;

&lt;p&gt;They will be the teams that know which reviews are worth paying for.&lt;/p&gt;

&lt;h2&gt;
  
  
  references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.blog/changelog/2026-06-01-updates-to-github-copilot-billing-and-plans/" rel="noopener noreferrer"&gt;GitHub Changelog: Updates to GitHub Copilot billing and plans&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.blog/changelog/2026-04-27-github-copilot-code-review-will-start-consuming-github-actions-minutes-on-june-1-2026/" rel="noopener noreferrer"&gt;GitHub Changelog: Copilot code review will start consuming GitHub Actions minutes on June 1, 2026&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To test my projects, I use &lt;a href="https://railway.com?referralCode=G_jRmP" rel="noopener noreferrer"&gt;Railway&lt;/a&gt;. If you want $20 USD to get started, &lt;a href="https://railway.com?referralCode=G_jRmP" rel="noopener noreferrer"&gt;use this link&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>github</category>
      <category>githubcopilot</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Tested DeepSeek V4 Flash and GPT-4o Side by Side — Here's the p99 Latency Truth</title>
      <dc:creator>eagerspark</dc:creator>
      <pubDate>Tue, 02 Jun 2026 00:01:09 +0000</pubDate>
      <link>https://dev.to/eagerspark/i-tested-deepseek-v4-flash-and-gpt-4o-side-by-side-heres-the-p99-latency-truth-3jd1</link>
      <guid>https://dev.to/eagerspark/i-tested-deepseek-v4-flash-and-gpt-4o-side-by-side-heres-the-p99-latency-truth-3jd1</guid>
      <description>&lt;p&gt;I gotta say, let me tell you a story about the time I almost went bankrupt optimizing for the wrong metric. &lt;/p&gt;

&lt;p&gt;It was 3 AM, and my multi-region deployment was melting down. The p99 latency on our GPT-4o integration had spiked to 8 seconds during a traffic burst. Our auto-scaling group was spinning up instances like a slot machine on fire, and our monthly AI API bill was about to eclipse our AWS spend. Meanwhile, our startup competitor was shipping features twice as fast, paying 97% less per token, and sleeping through the night.&lt;/p&gt;

&lt;p&gt;That's when I realised: the conventional wisdom about AI API selection is broken. Most cloud architects focus on model performance benchmarks. But in production, it's not about which model scores 0.2% higher on MMLU — it's about throughput, SLA compliance, multi-region failover, and the hidden cost of provider lock-in.&lt;/p&gt;

&lt;p&gt;Here's what I learned after stress-testing 12 different AI providers across three continents, and why the "just go direct to the provider" advice is the fastest way to destroy your p99 SLAs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Startup Trap: Why "Free Tier" Is the Most Expensive Mistake
&lt;/h2&gt;

&lt;p&gt;Every startup founder I've met says the same thing: "We'll just use DeepSeek's API directly. It's cheap, right?"&lt;/p&gt;

&lt;p&gt;Wrong on three levels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 1: The Registration Nightmare&lt;/strong&gt;&lt;br&gt;
I spent four hours trying to register for a DeepSeek account. Chinese phone number? Don't have one. WeChat Pay? My startup doesn't accept payments through a social media app. Alipay? Same problem. By the time I gave up, I'd burned more engineer-hours than the API credits I was trying to save.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 2: The Single-Region Trap&lt;/strong&gt;&lt;br&gt;
DeepSeek's direct API runs out of one region. When that region goes down — and it will — your p99 latency doesn't degrade gracefully. It goes to infinity. I've seen it happen during Chinese New Year, during network maintenance windows, during random Tuesday afternoons. Without multi-region failover baked into your routing layer, you're betting your uptime SLA on a single datacenter's reliability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 3: The Credit Expiration Surprise&lt;/strong&gt;&lt;br&gt;
That "cheap" $10 pre-purchase? It expires in 30 days. If your startup's traffic ramps slowly (which it should — you're iterating), you're paying for idle capacity. That's not cost optimization. That's burning cash in a bonfire.&lt;/p&gt;

&lt;p&gt;Here's the p99 reality check: I benchmarked direct DeepSeek vs. routing through Global API's auto-failover layer. Direct provider had 99.9% uptime in their home region — when that region was healthy. Global API's multi-region routing delivered 99.99% effective uptime because it transparently hit whichever provider had the lowest p99 latency at that moment.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Direct DeepSeek p99&lt;/th&gt;
&lt;th&gt;Global API Routed p99&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Home region healthy&lt;/td&gt;
&lt;td&gt;350ms&lt;/td&gt;
&lt;td&gt;320ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home region degraded&lt;/td&gt;
&lt;td&gt;4,200ms&lt;/td&gt;
&lt;td&gt;480ms (auto-failover)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home region down&lt;/td&gt;
&lt;td&gt;Timeout&lt;/td&gt;
&lt;td&gt;520ms (fallback provider)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traffic burst (10x)&lt;/td&gt;
&lt;td&gt;2,100ms + throttling&lt;/td&gt;
&lt;td&gt;680ms (auto-scaled capacity)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The direct provider saves you $0.05 per million tokens on the base price, but costs you 10x in engineering time and 100x in p99 SLA risk. That's not an API choice — that's an architectural decision.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Enterprise Overhead Problem: Why Your $50,000/Month Bill Is Overengineered
&lt;/h2&gt;

&lt;p&gt;Now let me tell you about the other end of the spectrum. Two years ago, I architected a system for a Fortune 500 financial services company. The security team required SOC2, ISO 27001, dedicated capacity, and a 99.99% SLA. The compliance team needed a data processing agreement (DPA) that covered 17 regulatory frameworks. The procurement team wanted Net-90 invoice terms.&lt;/p&gt;

&lt;p&gt;We signed a direct contract with a major provider. Annual commitment: $600,000. Onboarding time: 8 weeks. Custom integration work: 6 weeks of engineer time.&lt;/p&gt;

&lt;p&gt;And you know what? When we finally went live, 80% of our traffic was routine customer support queries that could have been handled by a $0.25/M token model.&lt;/p&gt;

&lt;p&gt;The problem with enterprise-grade AI procurement is that you're paying for peak capacity. You're buying a dedicated fleet of GPUs to handle Black Friday traffic spikes, but those GPUs sit idle 90% of the time. You're paying a premium for SLA guarantees that your actual workloads don't need.&lt;/p&gt;

&lt;p&gt;Here's what I do now for enterprise deployments: I use a tiered architecture.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TieredAIModelRouter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ga_pro_xxxxxxxxxxxx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://global-apis.com/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;premium_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ga_pro_yyyyyyyyyyyy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://global-apis.com/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;route_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;standard&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        Route based on priority and current p99 latency.
        Standard: use default model with auto-failover.
        Premium: use dedicated capacity with SLA guarantee.
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;premium&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_call_premium&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Standard routing with automatic fallback
&lt;/span&gt;        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deepseek-ai/DeepSeek-V4-Flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# $0.25/M tokens
&lt;/span&gt;                &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
                &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;  &lt;span class="c1"&gt;# 2-second p99 target
&lt;/span&gt;            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;latency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;latency&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# Degraded performance
&lt;/span&gt;                &lt;span class="c1"&gt;# Auto-failover to next provider
&lt;/span&gt;                &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;premium_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pro/deepseek-ai/DeepSeek-V4-Flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Dedicated capacity
&lt;/span&gt;                    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;

        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Fallback to premium if default fails
&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;premium_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Qwen/Qwen3-32B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# $0.28/M, different provider
&lt;/span&gt;                &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_call_premium&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;premium_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pro/deepseek-ai/DeepSeek-R1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# $2.50/M, guaranteed capacity
&lt;/span&gt;            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This architecture costs us $12,000/month instead of $50,000/month — and our p99 latency is actually better because we're not over-provisioning. The dedicated capacity only gets hit for the 10% of requests that genuinely need it. Everything else routes through the shared pool with auto-failover.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hybrid Architecture That Actually Scales
&lt;/h2&gt;

&lt;p&gt;After running production AI workloads across 37 regions and 5 continents, here's the architecture that doesn't suck:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1: Default Model (80% of traffic)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Model: DeepSeek V4 Flash&lt;/li&gt;
&lt;li&gt;Cost: $0.25/M input tokens&lt;/li&gt;
&lt;li&gt;Provider: Routed through Global API with automatic failover&lt;/li&gt;
&lt;li&gt;p99 Latency Target: 500ms&lt;/li&gt;
&lt;li&gt;SLA: Best-effort, but effective 99.9% via multi-region routing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Layer 2: Mid-Tier Fallback (15% of traffic)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Model: Qwen3-32B&lt;/li&gt;
&lt;li&gt;Cost: $0.28/M input tokens&lt;/li&gt;
&lt;li&gt;Provider: Different provider (avoids correlated failures)&lt;/li&gt;
&lt;li&gt;p99 Latency Target: 1 second&lt;/li&gt;
&lt;li&gt;SLA: 99.5% guaranteed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Layer 3: Premium Tier (5% of traffic)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Model: DeepSeek R1 or GPT-4o&lt;/li&gt;
&lt;li&gt;Cost: $2.50/M input tokens&lt;/li&gt;
&lt;li&gt;Provider: Dedicated capacity with SLA&lt;/li&gt;
&lt;li&gt;p99 Latency Target: 200ms&lt;/li&gt;
&lt;li&gt;SLA: 99.99% guaranteed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key insight: your p99 doesn't care about the model's benchmark score. It cares about the routing layer's ability to failover within milliseconds, the provider's regional availability, and your auto-scaling group's response time.&lt;/p&gt;

&lt;p&gt;I've benchmarked this against single-provider architectures. The hybrid setup delivers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3x lower p99 latency during peak hours&lt;/li&gt;
&lt;li&gt;2x better cost efficiency (pay for dedicated capacity only when needed)&lt;/li&gt;
&lt;li&gt;100x improvement in effective uptime (no single point of failure)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Cost Reality: Why Startups Shouldn't Pay Enterprise Prices
&lt;/h2&gt;

&lt;p&gt;Let me show you the math that convinced me to stop signing annual provider contracts.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Growth Stage&lt;/th&gt;
&lt;th&gt;Monthly Token Volume&lt;/th&gt;
&lt;th&gt;Direct GPT-4o Cost&lt;/th&gt;
&lt;th&gt;DeepSeek V4 Flash via Global API&lt;/th&gt;
&lt;th&gt;Savings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MVP (100 users, iterating)&lt;/td&gt;
&lt;td&gt;5M tokens&lt;/td&gt;
&lt;td&gt;$50.00&lt;/td&gt;
&lt;td&gt;$1.25&lt;/td&gt;
&lt;td&gt;97.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Beta (1,000 users, learning)&lt;/td&gt;
&lt;td&gt;50M tokens&lt;/td&gt;
&lt;td&gt;$500.00&lt;/td&gt;
&lt;td&gt;$12.50&lt;/td&gt;
&lt;td&gt;97.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Launch (10K users, stabilizing)&lt;/td&gt;
&lt;td&gt;500M tokens&lt;/td&gt;
&lt;td&gt;$5,000.00&lt;/td&gt;
&lt;td&gt;$125.00&lt;/td&gt;
&lt;td&gt;97.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Growth (100K users, scaling)&lt;/td&gt;
&lt;td&gt;5B tokens&lt;/td&gt;
&lt;td&gt;$50,000.00&lt;/td&gt;
&lt;td&gt;$1,250.00&lt;/td&gt;
&lt;td&gt;97.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 97.5% savings doesn't come from the model itself. It comes from not over-provisioning. When you go direct to a premium provider, you're buying capacity for peak load. When you route through a multi-provider layer, you're buying capacity from the most cost-efficient provider at each moment.&lt;/p&gt;

&lt;p&gt;And here's the thing that keeps me up at night: that $50,000/month GPT-4o contract? It doesn't include the cost of multi-region deployment, auto-scaling infrastructure, or failover engineering. You're still paying for that separately.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pro Channel Trade-Off: When You Actually Need Dedicated Capacity
&lt;/h2&gt;

&lt;p&gt;I'm not anti-enterprise. I run enterprise workloads. Sometimes you genuinely need dedicated capacity.&lt;/p&gt;

&lt;p&gt;When I architect for a regulated industry (healthcare, finance, defense), I use the Pro Channel. Here's the difference:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Standard API (for 95% of workloads):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shared inference capacity&lt;/li&gt;
&lt;li&gt;Best-effort routing&lt;/li&gt;
&lt;li&gt;Community support&lt;/li&gt;
&lt;li&gt;Pay-as-you-go credits (never expire)&lt;/li&gt;
&lt;li&gt;184 models available&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pro Channel (for the 5% that matters):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dedicated GPU instances&lt;/li&gt;
&lt;li&gt;99.9% uptime SLA with penalties&lt;/li&gt;
&lt;li&gt;24/7 priority support with 15-minute response&lt;/li&gt;
&lt;li&gt;Custom data processing agreements&lt;/li&gt;
&lt;li&gt;Invoice billing (Net-30)&lt;/li&gt;
&lt;li&gt;Queue priority during peak hours
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Pro Channel example — same API endpoint, different backend
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;pro_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GLOBAL_API_PRO_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://global-apis.com/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# This call goes to dedicated capacity, not the shared pool
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pro_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pro/deepseek-ai/DeepSeek-V3.2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a compliance officer.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Review this financial document for regulatory issues.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;30.0&lt;/span&gt;  &lt;span class="c1"&gt;# Enterprise SLA requires this
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Check if we hit dedicated capacity
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Response latency: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;total_time&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Model used: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  # Should show &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Pro/deepseek-ai/DeepSeek-V3.2&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Pro Channel costs more per token — but it's still 60-70% cheaper than a direct enterprise contract because you're not paying for idle capacity. You pay for what you use, but with guaranteed throughput.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Production Playbook: What I Actually Deploy
&lt;/h2&gt;

&lt;p&gt;After burning through $200,000 in AI API costs on failed experiments, here's what I deploy today:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For startups (under $10,000/month AI spend):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One Global API key&lt;/li&gt;
&lt;li&gt;Auto-route between DeepSeek V4 Flash (default) and Qwen3-32B (fallback)&lt;/li&gt;
&lt;li&gt;No dedicated capacity&lt;/li&gt;
&lt;li&gt;Credits never expire (this saved my startup during a 3-month pivot)&lt;/li&gt;
&lt;li&gt;184 models available for experimentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For enterprises (over $10,000/month or regulated):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pro Channel for mission-critical workloads&lt;/li&gt;
&lt;li&gt;Standard API for everything else&lt;/li&gt;
&lt;li&gt;Multi-region routing with automatic failover&lt;/li&gt;
&lt;li&gt;Custom SLA with 99.9% uptime guarantee&lt;/li&gt;
&lt;li&gt;Dedicated engineer for onboarding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The one rule I never break:&lt;/strong&gt; Never trust a single provider. Not even the big ones. I've seen AWS go down, Azure go down, OpenAI go down, DeepSeek go down. Your routing layer should treat every provider as ephemeral.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;The AI API market in 2026 is mature enough that you don't need to choose between "cheap and unreliable" or "expensive and locked in." The technology exists to have both — if you architect for it.&lt;/p&gt;

&lt;p&gt;Stop optimizing for model performance. Start optimizing for p99 latency, multi-region availability, and cost efficiency across your entire stack.&lt;/p&gt;

&lt;p&gt;And if you want to skip the 8-week onboarding and just start routing traffic with auto-failover built-in, check out Global API. I'm not saying it's the only option — but after testing 12 providers across 37 regions, it's the one I deploy in production. Your infrastructure should be boring, reliable, and cost-effective. That's not a product pitch. That's a production reality.&lt;/p&gt;

&lt;p&gt;Now go fix your p99 latency. Your users are waiting.&lt;/p&gt;

</description>
      <category>deepseek</category>
      <category>ai</category>
      <category>api</category>
      <category>programming</category>
    </item>
    <item>
      <title>Web accessibility overlays don't work — here's what actually does</title>
      <dc:creator>Vaibhav Jain</dc:creator>
      <pubDate>Tue, 02 Jun 2026 00:00:06 +0000</pubDate>
      <link>https://dev.to/vaibhav_jain_3b62a5510248/web-accessibility-overlays-dont-work-heres-what-actually-does-2a9b</link>
      <guid>https://dev.to/vaibhav_jain_3b62a5510248/web-accessibility-overlays-dont-work-heres-what-actually-does-2a9b</guid>
      <description>&lt;p&gt;Accessibility overlay services like accessiBe and UserWay market themselves as a one-click fix for ADA compliance. They promise that a single line of JavaScript will instantly make your website accessible to users with disabilities, protecting you from lawsuits overnight.&lt;/p&gt;

&lt;p&gt;The reality is that these overlays are a band-aid on a broken foundation. They inject a visual widget and attempt to patch accessibility issues in the browser, but they never touch the underlying source code. Screen readers and assistive technologies still encounter improperly nested headings, missing form labels, and non-semantic markup that automation cannot reliably interpret.&lt;/p&gt;

&lt;p&gt;What screen reader and keyboard users actually need is clean, semantic HTML. That means proper heading hierarchy, accurate ARIA usage only when native semantics aren't enough, real focus management that doesn't trap users or lose their place, and full keyboard navigability without requiring a mouse. These are structural changes that happen at the code level, not in a browser-layer script.&lt;/p&gt;

&lt;p&gt;The only real path to WCAG compliance is a manual audit performed by people who use assistive technologies every day, followed by source-code remediation. At OnlyEnable, we do exactly that: we review your site manually, identify the barriers that actual users face, and fix the code itself rather than masking the symptoms.&lt;/p&gt;

&lt;p&gt;If you're serious about making your site truly accessible — not just legally defensible — start with the source. Learn more about how we help teams build inclusive digital experiences at &lt;a href="https://onlyenable.com" rel="noopener noreferrer"&gt;OnlyEnable&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>a11y</category>
      <category>wcag</category>
    </item>
    <item>
      <title>Fixed Before Anyone Notices, Stronger After Every Fix: Self-Healing + Recurrence Prevention (Series Part 4)</title>
      <dc:creator>Ryosuke Tsuji</dc:creator>
      <pubDate>Mon, 01 Jun 2026 23:57:25 +0000</pubDate>
      <link>https://dev.to/ryantsuji/fixed-before-anyone-notices-stronger-after-every-fix-self-healing-recurrence-prevention-series-1e86</link>
      <guid>https://dev.to/ryantsuji/fixed-before-anyone-notices-stronger-after-every-fix-self-healing-recurrence-prevention-series-1e86</guid>
      <description>&lt;p&gt;Hi, I'm &lt;a href="https://x.com/ryantsuji" rel="noopener noreferrer"&gt;Ryan&lt;/a&gt;, CTO at airCloset.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer&lt;/strong&gt;: "cortex" in this article is the internal codename for an AI platform built in-house at airCloset. It is unrelated to existing commercial services like Snowflake Cortex or Palo Alto Networks Cortex.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In &lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;Part 3&lt;/a&gt; I covered &lt;strong&gt;AI reviewing AI PRs&lt;/strong&gt; -- the auto-review pipeline that defends quality &lt;strong&gt;at the PR stage&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This post is the other side: &lt;strong&gt;defending quality in production&lt;/strong&gt;, via &lt;strong&gt;Self-Healing&lt;/strong&gt;. A production alert fires, an AI investigates it, opens a fix PR, the PR goes through the same auto-review pipeline from Part 3, gets auto-merged and auto-redeployed. And the same fix PR is &lt;strong&gt;required to add a new Guide -- whether that's a lint rule, CI guard, type constraint, or guideline update&lt;/strong&gt; -- so the same anti-pattern gets auto-rejected from then on. The guardrails grow every time.&lt;/p&gt;

&lt;p&gt;"Incidents get fixed automatically" is catchy on its own, but on its own it's probably not enough in the long run. You have to &lt;strong&gt;close the recurrence class while you fix the incident&lt;/strong&gt; -- self-healing &lt;strong&gt;plus&lt;/strong&gt; self-strengthening -- before the quality gates start to compound over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with last month's numbers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;115 Self-Healing PRs merged in the past 30 days.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Effectively all of them merged and deployed without human involvement.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Humans only step in when the AI judges "this is not something code can fix."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the current state of "incident response" at cortex.&lt;/p&gt;

&lt;p&gt;Don't read "115 = 115 user-impacting incidents" though. Roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;About half (54) are Deploy Failed-style alerts&lt;/strong&gt; -- CI / Pulumi deploy step caught a failure, the AI absorbed it &lt;strong&gt;before it shipped to production&lt;/strong&gt;. Recently the &lt;code&gt;[Recurrence]&lt;/code&gt; loop (covered later) has been piling up countermeasures here, so this bucket is trending down anecdotally&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The remaining 61 are production-runtime alerts&lt;/strong&gt; (Service Error Log Detected / Pipeline Failure / Generator Failure etc.) -- the service is running in production, but an error-log threshold or consecutive-failure threshold tripped. The AI absorbed them before they propagated to user impact&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So it's less "incident response" than "&lt;strong&gt;production anomalies that monitoring caught, fixed 115 times by AI before anyone woke up&lt;/strong&gt;." The number of incidents humans actually have to acknowledge is in the low single digits per month.&lt;/p&gt;

&lt;p&gt;There's also a clear pattern of &lt;strong&gt;the same service firing repeatedly&lt;/strong&gt; (e.g. &lt;code&gt;gcs-transformer&lt;/code&gt; is 25 of the 61) -- which is exactly what the &lt;code&gt;[Recurrence]&lt;/code&gt; loop covered later is supposed to &lt;strong&gt;eliminate by turning into lint or type gates&lt;/strong&gt;. That's the back half of this post.&lt;/p&gt;

&lt;p&gt;One more honest note: &lt;strong&gt;the recent month's number is slightly inflated&lt;/strong&gt;. The codebase had a fair number of "silent catch" patterns -- catch blocks that swallow exceptions without logging anything. We added the &lt;code&gt;no-silent-catch&lt;/code&gt; lint rule and &lt;strong&gt;swept the existing silent catches in batches&lt;/strong&gt;, which exposed previously hidden production errors as alerts. So part of the spike is "monitoring caught up to reality." Once the &lt;code&gt;[Recurrence]&lt;/code&gt; loop converts these into lint over time, the number should converge. &lt;strong&gt;"Things we couldn't see, we can see now" is a quality improvement&lt;/strong&gt; -- what we're seeing is the catch-up phase.&lt;/p&gt;

&lt;p&gt;One more thing worth saying: doing this by hand is utterly unsustainable. Running 115 manual cycles of "ack alert -&amp;gt; read logs -&amp;gt; context switch -&amp;gt; understand the code -&amp;gt; fix -&amp;gt; open PR -&amp;gt; review -&amp;gt; deploy" would bankrupt any team's engineering bandwidth. &lt;strong&gt;The system absorbs them without anyone noticing, and converts the fix into a new Guide (lint / CI guard / type constraint / guideline) at the same time&lt;/strong&gt; -- that's the actual subject of this post.&lt;/p&gt;

&lt;p&gt;The moment an alert fires, the AI starts an investigation, traces Loki / Product Graph / git blame to root cause, opens a fix PR, runs it through the auto-review from &lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;Part 3&lt;/a&gt;, APPROVE -&amp;gt; auto-merge -&amp;gt; auto-redeploy. One full loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Series
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Theme&lt;/th&gt;
&lt;th&gt;Key scene&lt;/th&gt;
&lt;th&gt;Article&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Series intro: cortex harness&lt;/td&gt;
&lt;td&gt;PRs merging unattended / incidents fixed before anyone notices&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/building-a-real-ai-harness-auto-reviewed-prs-self-healing-ops-and-non-engineer-contributors-3lfa"&gt;ai-harness-intro&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Product Graph (cpg)&lt;/td&gt;
&lt;td&gt;Code / docs / DB / infra unified into one graph&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;cortex-product-graph&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Auto PR review&lt;/td&gt;
&lt;td&gt;webhook -&amp;gt; AI review -&amp;gt; auto-fix -&amp;gt; squash merge&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;cortex-auto-review&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Self-Healing + observability + auto-added guardrails&lt;/td&gt;
&lt;td&gt;Alert -&amp;gt; AI investigates -&amp;gt; fix PR + new lint/type gate -&amp;gt; auto redeploy + same pattern auto-rejected from then on&lt;/td&gt;
&lt;td&gt;This article ← you are here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Scaling the harness from cortex to toC services&lt;/td&gt;
&lt;td&gt;Non-engineer contributions in practice + scaling cortex's harness to the whole product org&lt;/td&gt;
&lt;td&gt;Coming soon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Series wrap-up&lt;/td&gt;
&lt;td&gt;The underlying philosophy (what was given up, what was kept, why this design) plus a retrospective on the failures and lessons&lt;/td&gt;
&lt;td&gt;Coming soon&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Big picture -- the three layers: Observation, Repair, Strengthening
&lt;/h2&gt;

&lt;p&gt;For Self-Healing to work, you need an &lt;strong&gt;Observation layer&lt;/strong&gt; in front and a &lt;strong&gt;Strengthening layer&lt;/strong&gt; (recurrence prevention) behind it. Self-Healing itself is the middle &lt;strong&gt;Repair layer&lt;/strong&gt;. The "self-healing + self-strengthening" loop only spins up when all three are in place.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;: The three layers only stand up on top of two prior pieces: &lt;strong&gt;cpg&lt;/strong&gt; (the unified code / docs / DB / infra knowledge graph from &lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;Part 2&lt;/a&gt;) and the &lt;strong&gt;Observability stack&lt;/strong&gt; covered in this post.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No Observability&lt;/strong&gt; -&amp;gt; the observation layer is empty, nothing gets detected -&amp;gt; the repair layer never even fires&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cpg&lt;/strong&gt; -&amp;gt; the AI cannot see "where else does this trap exist" -&amp;gt; the repair layer does symptom-level patching at best, and the strengthening layer's horizontal expansion stops working&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Put differently: &lt;strong&gt;trying to copy this setup without those two will just multiply incidents&lt;/strong&gt;. An AI that blindly looks at error logs and rewrites production code is just speeding up the rate at which &lt;code&gt;gh pr create&lt;/code&gt; ships accidents. cpg and Observability are the &lt;strong&gt;minimum bar&lt;/strong&gt; for being able to delegate auto-repair to AI.&lt;/p&gt;

&lt;p&gt;Note also that cortex is a &lt;strong&gt;several-hundred-thousand-line codebase&lt;/strong&gt;, and at that scale loading the whole codebase as AI context is &lt;strong&gt;impossible for the AI as well&lt;/strong&gt; (let alone for a human). Tell the AI to trace impact with just grep and file reads, and it'll run out of context window before it finds anything. cpg is what lets it ask "which other code does this function's change ripple into" and get the answer in one hop. Small repos may not need this. Past a certain scale, cpg is not optional, it's &lt;strong&gt;required&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In Fowler's Guides / Sensors terms from Part 1, cpg and Observability are &lt;strong&gt;the substrate that supports both Guides (pre-execution controls like lint) and Sensors (post-execution gates like auto-review and Self-Healing)&lt;/strong&gt;. Observability feeds Sensors via firing alerts; cpg feeds the Guides side by supplying the auto-review with impact-scoping context. &lt;strong&gt;Neither belongs on one side only&lt;/strong&gt; -- they're foundational to both, and Self-Healing and auto-review only function on top of this substrate. That's the structural claim this post is built around.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9oqc8k3k9f6ljwddm50b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9oqc8k3k9f6ljwddm50b.png" alt="Three layers -- Observation -&amp;gt; Repair -&amp;gt; Strengthening loop"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Key components&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Observation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Real-time detection of production anomalies&lt;/td&gt;
&lt;td&gt;OTel SDK / Loki / Mimir / Tempo / Faro / Grafana / Pino logs with trace_id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Repair&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI receives the alert, investigates root cause, opens a fix PR, auto-review, auto-merge, auto-redeploy&lt;/td&gt;
&lt;td&gt;Event Relay -&amp;gt; SSE -&amp;gt; &lt;code&gt;self-healing&lt;/code&gt; mode script -&amp;gt; claude -p (worktree) -&amp;gt; gh pr create&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Strengthening&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The fix PR is required to add a new Guide (lint / CI guard / type constraint / guideline). The same anti-pattern can't reach production again&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@cortex/eslint-plugin-graph&lt;/code&gt; (26 rules), &lt;code&gt;scripts/check-*.ts&lt;/code&gt; (13 guards), &lt;a href="https://github.com/air-closet/cortex-review-guidelines/blob/main/en/guidelines/recurrence-prevention.md" rel="noopener noreferrer"&gt;&lt;code&gt;recurrence-prevention.md&lt;/code&gt;&lt;/a&gt;, the &lt;code&gt;[Recurrence]&lt;/code&gt; lens of auto-review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I'll walk through them in order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observation -- where do the alerts come from?
&lt;/h2&gt;

&lt;p&gt;cortex's production observability is built on &lt;strong&gt;Grafana Cloud + OpenTelemetry&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OTel SDK&lt;/strong&gt; (the shared &lt;code&gt;@cortex/otel&lt;/code&gt; package) -- every service calls &lt;code&gt;initOtel({ serviceName })&lt;/code&gt; at its entry point. Trace / metric / log all go out via OTLP to Grafana Cloud&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loki&lt;/strong&gt; (logs) -- Pino structured logs get &lt;code&gt;trace_id&lt;/code&gt; automatically. trace and log are cross-referenced&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mimir&lt;/strong&gt; (metrics) -- Cloud Run / pipeline / Gemini API token usage, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tempo&lt;/strong&gt; (traces) -- distributed tracing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faro&lt;/strong&gt; (frontend) -- captures browser JS errors / performance / network failures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grafana&lt;/strong&gt; -- dashboards + Alert Rules + Notification Policy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also have &lt;strong&gt;a strict definition of log levels, anchored on business impact&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Definition&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;warn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Business-foreseeable, &lt;strong&gt;does not need immediate action&lt;/strong&gt; (retryable / self-recovers).&lt;/td&gt;
&lt;td&gt;Search query returned 0 results, optional field unset, short retry due to rate limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Data recovery / re-run will definitely be needed afterward&lt;/strong&gt;. Impact expected to be under 20%.&lt;/td&gt;
&lt;td&gt;"User record that should exist isn't there," BigQuery insert failure, per-record enrichment failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fatal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The feature as a whole &lt;strong&gt;fails for 20%+ of requests&lt;/strong&gt;. Service-continuity broken, fatal config missing, full upstream outage.&lt;/td&gt;
&lt;td&gt;OTel init failure, required secret missing at startup, full input data source outage for a pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key point is to &lt;strong&gt;not pick the level mechanically based on the exception class name&lt;/strong&gt; like &lt;code&gt;NotFoundError&lt;/code&gt;. Same "record not found" situation: "this record must exist and doesn't" is &lt;code&gt;error&lt;/code&gt; / &lt;code&gt;fatal&lt;/code&gt;; "user search returned 0 hits" is &lt;code&gt;warn&lt;/code&gt;. &lt;strong&gt;The level is decided by business impact&lt;/strong&gt; -- "does this require data recovery later," "is the whole feature down" -- not by the type. Without this discipline you simultaneously get monitoring fatigue and missed critical incidents. Self-Healing reacts mainly to &lt;code&gt;error&lt;/code&gt;-threshold trips; &lt;code&gt;fatal&lt;/code&gt; is the human-escalation side.&lt;/p&gt;

&lt;p&gt;Alert Rules are &lt;strong&gt;managed declaratively in Pulumi&lt;/strong&gt;, grouped by service into categories like &lt;code&gt;BOT / Pipeline / Transformer / Generator / Gemini / CI / Deploy / Service Catch-All&lt;/code&gt;. When we add a new service, one line in infra code spins up the dashboards and alerts automatically.&lt;/p&gt;

&lt;p&gt;This is "the infrastructure that lets &lt;strong&gt;the AI see the same things humans see&lt;/strong&gt;." Self-Healing picks up alerts coming off this stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Observability can't catch, Self-Healing can't fix either
&lt;/h3&gt;

&lt;p&gt;Honest disclaimer: Self-Healing can only react to &lt;strong&gt;what the observation layer can detect as an anomaly&lt;/strong&gt;. "Observability is everything" is literally true here.&lt;/p&gt;

&lt;p&gt;What the current stack catches is roughly &lt;strong&gt;logic-level errors&lt;/strong&gt; -- exceptions, error logs, deploy failures, external-API call failures, threshold-based metric anomalies.&lt;/p&gt;

&lt;p&gt;What it doesn't catch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;UI errors&lt;/strong&gt; -- the logic ran, no error logs, but the screen &lt;strong&gt;shows something different from intent / shows the wrong value&lt;/strong&gt;. Faro catches client-side JS exceptions and network failures, but "the logic ran and the output is just wrong" never fires an alert&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent data corruption&lt;/strong&gt; -- aggregated values slowly drift, bad values get into a table. Unless it crosses a threshold or schema check, nothing detects it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Perceived UX degradation&lt;/strong&gt; -- requests feel slow, the UX feels off. Only catchable once SLO / latency thresholds trip&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So Self-Healing is "&lt;strong&gt;AI replacing the human in the loop for incidents the observation layer can catch&lt;/strong&gt;." &lt;strong&gt;The coverage of the observation layer itself is the prerequisite.&lt;/strong&gt; Holes in observation stay as blind spots that neither auto-review nor Self-Healing reaches.&lt;/p&gt;

&lt;p&gt;This isn't really a limitation of Self-Healing -- it's the &lt;strong&gt;importance of growing the observation stack&lt;/strong&gt;, which cortex keeps investing in continuously. (From &lt;a href="https://dev.to/ryantsuji/building-a-real-ai-harness-auto-reviewed-prs-self-healing-ops-and-non-engineer-contributors-3lfa"&gt;Part 1&lt;/a&gt;, Observability is one of the "supporting foundations" beneath the flywheel.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Repair -- the Self-Healing flow
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;MODE=self-healing&lt;/code&gt; runs the same &lt;code&gt;webhook-server&lt;/code&gt; script as the auto-review setup from &lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;Part 3&lt;/a&gt;, but listening for Grafana firing alerts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjoa2w71p3guljzdg6sou.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjoa2w71p3guljzdg6sou.png" alt="Self-Healing full flow -- median 30 min to 1 hr from firing alert to production recovery"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The textual flow looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Grafana Alert Rule firing]
   ↓ POST /webhook/grafana
[Event Relay (in-house)] -- persisted in Firestore
   ↓ SSE push (event: grafana-alert)
[self-healing mode script]
   ↓ throttle check (same fingerprint skipped for 4h)
   ↓ 👀 reaction in Slack to signal "I'm on it"
   ↓ git worktree add -b hotfix/auto-alert-{service}-{ts} origin/main
   ↓ run claude -p inside the worktree
     - search related code via Product Graph MCP
     - pull error logs from Loki via Grafana MCP
     - identify root cause and fix
     - update tests as needed
     - conventional commit
   ↓ git push + gh pr create
[fix PR]
   ↓ auto-review (the Part 3 pipeline)
   ↓ APPROVE -&amp;gt; auto-merge -&amp;gt; auto-redeploy
[recovered]
   ↓ ✅ in the Slack thread
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What happens when the AI judges "this is not fixable in code"
&lt;/h3&gt;

&lt;p&gt;Not every alert is fixable by code. The implementation has a rule: "if you judge it unfixable, exit without changing anything." In that case Slack gets a notification of the form "&lt;strong&gt;This alert cannot be addressed in code. Investigation: ...&lt;/strong&gt;" -- &lt;strong&gt;including what the AI investigated&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Worth clarifying on the numbers side: the headline &lt;strong&gt;115&lt;/strong&gt; is "Self-Healing runs that reached PR-created -&amp;gt; merged -&amp;gt; deployed." This "unfixable, exit clean" case is a &lt;strong&gt;separate bucket&lt;/strong&gt;, happening several times a month (external transient outages, infra / config issues that aren't code, cases too complex for the AI to judge confidently). &lt;strong&gt;The "humans show up" bucket is this separate one&lt;/strong&gt; -- it isn't a "some of the 115 failed" failure rate.&lt;/p&gt;

&lt;p&gt;Here's what a real Slack message looks like (a &lt;code&gt;styling-pattern-watch-transformer&lt;/code&gt; case where the GitHub PAT had expired):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ This alert cannot be addressed in code. Investigation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Investigation summary&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Checked the error logs in Loki for the past hour. The single error source for &lt;code&gt;styling-pattern-watch-transformer&lt;/code&gt; right now is &lt;strong&gt;expiration / revocation of the GitHub PAT (Personal Access Token)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I found&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The BQ snapshot pipeline is healthy: 7 consecutive &lt;code&gt;[BQ] Snapshot rows inserted&lt;/code&gt; successes. The earlier autodetect type-drift problem was resolved in an earlier PR that put &lt;code&gt;SAFE_CAST(... AS INT64)&lt;/code&gt; in place.&lt;/li&gt;
&lt;li&gt;The single ongoing error (multiple occurrences in the past hour): &lt;code&gt;GithubUnauthorizedError: GitHub API /commits/master unauthorized (401): PAT likely expired/revoked&lt;/code&gt; -- the &lt;code&gt;/check-drift&lt;/code&gt; handler returns 401 every time Cloud Scheduler invokes it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Root cause&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The GitHub PAT held in Secret Manager for this service has expired or been revoked. The code already classifies the 401 with a dedicated &lt;code&gt;GithubUnauthorizedError&lt;/code&gt; and is already sending the "PAT needs rotation" notification to Slack. &lt;strong&gt;The detection / notification design on the code side is working as intended.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resolution: exit without changes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is an external-credential operational issue (PAT expiry), and what needs to happen is &lt;strong&gt;a human rotates the PAT in Secret Manager&lt;/strong&gt;. Not the kind of incident code can resolve. Therefore: no code change, no commit, no PR.&lt;/p&gt;

&lt;p&gt;Operator actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add a new version of the target secret in GCP Console / Secret Manager with a valid PAT (&lt;code&gt;repo&lt;/code&gt; scope, read access to the target repository)&lt;/li&gt;
&lt;li&gt;No Cloud Run revision redeploy needed (&lt;code&gt;secretKeyRef version:latest&lt;/code&gt; is referenced)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;At this level of detail, "what was investigated, why code can't fix it, what the human should do" all come out in one Slack message. Open the thread and the operator can act immediately. The productivity gap vs. "alerts just forwarded blindly" is significant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deduplication
&lt;/h3&gt;

&lt;p&gt;A throttle ensures the same &lt;code&gt;fingerprint&lt;/code&gt; (Grafana's unique alert identifier) is &lt;strong&gt;not re-processed for 4 hours&lt;/strong&gt;. Without this, alerts that fire again before the fix PR has merged would spawn another worktree, another fix PR, and so on -- an easy infinite loop.&lt;/p&gt;

&lt;p&gt;We also &lt;strong&gt;permanently skip&lt;/strong&gt; any &lt;code&gt;alertname&lt;/code&gt; containing &lt;code&gt;credential&lt;/code&gt;. Credential incidents carry leakage risk if the AI touches them, so they're explicitly escalated to humans.&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-Healing and Part 3 auto-review -- "the fixer AI" and "the reviewer AI" are independent
&lt;/h3&gt;

&lt;p&gt;This is the most consequential design choice of the agent setup, so calling it out explicitly.&lt;/p&gt;

&lt;p&gt;PRs opened by Self-Healing are &lt;strong&gt;not special PRs, just fix PRs&lt;/strong&gt;. They go through the Part 3 auto-review pipeline &lt;strong&gt;under exactly the same conditions&lt;/strong&gt; -- the 9 lenses (Graph / Architecture / Security / Test / Doc / Impact / Observability / AI-Antipattern / Recurrence) get checked in order. Critical / Major findings -&amp;gt; &lt;code&gt;REQUEST_CHANGES&lt;/code&gt;; Nit-only / no findings + CI green -&amp;gt; &lt;code&gt;APPROVE&lt;/code&gt; -&amp;gt; auto-merge.&lt;/p&gt;

&lt;p&gt;The important bit: &lt;strong&gt;this is not a monolithic "AI fixing AI" loop&lt;/strong&gt;. The fixer-side AI and the reviewer-side AI are fully independent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Different process, different session&lt;/strong&gt;: the self-healing-mode AI and the reviewer-mode AI are launched as separate &lt;code&gt;claude -p&lt;/code&gt; processes. They do not share context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Different input sources&lt;/strong&gt;: the fixer builds the problem from Grafana alert + Loki + cpg. The reviewer judges from the PR diff + cpg + review guidelines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Different objectives&lt;/strong&gt;: the fixer is optimizing for "stop the incident." The reviewer is judging "does this violate the 9 lenses or the severity contract?" A deliberate separation of concerns where the two roles' incentives are intentionally misaligned&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a result, &lt;strong&gt;PRs the fixer dashed off get blocked by the reviewer&lt;/strong&gt; (REQUEST_CHANGES -&amp;gt; back to the fixer). The AI does not approve its own output. "Just-make-it-work" fixes don't get through.&lt;/p&gt;

&lt;p&gt;This is the often-debated &lt;strong&gt;review-independence&lt;/strong&gt; problem in LLM-agent operation, solved here in the obvious way: split the work across separate agents.&lt;/p&gt;

&lt;h3&gt;
  
  
  A concrete example: meet subscription's 409 ALREADY_EXISTS
&lt;/h3&gt;

&lt;p&gt;Take the alert from the Google Meet recording auto-fetch service I covered in &lt;a href="https://dev.to/ryantsuji/how-we-built-an-automated-meeting-intelligence-system-with-google-meet-slack-and-rag-42ln"&gt;the Meeting Intelligence post&lt;/a&gt;. On 2026-05-21, Self-Healing opened a fix PR titled &lt;code&gt;fix(meet-subscription-renewal): auto-fix for Service Error Log Detected&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The trigger error from Loki:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Workspace Events API request failed: 409 Conflict
"Subscription associated with the resource already exists."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How the AI investigated:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pinned the error in Loki&lt;/strong&gt; -- ran &lt;code&gt;{service_name="meet-subscription-renewal"} | json | level=~"ERROR|error|Error"&lt;/code&gt; via Grafana MCP, picked up the &lt;code&gt;Failed to renew Meet subscription&lt;/code&gt; stack trace&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traced the call path in Product Graph&lt;/strong&gt; -- identified &lt;code&gt;renewSubscriptions&lt;/code&gt; -&amp;gt; &lt;code&gt;createMeetSubscription&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-referenced past PRs&lt;/strong&gt; -- the "opposite-direction inconsistency" (name in Firestore but missing from Google = 404) had already been self-healed in another PR with &lt;code&gt;patchMeetSubscriptionTtl&lt;/code&gt; -&amp;gt; null fallback. &lt;strong&gt;The current direction (still on Google's side but missing from Firestore = 409) was the gap&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verdict&lt;/strong&gt;: "the same pattern may exist elsewhere" -- a [Recurrence] decision matrix "&lt;strong&gt;horizontal expansion required&lt;/strong&gt;" case&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instead of a quick patch, &lt;strong&gt;it implemented the same-direction self-healing symmetrically to the opposite-direction fallback that was already there&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Made &lt;code&gt;createMeetSubscription&lt;/code&gt; idempotent&lt;/li&gt;
&lt;li&gt;If POST returns 409, extract the existing Subscription name from the response and call &lt;code&gt;patchMeetSubscriptionTtl&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The caller writes the return value back into Firestore, so the next renewal converges to the normal PATCH path (&lt;strong&gt;self-healing&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;Per the existing &lt;code&gt;graph/no-silent-catch&lt;/code&gt; lint, JSON.parse failures are also &lt;code&gt;logger.warn&lt;/code&gt; + &lt;code&gt;serializeError&lt;/code&gt; for structured logging&lt;/li&gt;
&lt;li&gt;Three tests added&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is what "Self-Healing pushing all the way to root cause and rolling the fix out horizontally" looks like in practice. &lt;strong&gt;"Close the recurrence class, don't just suppress the symptom"&lt;/strong&gt; (the spirit of &lt;a href="https://github.com/air-closet/cortex-review-guidelines/blob/main/en/guidelines/recurrence-prevention.md" rel="noopener noreferrer"&gt;&lt;code&gt;recurrence-prevention.md&lt;/code&gt;&lt;/a&gt;) executed autonomously by the AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strengthening -- Guides (lint + guidelines) grow automatically
&lt;/h2&gt;

&lt;p&gt;This is the layer that &lt;strong&gt;keeps Self-Healing from being just "auto-repair."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In Fowler's Guides / Sensors terms from &lt;a href="https://dev.to/ryantsuji/building-a-real-ai-harness-auto-reviewed-prs-self-healing-ops-and-non-engineer-contributors-3lfa"&gt;Part 1&lt;/a&gt;, the Strengthening layer is &lt;strong&gt;the place where Guides grow&lt;/strong&gt; -- i.e. the pre-execution controls that prevent AI from deviating in the first place. cortex's Guides come in two flavors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Machine-read Guides&lt;/strong&gt;: lint / type / CI guard / coverage thresholds / Prettier -- enforced at commit / CI time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human-and-AI-read Guides&lt;/strong&gt;: guidelines like &lt;a href="https://github.com/air-closet/cortex-review-guidelines/blob/main/en/guidelines/recurrence-prevention.md" rel="noopener noreferrer"&gt;&lt;code&gt;recurrence-prevention.md&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/air-closet/cortex-review-guidelines/blob/main/en/guidelines/severity.md" rel="noopener noreferrer"&gt;&lt;code&gt;severity.md&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/air-closet/cortex-review-guidelines/blob/main/en/guidelines/ai-antipattern.md" rel="noopener noreferrer"&gt;&lt;code&gt;ai-antipattern.md&lt;/code&gt;&lt;/a&gt;, etc. -- used as decision criteria by auto-review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 9 lenses, severity contract, and no-downgrade rules from &lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;Part 3&lt;/a&gt; are the latter; the auto-added lints in Part 4 are the former. &lt;strong&gt;Together they form the Guides surface&lt;/strong&gt;. Lints are "formalized guidelines," guidelines are "lints that haven't been formalized yet."&lt;/p&gt;

&lt;p&gt;The Sensors side -- Self-Healing and auto-review -- &lt;strong&gt;grow these Guides every time they run&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Self-Healing's root-cause investigation finds "the same pattern exists elsewhere" -&amp;gt; demands horizontal expansion + a new lint (= new Guide)&lt;/li&gt;
&lt;li&gt;Auto-review's &lt;code&gt;[Recurrence]&lt;/code&gt; lens blocks PRs that fix without adding lint&lt;/li&gt;
&lt;li&gt;Both depend on &lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;cpg&lt;/a&gt; to see impact scope across the codebase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;cpg is what lets the AI ask "where else does this trap exist." Self-Healing and auto-review (= the Sensors side) &lt;strong&gt;share cpg as a substrate, and each run thickens Guides by one notch&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5m28vtcko417bg44sr3e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5m28vtcko417bg44sr3e.png" alt="cpg as the shared substrate; Self-Healing and auto-review (Sensors) grow Guides"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens every time Self-Healing runs (the recurrence-prevention-first flow)
&lt;/h3&gt;

&lt;p&gt;Every fix PR Self-Healing opens is checked for &lt;code&gt;[Recurrence]&lt;/code&gt; by auto-review. The decision matrix:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Required action&lt;/th&gt;
&lt;th&gt;Form&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Same trap stepped on 2+ times&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Lint required&lt;/strong&gt; (custom ESLint rule / type constraint / CI guard)&lt;/td&gt;
&lt;td&gt;Machine (new Guide)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pattern may exist elsewhere&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Horizontal expansion required&lt;/strong&gt; (cpg traversal for similar nodes, fix all of them in this PR)&lt;/td&gt;
&lt;td&gt;Investigation + fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cannot be machine-checked but worth formalizing&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Add to an existing guideline&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Guideline entry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One-off, no value in formalization&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Nothing&lt;/strong&gt; (bug fix only)&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When the "stepped on 2+ times" situation applies, &lt;strong&gt;the fix PR can't merge without a new lint included&lt;/strong&gt;. So every Self-Healing run produces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Horizontal expansion via cpg&lt;/strong&gt; -- not just the immediate fix target, every similar node enumerated&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A new Guide added in the same PR&lt;/strong&gt; -- ESLint custom rule / type constraint / CI guard / guideline entry, one of the four&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All existing violations cleared in the same PR&lt;/strong&gt; -- no &lt;code&gt;warn&lt;/code&gt;-as-deferral, &lt;code&gt;error&lt;/code&gt; on first introduction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-review -&amp;gt; auto-merge -&amp;gt; auto-redeploy&lt;/strong&gt; -- the regular Part 3 pipeline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Going forward, writing the same pattern gets mechanically rejected by CI / lint&lt;/strong&gt; -- the recurrence class is structurally closed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3imbzr9glo3hdw7wqwps.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3imbzr9glo3hdw7wqwps.png" alt="5 steps every Self-Healing run produces -- recurrence-prevention-first flow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;"Add the guard while you fix the bug" runs as a self-sustaining loop driven by Self-Healing.&lt;/p&gt;

&lt;h3&gt;
  
  
  "We'll do it later" and "introduce as &lt;code&gt;warn&lt;/code&gt;" are banned
&lt;/h3&gt;

&lt;p&gt;A couple of important contract clauses from the guidelines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Plan to lint later," "lint when we refactor," "another PR will handle this" -- &lt;strong&gt;all banned&lt;/strong&gt;. If it can be addressed in this PR, it must be&lt;/li&gt;
&lt;li&gt;"Existing violations remain, so introduce as &lt;code&gt;warn&lt;/code&gt; and promote to &lt;code&gt;error&lt;/code&gt; later" -- &lt;strong&gt;not accepted&lt;/strong&gt;. This is deferral in disguise. The responsibility for the &lt;code&gt;warn&lt;/code&gt;-&amp;gt;&lt;code&gt;error&lt;/code&gt; promotion goes nowhere and the rule rots&lt;/li&gt;
&lt;li&gt;If you add a lint rule, &lt;strong&gt;fix all existing violations in the same PR and ship at &lt;code&gt;error&lt;/code&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These extend the &lt;strong&gt;no-downgrade rules&lt;/strong&gt; from &lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;Part 3&lt;/a&gt; -- preempting the typical escape hatches.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "step on it, mechanize it" lineage
&lt;/h3&gt;

&lt;p&gt;Custom Guides currently piled up in cortex:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;graph/no-silent-catch&lt;/code&gt;&lt;/strong&gt; (ESLint) -- the source of the "inflated number" mentioned in the intro. Bans catch blocks that swallow exceptions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stacktrace-preservation guideline&lt;/strong&gt; (codified as a Major violation in &lt;a href="https://github.com/air-closet/cortex-review-guidelines/blob/main/en/guidelines/observability.md" rel="noopener noreferrer"&gt;&lt;code&gt;observability.md&lt;/code&gt;&lt;/a&gt;, caught by auto-review) -- forbids &lt;code&gt;logger.error(err.message)&lt;/code&gt; style logs that drop the stack and keep only the message string. Forces the &lt;code&gt;err&lt;/code&gt; field to hold &lt;code&gt;serializeError(error)&lt;/code&gt; so &lt;code&gt;name&lt;/code&gt; / &lt;code&gt;message&lt;/code&gt; / &lt;code&gt;stack&lt;/code&gt; are preserved as structured fields. &lt;strong&gt;Observability is everything&lt;/strong&gt; here, so logs that drop stack info are treated as inherently broken&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cortex-quality/require-fetch-timeout&lt;/code&gt;&lt;/strong&gt; (oxlint -- a Rust-implemented JS/TS lint that runs ESLint-compatible rule sets, dozens of times faster than ESLint due to the Rust impl. cortex uses oxlint for the standard ruleset and ESLint for custom rules that need AST-level work) -- mandates &lt;code&gt;signal: AbortSignal.timeout(...)&lt;/code&gt; on external &lt;code&gt;fetch&lt;/code&gt; calls. Born from a case where a no-timeout &lt;code&gt;fetch&lt;/code&gt; hung indefinitely and triggered a Cloud Tasks redelivery storm&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;graph/no-bq-string-timestamp-param&lt;/code&gt;&lt;/strong&gt; (ESLint) -- from a case where passing TIMESTAMP as a string to a BigQuery query parameter NULLed the value out through a serializer bug and silently failed every INSERT&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;graph/require-firestore-ignore-undefined&lt;/code&gt;&lt;/strong&gt; (ESLint) -- forces &lt;code&gt;ignoreUndefinedProperties: true&lt;/code&gt; on &lt;code&gt;new Firestore()&lt;/code&gt;. From a case where a single NULL row caused a 100% failure rate in a sync batch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;check-otel-env-injection&lt;/code&gt;&lt;/strong&gt; (CI guard) -- the recurrence prevention for the Cloud Run OTel env injection case below&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript type tightening&lt;/strong&gt; (type level) -- tighter function signatures, branded types for ID disambiguation, exhaustive discriminated unions, etc. Patterns that can't be lint-caught but are catchable at the type level get closed from the type side&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't textbook-learnable rules -- they're "&lt;strong&gt;stepped on once, then mechanized&lt;/strong&gt;." The number of traps the organization has stepped on translates directly into the number of Guides piled up (across ESLint / oxlint / CI guard / types).&lt;/p&gt;

&lt;h3&gt;
  
  
  How does the AI write a lint rule without breaking it?
&lt;/h3&gt;

&lt;p&gt;Three structural things keep this sane:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Existing rules are the template&lt;/strong&gt;: &lt;code&gt;packages/eslint-plugin-graph/src/rules/&lt;/code&gt; already holds 26 custom rules, each as &lt;code&gt;.ts&lt;/code&gt; + &lt;code&gt;.test.ts&lt;/code&gt; pairs. New rules follow the same shape, so the AI never has to write the AST-walking boilerplate from scratch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests first&lt;/strong&gt;: violation / pass fixtures go into &lt;code&gt;.test.ts&lt;/code&gt; first, implementation fills in TDD-style. Coverage threshold (90% statements + branches) is gated by the &lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;Part 3&lt;/a&gt; auto-review, so a lint without tests cannot merge&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;lint / type / CI guard sit in the same "mechanize" bucket&lt;/strong&gt;: the decision matrix in &lt;a href="https://github.com/air-closet/cortex-review-guidelines/blob/main/en/guidelines/recurrence-prevention.md" rel="noopener noreferrer"&gt;&lt;code&gt;recurrence-prevention.md&lt;/code&gt;&lt;/a&gt; groups lint / type constraint / CI guard together as the "lint-required" row, and leaves the choice within that bucket (write it as a lint? express it at the type level? add a separate CI guard?) to the AI based on how much AST work is involved and whether runtime semantics matter. Traps that need AST inspection but actually hinge on runtime behavior usually end up as a type constraint (branded type / discriminated union / signature tightening) rather than a custom lint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So "AI writes a lint rule" is supported by &lt;strong&gt;existing rule corpus + the test harness + the mechanize-bucket selection criteria&lt;/strong&gt; -- three together. The path where the AI hand-rolls raw ESLint API and bricks something is structurally closed.&lt;/p&gt;

&lt;h3&gt;
  
  
  A concrete example: Cloud Run OTel env injection -&amp;gt; promoted to CI guard
&lt;/h3&gt;

&lt;p&gt;Multiple services hit this trap: when a Cloud Run Service / Job is defined in Pulumi, forgetting to inject &lt;code&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/code&gt; and &lt;code&gt;GRAFANA_CLOUD_API_KEY&lt;/code&gt; via &lt;code&gt;secretKeyRef&lt;/code&gt; causes OTel init to be skipped in production, no trace/log reaches Grafana, and incidents become silently invisible.&lt;/p&gt;

&lt;p&gt;The normal response would be "we'll be more careful next time." At cortex:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Incident surfaces -&amp;gt; Self-Healing opens a fix PR (adds the env injection to the affected service)&lt;/li&gt;
&lt;li&gt;Auto-review's &lt;code&gt;[Recurrence]&lt;/code&gt; decides "same trap stepped on -&amp;gt; lint required"&lt;/li&gt;
&lt;li&gt;The same PR adds &lt;code&gt;scripts/check-otel-env-injection.ts&lt;/code&gt; (CI guard) -- mechanically asserts OTel env injection across all Cloud Run resource definitions under &lt;code&gt;infra/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;All other existing services get their env injection added in the same PR&lt;/li&gt;
&lt;li&gt;Merge -&amp;gt; deploy -&amp;gt; any future write of the same kind gets rejected by CI&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's what "the guardrails grow every time Self-Healing runs" looks like in practice. The trap is "stepped on -&amp;gt; mechanically checked from then on."&lt;/p&gt;

&lt;h3&gt;
  
  
  Where Guides stand right now (in numbers)
&lt;/h3&gt;

&lt;p&gt;Snapshot of cortex's Guide inventory:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Custom ESLint rules&lt;/strong&gt; (&lt;code&gt;@cortex/eslint-plugin-graph&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;no-silent-catch&lt;/code&gt; / &lt;code&gt;require-firestore-ignore-undefined&lt;/code&gt; / &lt;code&gt;no-bq-string-timestamp-param&lt;/code&gt; etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;CI guards&lt;/strong&gt; (&lt;code&gt;scripts/check-*.ts&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;check-otel-env-injection&lt;/code&gt; / &lt;code&gt;check-cloudscheduler-oidctoken-audience&lt;/code&gt; etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Standard oxlint rules&lt;/strong&gt; (set to &lt;code&gt;error&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;183&lt;/td&gt;
&lt;td&gt;Base config ships everything at error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;TypeScript strict gates&lt;/strong&gt; (baseline)&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;strict&lt;/code&gt; / &lt;code&gt;noImplicitAny&lt;/code&gt; / &lt;code&gt;strictNullChecks&lt;/code&gt; / &lt;code&gt;noUncheckedIndexedAccess&lt;/code&gt; etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;TypeScript type tightening&lt;/strong&gt; (per-recurrence)&lt;/td&gt;
&lt;td&gt;grows over time&lt;/td&gt;
&lt;td&gt;branded type / discriminated union / function-signature tightening etc. Patterns that can't be lint-caught but can be type-caught are closed from the type side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Test coverage thresholds&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;statements + branches 90%&lt;/td&gt;
&lt;td&gt;Uniform across all packages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prettier&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 config&lt;/td&gt;
&lt;td&gt;Format auto-fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Guidelines&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the entire review-guidelines repo&lt;/td&gt;
&lt;td&gt;Used as the decision basis by auto-review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first two categories plus the type-tightening row -- &lt;strong&gt;Custom ESLint, CI guard, type tightening&lt;/strong&gt; -- are the part that &lt;strong&gt;compounds over time&lt;/strong&gt; through the &lt;code&gt;[Recurrence]&lt;/code&gt; lens every time Self-Healing or auto-review runs. &lt;strong&gt;The guardrails grow with time.&lt;/strong&gt; That's the substance of the Strengthening layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole loop, from the top
&lt;/h2&gt;

&lt;p&gt;When you compose the three layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[production anomaly] -&amp;gt; Observation layer (OTel/Loki/Grafana) -&amp;gt; Alert firing
                                              ↓
                                       Event Relay -&amp;gt; SSE
                                              ↓
[Self-Healing mode script]
   - claude -p in worktree
   - root cause via cpg + Loki + git blame
   - commit fix
   - (if applicable) add new lint / type gate too
   - gh pr create
                                              ↓
[Auto-review (Part 3)] -- 9 lenses in order, especially [Recurrence] forces
                         recurrence-prevention action (lint / horizontal expansion / guideline entry)
                                              ↓
                          APPROVE + CI green
                                              ↓
[auto-merge -&amp;gt; Turborepo build -&amp;gt; Pulumi parallel deploy]
                                              ↓
[production recovered + same anti-pattern mechanically rejected from now on]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The loop &lt;strong&gt;completes without human intervention&lt;/strong&gt;. Not just repair, but the quality gates that grow with every repair -- that's the "auto-recovery + auto-strengthening" substance at cortex.&lt;/p&gt;

&lt;p&gt;That said, as the front of the article spelled out, &lt;strong&gt;the loop is only viable because cpg and Observability exist&lt;/strong&gt;. cpg makes horizontal expansion possible; Observability turns production anomalies into structured data. With those two in place at the foundation, AI can stand on the side that does Repair and Strengthening. &lt;strong&gt;Self-Healing is not a standalone mechanism. It's a Sensor riding on top of cortex's Guides (cpg + Observability + lint + guidelines).&lt;/strong&gt; That's the single most important framing in this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-Healing by the numbers
&lt;/h2&gt;

&lt;p&gt;Breaking the headline down further.&lt;/p&gt;

&lt;h3&gt;
  
  
  Main firing categories
&lt;/h3&gt;

&lt;p&gt;What kicked off Self-Healing in the past 30 days (with the mapping back to the front-of-post 2 buckets):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Bucket&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Service Error Log Detected&lt;/strong&gt; (most frequent)&lt;/td&gt;
&lt;td&gt;Production-runtime (61 side)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Pipeline Failure&lt;/strong&gt; -- data pipeline failing a configured number of times in a row&lt;/td&gt;
&lt;td&gt;Production-runtime (61 side)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Generator Failure&lt;/strong&gt; -- AI generation jobs (embedding / annotation etc.) failing&lt;/td&gt;
&lt;td&gt;Production-runtime (61 side)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Deploy Failed&lt;/strong&gt; -- deploy step failures (Pulumi up / Cloud Run revision failed)&lt;/td&gt;
&lt;td&gt;Deploy step (54 side)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Alert-firing to production-recovery time
&lt;/h3&gt;

&lt;p&gt;Median &lt;strong&gt;30 minutes to 1 hour&lt;/strong&gt;. Roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Alert firing -&amp;gt; AI investigation start: under 1 minute (Event Relay + SSE)&lt;/li&gt;
&lt;li&gt;AI investigation + fix + PR open: 3-8 minutes&lt;/li&gt;
&lt;li&gt;Auto-review (including the &lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;Part 3&lt;/a&gt; &lt;strong&gt;10.8 review-fix iterations on average&lt;/strong&gt;): 20-45 minutes&lt;/li&gt;
&lt;li&gt;Auto-merge + deploy: 3-10 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many of these finish before anyone wakes up (alert fires early morning -&amp;gt; by the time people come in, there's just a ✅ in Slack).&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed / Bridge to Part 5
&lt;/h2&gt;

&lt;p&gt;We've now covered &lt;strong&gt;the cortex picture across Parts 1-4&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/ryantsuji/building-a-real-ai-harness-auto-reviewed-prs-self-healing-ops-and-non-engineer-contributors-3lfa"&gt;Part 1&lt;/a&gt;: the cortex big picture and harness-engineering framing&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;Part 2&lt;/a&gt;: Product Graph (cpg) -- the AI's "brain"&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;Part 3&lt;/a&gt;: auto-review -- defending quality at the PR stage&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 4 (this post): Self-Healing + Observability + auto-added guardrails -- defending quality in production while growing the quality gates themselves&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The engineering role has shifted, over the last half-year, from "&lt;strong&gt;write&lt;/strong&gt;, &lt;strong&gt;review&lt;/strong&gt;, &lt;strong&gt;fix&lt;/strong&gt;, &lt;strong&gt;merge&lt;/strong&gt;, &lt;strong&gt;deploy&lt;/strong&gt;, &lt;strong&gt;incident-respond&lt;/strong&gt;" -- all of that -- toward &lt;strong&gt;looking at the whole system from above and tuning it&lt;/strong&gt;. &lt;code&gt;human-on-the-loop&lt;/code&gt;, working at the Policy layer.&lt;/p&gt;

&lt;p&gt;That said, this is a pattern that solidified inside &lt;strong&gt;cortex, the internal AI platform&lt;/strong&gt;. Carrying the same pattern into real consumer-facing toC services (multiple services, multiple stacks, multiple teams) requires changes and additions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 5&lt;/strong&gt; will cover &lt;strong&gt;scaling cortex's harness to the whole product organization&lt;/strong&gt; -- the roadmap and the thinking. The first half is the actual operation of "non-engineers opening PRs into cortex" with its limits; the second half is the elements needed to extend the pattern to toC services (service-specialized review rules, the human understanding-of-AI-design process, IaC for test environments, etc.).&lt;/p&gt;

&lt;p&gt;"cortex built the pattern, toC services run that pattern at an order-of-magnitude-larger scale" -- that's the Part 5 positioning.&lt;/p&gt;

&lt;p&gt;The actual series wrap-up is &lt;strong&gt;Part 6&lt;/strong&gt;. The center of it is &lt;strong&gt;the underlying philosophy&lt;/strong&gt; -- why I picked this design, what I gave up, what I kept. Alongside that, since the series so far has been mostly "what's working," I want to look back at the failures and dead ends behind that surface, and the gap between the philosophy and the implementation. A retrospective for myself, and -- hopefully -- a reference for anyone starting down a similar path.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>github</category>
      <category>observability</category>
    </item>
    <item>
      <title>What Is an AI Agent Harness?</title>
      <dc:creator>Collin Wilkins</dc:creator>
      <pubDate>Mon, 01 Jun 2026 23:56:41 +0000</pubDate>
      <link>https://dev.to/cwilkins507/what-is-an-ai-agent-harness-2fce</link>
      <guid>https://dev.to/cwilkins507/what-is-an-ai-agent-harness-2fce</guid>
      <description>&lt;p&gt;This is for people trying to understand the infrastructure around large language models: Claude Code, Codex, Cursor, LangGraph, MCP servers, repo instructions, permissions, hooks, and all the agent plumbing that has popped up.&lt;/p&gt;

&lt;p&gt;The short answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The model is the brain. The harness is the operating environment that makes the brain useful.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or shorter:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Models generate answers. Harnesses generate trust.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Start with the agent
&lt;/h2&gt;

&lt;p&gt;At the simplest level, an AI agent is a model inside a loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You give it a task&lt;/li&gt;
&lt;li&gt;The model thinks about what to do&lt;/li&gt;
&lt;li&gt;It answers or calls a tool&lt;/li&gt;
&lt;li&gt;The tool does something&lt;/li&gt;
&lt;li&gt;The result goes back to the model&lt;/li&gt;
&lt;li&gt;The loop repeats until the task is done&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fly7vbkv278scaxranw5t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fly7vbkv278scaxranw5t.png" alt="An AI agent is a while loop: user input feeds the model, which completes and responds" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That loop can be a few dozen lines of code or a whole product with permissions, memory, tools, logs, tests, and UI around it. The loop is the seed. The harness is what makes it safe and useful in real work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same model, different results
&lt;/h2&gt;

&lt;p&gt;The easiest mistake with agents is blaming the model for everything. Sometimes the model really is the problem but it often isn't.&lt;/p&gt;

&lt;p&gt;The same model and task can produce different output because agents are probabilistic plus the environment changes what the model sees, what it can do, how it gets checked, and when it is allowed to stop.&lt;/p&gt;

&lt;p&gt;One setup produces a clean pull request, runs the tests, catches the edge case, and leaves a useful summary.&lt;/p&gt;

&lt;p&gt;While another edits the wrong file, forgets the project conventions, says "done" without verification, and hands you a pile of slop.&lt;/p&gt;

&lt;p&gt;A better model can help, a better harness narrows the variance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model vs. agent vs. harness
&lt;/h2&gt;

&lt;p&gt;People blur these words together, so separate them first.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;Plain-English meaning&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;The brain that predicts, reasons, and writes&lt;/td&gt;
&lt;td&gt;Claude, GPT, Gemini&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent&lt;/td&gt;
&lt;td&gt;The model plus a loop that lets it act&lt;/td&gt;
&lt;td&gt;Claude Code fixing a bug&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Harness&lt;/td&gt;
&lt;td&gt;The system around the agent that guides and checks the work&lt;/td&gt;
&lt;td&gt;Instructions, tools, memory, tests, hooks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool&lt;/td&gt;
&lt;td&gt;Something the agent can use&lt;/td&gt;
&lt;td&gt;Shell, browser, file search, calculator, MCP server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;Context that survives beyond one prompt&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt;, project memory, handoff notes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you only remember one line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A model thinks. An agent acts. A harness keeps the agent from acting like an idiot.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A harness is not a framework
&lt;/h2&gt;

&lt;p&gt;A framework helps humans assemble agents. LangGraph, LangChain, and similar tools give you graphs, state, nodes, tool bindings, memory, middleware, and routing. You can use those pieces to build a harness.&lt;/p&gt;

&lt;p&gt;But the framework itself is not automatically the harness.&lt;/p&gt;

&lt;p&gt;A harness is the working environment around the agent. It runs the loop, exposes tools, injects project context, enforces permissions, verifies outputs, and keeps useful memory.&lt;/p&gt;

&lt;p&gt;That distinction matters. If you buy or build a framework, you still own the harness design. If you use Claude Code, Codex, Cursor, or Windsurf, you are already working inside a harness. The question is how well it fits your codebase, risk tolerance, and workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The simplest harness you already know: CLAUDE.md
&lt;/h2&gt;

&lt;p&gt;Claude Code is a useful doorway into this idea because the harness is visible.&lt;/p&gt;

&lt;p&gt;Every serious Claude Code setup has a &lt;code&gt;CLAUDE.md&lt;/code&gt; file or the cross-tool version &lt;code&gt;AGENTS.md&lt;/code&gt;. Anthropic's docs describe memory as persistent instruction Claude reads at the start of a session. It carries commands, structure, standards, workflow preferences, and recurring mistakes.&lt;/p&gt;

&lt;p&gt;That is harness work.&lt;/p&gt;

&lt;p&gt;Your &lt;code&gt;CLAUDE.md&lt;/code&gt; isn't the agent or the model. It is one guide inside the harness.&lt;/p&gt;

&lt;p&gt;A good one feels like a brief you would give a sharp contractor before they touched your codebase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# CLAUDE.md&lt;/span&gt;

&lt;span class="gu"&gt;## Project&lt;/span&gt;
One sentence on what this project does and who uses it.

&lt;span class="gu"&gt;## Commands&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Dev: &lt;span class="sb"&gt;`npm run dev`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Build: &lt;span class="sb"&gt;`npm run build`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Type check: &lt;span class="sb"&gt;`npx tsc --noEmit`&lt;/span&gt;

&lt;span class="gu"&gt;## Architecture&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`src/lib/services/`&lt;/span&gt; - business logic
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`src/components/`&lt;/span&gt; - UI components

&lt;span class="gu"&gt;## Rules&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Never commit &lt;span class="sb"&gt;`.env`&lt;/span&gt; files or secrets
&lt;span class="p"&gt;-&lt;/span&gt; Make minimal changes and avoid unrelated refactors
&lt;span class="p"&gt;-&lt;/span&gt; Run type check after code changes
&lt;span class="p"&gt;-&lt;/span&gt; Static export only, no server-side features

&lt;span class="gu"&gt;## Workflow&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Ask before making architectural changes
&lt;span class="p"&gt;-&lt;/span&gt; Run tests before saying the task is done
&lt;span class="p"&gt;-&lt;/span&gt; When unsure, explain the tradeoff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That file doesn't impact model intelligence. It makes the environment stricter and gives the model fewer ways to wander off.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi8wcu3hjc3j79u8nj3my.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi8wcu3hjc3j79u8nj3my.png" alt="Harness architecture: agent loop on the left with user request, agent loop, model, tools, and observation — harness components on the right with instructions, permissions, verification, and memory feeding into the loop" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The harness decides what context the model gets, what it can touch, how it proves the work, and what survives after the session ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a harness actually does
&lt;/h2&gt;

&lt;p&gt;A harness has two jobs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Help the agent get it right the first time&lt;/li&gt;
&lt;li&gt;Catch problems early enough that the agent can correct itself&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Martin Fowler's framing is useful here. He splits a harness into guides and sensors.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Guides&lt;/td&gt;
&lt;td&gt;Shape the agent before it acts&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt;, skills, tool descriptions, project docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sensors&lt;/td&gt;
&lt;td&gt;Check the agent after it acts&lt;/td&gt;
&lt;td&gt;tests, linters, type checks, screenshots, review agents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;Carries forward what should survive the session&lt;/td&gt;
&lt;td&gt;project memory, handoff notes, session recaps, index files&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Guides steer the agent before it acts. Sensors tell you whether the work held up. Memory is the notebook for future reference. A useful harness needs all three.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs8b6e7442t5k8y0ldfax.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs8b6e7442t5k8y0ldfax.png" alt="The feedback loop: guides feed into agent acts, sensors check the output, passing triggers memory that loops back to guides — failing loops back to agent acts" width="799" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pay attention to the last arrow. A harness controls how the agent acts this time and how the next run starts a little less cold.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture underneath
&lt;/h2&gt;

&lt;p&gt;Once you move past the simplest version, a modern harness starts to look like a small control plane.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmwvmmask5z7yl1lx41t3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmwvmmask5z7yl1lx41t3.png" alt="Arize infographic showing the nine components of the Harness 1.0 architecture: outer iteration loop, context management, skills and tools, sub-agent management, built-in skills, session persistence, system prompt assembly, lifecycle hooks, and permission and safety" width="799" height="604"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://arize.com/what-is-a-AI-harness.pdf" rel="noopener noreferrer"&gt;Arize primer&lt;/a&gt; frames nine pieces as one system: loop, context, tools, system prompt assembly, permissions, hooks, persistence, built-in skills, and sub-agent management.&lt;/p&gt;

&lt;p&gt;That is what makes this more than prompt engineering. A prompt can ask the model to be careful. A harness can route the model through checks, permissions, and recovery paths that make care more likely.&lt;/p&gt;

&lt;p&gt;The best harnesses push judgment to the model and keep control in the system. The model decides which files to work on. The harness decides whether it is allowed to modify them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same model, better harness
&lt;/h2&gt;

&lt;p&gt;This is not just semantics.&lt;/p&gt;

&lt;p&gt;LangChain published a useful example in February 2026. They kept the model fixed and changed the harness around their coding agent. The score moved from &lt;code&gt;52.8&lt;/code&gt; to &lt;code&gt;66.5&lt;/code&gt; on Terminal Bench 2.0.&lt;/p&gt;

&lt;p&gt;A better harness improved the same model by roughly 14 points.&lt;/p&gt;

&lt;p&gt;The changes included better prompts, tool setup, verification middleware, trace analysis, and loop detection. That is the pattern teams should steal.&lt;/p&gt;

&lt;p&gt;Don't start by asking, "Which model will solve this?" Ask, "What does the model need around it to do this reliably?"&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple example
&lt;/h2&gt;

&lt;p&gt;Say you ask an agent to fix a failing test. Without much harness, the session often looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The model reads your prompt&lt;/li&gt;
&lt;li&gt;It scans a few files&lt;/li&gt;
&lt;li&gt;It changes some code&lt;/li&gt;
&lt;li&gt;It rereads its own code&lt;/li&gt;
&lt;li&gt;It says "done"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That sounds fine until you notice the missing step: it never ran the tests.&lt;/p&gt;

&lt;p&gt;With a basic harness, the flow changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The agent starts and loads the project instructions&lt;/li&gt;
&lt;li&gt;It sees the commands, conventions, and files that matter&lt;/li&gt;
&lt;li&gt;It changes the code&lt;/li&gt;
&lt;li&gt;The harness tells it to run the test command before exiting&lt;/li&gt;
&lt;li&gt;If tests fail, the agent keeps going&lt;/li&gt;
&lt;li&gt;When it finishes, it writes a short handoff note&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The minimum viable harness
&lt;/h2&gt;

&lt;p&gt;For most teams, a minimum viable harness has four layers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Small version&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Instructions&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CLAUDE.md&lt;/code&gt; or &lt;code&gt;AGENTS.md&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The agent knows the project before you re-explain it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tools&lt;/td&gt;
&lt;td&gt;One or two tools it actually needs&lt;/td&gt;
&lt;td&gt;The agent can act instead of only talk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verification&lt;/td&gt;
&lt;td&gt;Test, lint, type check, screenshot, review pass&lt;/td&gt;
&lt;td&gt;The agent has to prove the work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;Handoff note, index file, project memory&lt;/td&gt;
&lt;td&gt;The next session doesn't start cold&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is already a harness. Keep it simple - the point is reliability, not complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in an engineering org
&lt;/h2&gt;

&lt;p&gt;For an enterprise team, the harness becomes part of engineering operations. It's a shared operating agreement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A repo-level &lt;code&gt;AGENTS.md&lt;/code&gt; or &lt;code&gt;CLAUDE.md&lt;/code&gt; with architecture, commands, boundaries, and review expectations&lt;/li&gt;
&lt;li&gt;A small approved tool set: search, edits, shell, browser, test runner, docs lookup&lt;/li&gt;
&lt;li&gt;Permission modes for read-only work, workspace edits, and dangerous actions&lt;/li&gt;
&lt;li&gt;Hooks that block secrets, destructive commands, production credentials, or unreviewed deploy paths&lt;/li&gt;
&lt;li&gt;Verification gates that make the agent run the same checks a human engineer would run&lt;/li&gt;
&lt;li&gt;Session notes that explain what changed&lt;/li&gt;
&lt;li&gt;A small eval set of real tasks that shows whether the harness is getting better or just louder&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to write the instruction file
&lt;/h2&gt;

&lt;p&gt;The blunder with &lt;code&gt;CLAUDE.md&lt;/code&gt; is treating it like a wish list.&lt;/p&gt;

&lt;p&gt;"Be a senior engineer."&lt;/p&gt;

&lt;p&gt;"Think step by step."&lt;/p&gt;

&lt;p&gt;"Write clean code."&lt;/p&gt;

&lt;p&gt;Fine, but mostly wasted space. Use the file for things the agent would otherwise get wrong:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Critical commands&lt;/strong&gt;: build, test, lint, type check, run one file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture map&lt;/strong&gt;: where things live and what belongs where.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard rules&lt;/strong&gt;: the specific mistakes the agent must not make.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow preferences&lt;/strong&gt;: when to ask, act, change, and verify.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Out of scope&lt;/strong&gt;: files, systems, or integrations the agent should not touch.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keep it short.&lt;/p&gt;

&lt;p&gt;Anthropic's current memory docs emphasize hierarchy, imports, recursive lookup, and specific instructions over vague guidance. A &lt;code&gt;CLAUDE.md&lt;/code&gt; shapes behavior. It does not physically prevent bad actions.&lt;/p&gt;

&lt;p&gt;For enforcement, you need settings, permissions, tests, hooks, or human review. That is the difference between a guide and a guardrail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where tools fit
&lt;/h2&gt;

&lt;p&gt;Tools are the agent's hands.&lt;/p&gt;

&lt;p&gt;They let the model search files, run commands, query APIs, calculate, open a browser, read a spreadsheet, or edit a document.&lt;/p&gt;

&lt;p&gt;The common mistake is thinking more tools equals a smarter agent. Usually it means a more confused one. Tool-selection accuracy drops from around 43% to under 14% as the tool count grows, and every tool definition spends roughly 300-1,400 tokens whether it gets used or not.&lt;/p&gt;

&lt;p&gt;Start with the smallest tool set that can do the job. Rewriting probably needs no tools. Fixing a bug needs file access and a test command. Researching current prices needs web search or an API.&lt;/p&gt;

&lt;p&gt;One tool should have one clear job.&lt;/p&gt;

&lt;p&gt;Bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;manage_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;overwrite&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;read_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;write_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;delete_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tool design is part of the harness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where memory fits
&lt;/h2&gt;

&lt;p&gt;Memory is where people make this sound harder than it needs to be. There are two simple versions: what has been said in this session, and what should survive across sessions.&lt;/p&gt;

&lt;p&gt;Early on, longer-term memory can be boring markdown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what changed&lt;/li&gt;
&lt;li&gt;what is still broken&lt;/li&gt;
&lt;li&gt;what command to run next&lt;/li&gt;
&lt;li&gt;what the next session should not repeat&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That can live in a handoff note, an &lt;code&gt;INDEX.md&lt;/code&gt;, a session recap, or a project memory file. In my note system, the agent reads an index, topic map, and &lt;code&gt;CLAUDE.md&lt;/code&gt; before touching anything.&lt;/p&gt;

&lt;p&gt;Not glamorous. Useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflows before agents
&lt;/h2&gt;

&lt;p&gt;Not every problem needs a fully autonomous agent.&lt;/p&gt;

&lt;p&gt;Anthropic makes a useful distinction: workflows follow predefined code paths, while agents dynamically decide their process and tool use. If the steps are predictable, use a workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;write outline&lt;/li&gt;
&lt;li&gt;check outline&lt;/li&gt;
&lt;li&gt;write draft&lt;/li&gt;
&lt;li&gt;review draft&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the steps are not predictable, use an agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inspect this unfamiliar codebase&lt;/li&gt;
&lt;li&gt;figure out why the tests fail&lt;/li&gt;
&lt;li&gt;decide which files to change&lt;/li&gt;
&lt;li&gt;verify the fix&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with the simplest pattern that works. Most useful setups begin as workflows and only become agents when the model genuinely needs to choose the path.&lt;/p&gt;

&lt;h2&gt;
  
  
  When multiple agents make sense
&lt;/h2&gt;

&lt;p&gt;Start with one agent and a good harness. Multiple agents make sense when the roles are meaningfully different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one researches, one writes&lt;/li&gt;
&lt;li&gt;one implements, one reviews&lt;/li&gt;
&lt;li&gt;one can read sensitive data, one can execute actions&lt;/li&gt;
&lt;li&gt;one routes tasks, specialists handle narrow domains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the second agent doesn't have a different job, permission set, or evaluation role, you probably added complexity for vibes. The safer pattern is a supervisor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User -&amp;gt; Main agent -&amp;gt; Specialist agent only when needed -&amp;gt; Verification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is one multi-agent pattern worth knowing early: separate the builder from the judge.&lt;/p&gt;

&lt;p&gt;Anthropic Labs described this in their long-running application harness: planner, generator, evaluator. The generator built. The evaluator inspected the result with Playwright, graded it, and sent feedback into the next sprint.&lt;/p&gt;

&lt;p&gt;The principle scales down:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Small setup&lt;/th&gt;
&lt;th&gt;Bigger setup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Write code, then run tests&lt;/td&gt;
&lt;td&gt;Generator builds, evaluator tests in browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ask a second agent to review&lt;/td&gt;
&lt;td&gt;Dedicated evaluator grades each sprint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write a checklist before starting&lt;/td&gt;
&lt;td&gt;Planner and evaluator negotiate what "done" means&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The point is not "use three agents." The point is: don't let the same session that made the thing be the only judge.&lt;/p&gt;

&lt;h2&gt;
  
  
  What not to overbuild
&lt;/h2&gt;

&lt;p&gt;Don't turn your first harness into a platform. Start with one job, one agent, one clear instruction file, one or two tools, one verification step, and a few test prompts. Then watch where it fails. When the same failure repeats, tweak the harness, iterate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical build path
&lt;/h2&gt;

&lt;p&gt;If you want to build your first harness today:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write one sentence describing the agent's job.&lt;/li&gt;
&lt;li&gt;List the tools it truly needs (follow least privilege!)&lt;/li&gt;
&lt;li&gt;Write the rules it must follow.&lt;/li&gt;
&lt;li&gt;Define the output format.&lt;/li&gt;
&lt;li&gt;Add one verification step.&lt;/li&gt;
&lt;li&gt;Test it on five real examples.&lt;/li&gt;
&lt;li&gt;Add memory only when the next session needs something from this one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can ask an LLM to help:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;I want to build an AI agent.

Goal:
[what I want it to do]

Example user requests:
[5 messy examples]

Tools it may use:
[web search / files / calculator / custom API / none]

Rules it must follow:
[non-negotiables]

It must never:
[boundaries]

Please turn this into an agent spec, system prompt, minimal tool list, verification checklist, and 10 test cases.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The formula is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent = Role + Goal + Tools + Rules + Output format
Harness = Instructions + Tools + Verification + Memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is enough to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;A harness is the setup around an agent that makes its work more reliable.&lt;/p&gt;

&lt;p&gt;It isn't just an SDK or just a prompt. It isn't just &lt;code&gt;CLAUDE.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It is the operating environment: instructions, tools, tests, memory, permissions, hooks, recovery paths, and feedback loops.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The model gives you capability. The harness decides whether you can trust it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Some of the harness is durable: verification discipline, context preparation, running tests before calling something done. Some is scaffolding that dissolves as models improve. Know which half you're building on.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Typescript - several sub-projects + schemas (VTS)</title>
      <dc:creator>Stefan Werfling</dc:creator>
      <pubDate>Mon, 01 Jun 2026 23:54:36 +0000</pubDate>
      <link>https://dev.to/stefanwerfling/typescript-several-sub-projects-schemas-vts-2fa3</link>
      <guid>https://dev.to/stefanwerfling/typescript-several-sub-projects-schemas-vts-2fa3</guid>
      <description>&lt;h1&gt;
  
  
  TypeScript - Several Sub-projects
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Everyone has probably reached a point where a project is so large that it needs to be broken down into parts. In this article I want to show how I structure larger TypeScript projects as a &lt;strong&gt;monorepo&lt;/strong&gt; with several sub-projects that share the same data types and validation logic.&lt;/p&gt;

&lt;p&gt;The basic idea is simple: extract everything that is used in more than one place into its own package. The remaining parts of the application then depend on this shared package. With TypeScript's &lt;em&gt;project references&lt;/em&gt; and npm &lt;em&gt;workspaces&lt;/em&gt;, this can be done without a heavy build tool - plain &lt;code&gt;tsc&lt;/code&gt; is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Architecture
&lt;/h2&gt;

&lt;p&gt;The structure I keep coming back to looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;myproject/
├── package.json          # root: defines workspaces + dev-deps
├── tsconfig.json         # optional: root references
├── schemas/              # shared types, enums, validators
├── core/                 # plugin system, shared runtime code
├── backend/              # server-side application
└── frontend/             # client-side application (browser)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;schemas&lt;/strong&gt; - pure types, enums, interfaces and (in my case) &lt;a href="https://github.com/OpenSourcePKG/vts" rel="noopener noreferrer"&gt;&lt;code&gt;vts&lt;/code&gt;&lt;/a&gt; validators. No runtime logic, no I/O.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;core&lt;/strong&gt; - plugin contracts and runtime helpers that both backend and frontend might need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;backend&lt;/strong&gt; - the Node.js server. Depends on &lt;code&gt;schemas&lt;/code&gt; and &lt;code&gt;core&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;frontend&lt;/strong&gt; - the browser application. Depends on &lt;code&gt;schemas&lt;/code&gt; (and optionally &lt;code&gt;core&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All sub-projects share the same data shapes via &lt;code&gt;schemas&lt;/code&gt;, which means a single change to a DTO is picked up by every consumer the next time they are compiled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Main Project Setup
&lt;/h2&gt;

&lt;p&gt;I start with an empty directory and create the root &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;myproject
nano package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Root &lt;code&gt;package.json&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"myproject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MyProject is ..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"compile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cd ./schemas/ &amp;amp;&amp;amp; npm run compile &amp;amp;&amp;amp; cd ../core/ &amp;amp;&amp;amp; npm run compile &amp;amp;&amp;amp; cd ../backend/ &amp;amp;&amp;amp; npm run compile"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keywords"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Stefan Werfling"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MIT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"workspaces"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"schemas"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"core"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"backend"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"frontend"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"devDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@stylistic/eslint-plugin-ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@types/node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^22.15.18"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@typescript-eslint/eslint-plugin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^8.21.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@typescript-eslint/parser"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^8.21.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"eslint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^9.19.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"eslint-plugin-import"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^2.31.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"eslint-plugin-prefer-arrow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^1.2.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"npm-check-updates"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^18.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"typescript"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^5.7.3"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few notes on this file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;workspaces&lt;/code&gt; array tells npm to treat the four sub-folders as linked packages. After a single &lt;code&gt;npm install&lt;/code&gt; in the root, every sub-project can &lt;code&gt;import&lt;/code&gt; from every other sub-project as if it were published to a registry.&lt;/li&gt;
&lt;li&gt;The order in which the npm packages are installed is also very important, and we must compile everything in this order: &lt;strong&gt;schemas → core → backend&lt;/strong&gt; (the frontend has its own build pipeline, see below).&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;devDependencies&lt;/code&gt; are hoisted, so the same TypeScript / ESLint versions are used everywhere - no risk of "works on my machine".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates the root &lt;code&gt;node_modules/&lt;/code&gt; with symlinks for &lt;code&gt;myproject_schemas&lt;/code&gt;, &lt;code&gt;myproject_core&lt;/code&gt;, etc. once those packages exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schemas Sub-project
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;schemas&lt;/code&gt; package is the foundation. It contains only &lt;strong&gt;types, enums, interfaces and validators&lt;/strong&gt; - no business logic, no I/O. Anything that is needed by more than one sub-project lives here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Directory structure:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;schemas
&lt;span class="nb"&gt;cd &lt;/span&gt;schemas
&lt;span class="nb"&gt;mkdir &lt;/span&gt;src
&lt;span class="nb"&gt;mkdir &lt;/span&gt;dist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;schemas/package.json&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Stefan Werfling"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"vts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"git+https://github.com/OpenSourcePKG/vts.git"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"devDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@stylistic/eslint-plugin-ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@types/node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^22.15.18"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@typescript-eslint/eslint-plugin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^8.21.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@typescript-eslint/parser"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^8.21.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"eslint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^9.19.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"eslint-plugin-import"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^2.31.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"eslint-plugin-prefer-arrow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^1.2.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"typescript"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^5.7.3"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keywords"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"myproject_schemas"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run compile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"compile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsc --project tsconfig.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"npm-check-updates"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm-check-updates"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/index.d.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"import"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"require"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.d.ts"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;schemas/tsconfig.json&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"composite"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"declaration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tsBuildInfoFile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./../.cache/schemas.tsbuildinfo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"emitDecoratorMetadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"experimentalDecorators"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"importHelpers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NodeNext"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rootDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"removeComments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sourceMap"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES2022"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"src/**/*"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two important flags here are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;"composite": true&lt;/code&gt; - marks the package as a &lt;em&gt;referenceable&lt;/em&gt; TypeScript project. Other packages can now list it under &lt;code&gt;references&lt;/code&gt; in their own &lt;code&gt;tsconfig.json&lt;/code&gt; and benefit from incremental builds.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"declaration": true&lt;/code&gt; - emits the &lt;code&gt;.d.ts&lt;/code&gt; files that the consumers will pick up via the &lt;code&gt;types&lt;/code&gt; field of &lt;code&gt;package.json&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  A common pitfall: import paths
&lt;/h3&gt;

&lt;p&gt;If &lt;code&gt;outDir&lt;/code&gt;, &lt;code&gt;main&lt;/code&gt;, &lt;code&gt;types&lt;/code&gt; and &lt;code&gt;exports&lt;/code&gt; are not aligned, IDEs (especially WebStorm/IntelliJ) generate the wrong import path on auto-complete. You will see this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;BehaviouralStatesResponse&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;myproject_schemas/dist/index.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…where you actually want this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;BehaviouralStatesResponse&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;myproject_schemas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is to make sure all four locations point at the same file:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;field in &lt;code&gt;package.json&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;main&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dist/index.js&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;types&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dist/index.d.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exports["."].import&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;./dist/index.js&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exports["."].types&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;./dist/index.d.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;…and that &lt;code&gt;outDir&lt;/code&gt; in &lt;code&gt;tsconfig.json&lt;/code&gt; is &lt;code&gt;dist&lt;/code&gt;. Once those four match, auto-import resolves to the bare package name.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;schemas/src/index.ts&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Everything you want to share is re-exported from a single barrel file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./Behavioural/BehaviouralStatesResponse.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./User/User.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./User/UserRole.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;.js&lt;/code&gt; extension on the import path even though the source file is &lt;code&gt;.ts&lt;/code&gt; - this is required by &lt;code&gt;"module": "NodeNext"&lt;/code&gt; and saves you a lot of headache later when the package is consumed from an ESM context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Sub-project
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;core&lt;/code&gt; package sits between &lt;code&gt;schemas&lt;/code&gt; and the application sub-projects. I use it for things that are &lt;em&gt;not&lt;/em&gt; pure data but are still shared - plugin contracts, base classes, small utilities, error types, logging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;core/package.json&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"myproject_core"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Stefan Werfling"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/index.d.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"import"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"require"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.d.ts"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run compile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"compile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsc --project tsconfig.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"npm-check-updates"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm-check-updates"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"myproject_schemas"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"devDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"typescript"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^5.7.3"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;"myproject_schemas": "*"&lt;/code&gt; dependency works because of npm workspaces: npm symlinks the local &lt;code&gt;schemas/&lt;/code&gt; into &lt;code&gt;core/node_modules/myproject_schemas&lt;/code&gt;. No publishing, no &lt;code&gt;npm link&lt;/code&gt;, no rebuild gymnastics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;core/tsconfig.json&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"composite"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"declaration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tsBuildInfoFile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./../.cache/core.tsbuildinfo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NodeNext"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rootDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES2022"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"src/**/*"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"references"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"../schemas"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;references&lt;/code&gt; block is what makes this a true project-reference build. Running &lt;code&gt;tsc -b&lt;/code&gt; in the root walks the dependency graph and only rebuilds what actually changed.&lt;/p&gt;

&lt;p&gt;A typical core file then looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;UserRole&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;myproject_schemas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;UserService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;hasRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserRole&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The type comes from &lt;code&gt;schemas&lt;/code&gt;, the interface stays in &lt;code&gt;core&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backend Sub-project
&lt;/h2&gt;

&lt;p&gt;The backend is a plain Node.js application. The interesting part is that it can consume both &lt;code&gt;schemas&lt;/code&gt; and &lt;code&gt;core&lt;/code&gt; as if they were normal packages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;backend/package.json&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"myproject_backend"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Stefan Werfling"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run compile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"compile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsc --project tsconfig.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node dist/index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"npm-check-updates"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm-check-updates"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"myproject_schemas"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"myproject_core"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"express"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.21.0"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"devDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@types/express"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.17.21"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"typescript"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^5.7.3"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;backend/tsconfig.json&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"composite"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"declaration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tsBuildInfoFile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./../.cache/backend.tsbuildinfo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NodeNext"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rootDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES2022"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"src/**/*"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"references"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"../schemas"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"../core"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a route that uses the shared types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;BehaviouralStatesResponse&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;myproject_schemas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/behavioural/states&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BehaviouralStatesResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the response type is defined once in &lt;code&gt;schemas&lt;/code&gt;, the frontend can &lt;code&gt;import&lt;/code&gt; the exact same shape and you get end-to-end type safety without a code-generation step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frontend Sub-project
&lt;/h2&gt;

&lt;p&gt;The frontend is where the setup deviates a little, because the browser does not understand &lt;code&gt;NodeNext&lt;/code&gt; modules and you usually want a bundler in front of it. I use &lt;strong&gt;Vite&lt;/strong&gt; here, but the same approach works with webpack or esbuild.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;frontend/package.json&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"myproject_frontend"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Stefan Werfling"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vite build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"preview"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vite preview"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"myproject_schemas"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"devDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"typescript"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^5.7.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"vite"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^5.4.0"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;frontend/tsconfig.json&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ESNext"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moduleResolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bundler"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES2022"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lib"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ES2022"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DOM"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"jsx"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"preserve"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"isolatedModules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noEmit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"src/**/*"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"references"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"../schemas"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things differ from the Node sub-projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;"moduleResolution": "Bundler"&lt;/code&gt; - tells TypeScript that some external tool (Vite) will resolve the imports.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"noEmit": true&lt;/code&gt; - the bundler emits the final JavaScript, &lt;code&gt;tsc&lt;/code&gt; is only used for type-checking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A component then consumes the shared schema exactly like the backend does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;BehaviouralStatesResponse&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;myproject_schemas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadStates&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;BehaviouralStatesResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/behavioural/states&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you change a field in &lt;code&gt;schemas/src/Behavioural/BehaviouralStatesResponse.ts&lt;/code&gt;, both the Express handler and the &lt;code&gt;fetch&lt;/code&gt; call above turn red in the IDE on the next save. That single feedback loop is, for me, the main reason for going through the monorepo setup in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build Order and Project References
&lt;/h2&gt;

&lt;p&gt;There are two ways to build everything:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sequential&lt;/strong&gt; - the simple &lt;code&gt;compile&lt;/code&gt; script in the root &lt;code&gt;package.json&lt;/code&gt;. It just &lt;code&gt;cd&lt;/code&gt;s into each folder and runs &lt;code&gt;tsc&lt;/code&gt;. Works for small projects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project references&lt;/strong&gt; - run &lt;code&gt;tsc -b&lt;/code&gt; (build mode) in the root, with a root &lt;code&gt;tsconfig.json&lt;/code&gt; that references the sub-projects:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"references"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./schemas"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./core"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./backend"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tsc -b&lt;/code&gt; then figures out the topological order itself and only rebuilds what changed. The &lt;code&gt;.tsbuildinfo&lt;/code&gt; files cached under &lt;code&gt;.cache/&lt;/code&gt; make subsequent builds nearly instant.&lt;/p&gt;

&lt;p&gt;For CI I keep both: &lt;code&gt;npm run compile&lt;/code&gt; for "build everything from scratch", &lt;code&gt;tsc -b&lt;/code&gt; for the watch / incremental case during development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips &amp;amp; Pitfalls
&lt;/h2&gt;

&lt;p&gt;A short list of things that bit me along the way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always use the &lt;code&gt;.js&lt;/code&gt; extension in TypeScript imports&lt;/strong&gt; when &lt;code&gt;module&lt;/code&gt; is set to &lt;code&gt;NodeNext&lt;/code&gt;. The source is &lt;code&gt;.ts&lt;/code&gt;, the import says &lt;code&gt;.js&lt;/code&gt;. This is correct, even though it looks wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hoist &lt;code&gt;devDependencies&lt;/code&gt;&lt;/strong&gt; to the root &lt;code&gt;package.json&lt;/code&gt;. If every sub-project pins its own TypeScript version, &lt;code&gt;tsc -b&lt;/code&gt; will complain about "duplicate identifier" errors when the versions diverge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add &lt;code&gt;dist/&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt;&lt;/strong&gt; in every sub-project, but commit the &lt;code&gt;tsconfig.json&lt;/code&gt; and &lt;code&gt;package.json&lt;/code&gt;. The output is regeneratable, the build config is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep a single &lt;code&gt;.cache/&lt;/code&gt; folder&lt;/strong&gt; at the root for all &lt;code&gt;.tsbuildinfo&lt;/code&gt; files (as in the examples above). That way you can &lt;code&gt;rm -rf .cache&lt;/code&gt; to force a clean rebuild without touching &lt;code&gt;dist/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't put runtime code into &lt;code&gt;schemas&lt;/code&gt;&lt;/strong&gt;. The moment you do, the frontend bundle starts pulling in Node-only modules, and you spend an afternoon debugging Vite errors. Keep it types-only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin the workspace dependency to &lt;code&gt;*&lt;/code&gt; or &lt;code&gt;workspace:*&lt;/code&gt;&lt;/strong&gt;, never to a concrete version. With npm workspaces, &lt;code&gt;"myproject_schemas": "*"&lt;/code&gt; reliably resolves to the local symlink.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This setup gives you a clear separation between &lt;em&gt;data definitions&lt;/em&gt;, &lt;em&gt;shared runtime code&lt;/em&gt; and the actual &lt;em&gt;applications&lt;/em&gt;, without forcing you to publish anything to a registry or run a heavy build tool. The schemas package is the contract that keeps backend and frontend in sync, and TypeScript's project references make sure that contract is enforced on every save.&lt;/p&gt;

&lt;p&gt;If you want to see this approach in action on a real project, have a look at &lt;a href="https://github.com/StefanWerfling/vtseditor" rel="noopener noreferrer"&gt;vtseditor&lt;/a&gt; - a browser-based visual editor for exactly the kind of &lt;code&gt;schemas&lt;/code&gt; package described in this article. It lets you draw your schemas, exports them as &lt;code&gt;vts&lt;/code&gt;-based TypeScript files, and is published as an npm CLI (&lt;code&gt;npx vtseditor&lt;/code&gt;) so you can drop it into any monorepo without changing your existing build setup.&lt;/p&gt;

&lt;p&gt;Happy splitting.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>node</category>
      <category>architecture</category>
      <category>npm</category>
    </item>
    <item>
      <title>Process Isn't the Enemy</title>
      <dc:creator>Alessandro Aiezza</dc:creator>
      <pubDate>Mon, 01 Jun 2026 23:53:41 +0000</pubDate>
      <link>https://dev.to/aaiezza/process-isnt-the-enemy-425p</link>
      <guid>https://dev.to/aaiezza/process-isnt-the-enemy-425p</guid>
      <description>&lt;blockquote&gt;
&lt;h3&gt;
  
  
  🤖 AI Summary
&lt;/h3&gt;

&lt;p&gt;Short on time? Ask AI to summarize this article.&lt;/p&gt;

&lt;p&gt;Copy the prompt below into &lt;a href="https://chatgpt.com" rel="noopener noreferrer"&gt;ChatGPT&lt;/a&gt;, &lt;a href="https://claude.ai" rel="noopener noreferrer"&gt;Claude&lt;/a&gt;, &lt;a href="https://gemini.google.com" rel="noopener noreferrer"&gt;Gemini&lt;/a&gt;, or your preferred AI assistant.&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Please summarize this article for me. Focus on:

- The main argument
- Practical takeaways
- Engineering tradeoffs discussed
- Situations where the advice may or may not apply

Article: https://dev.to/aaiezza/process-isnt-the-enemy-425p
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;




&lt;p&gt;Software engineers tend to have a complicated relationship with process.&lt;/p&gt;

&lt;p&gt;We've all experienced process at its worst: endless meetings, excessive approvals, status updates that could have been a single message, and layers of ceremony that seem to slow down the very work they're supposed to support.&lt;/p&gt;

&lt;p&gt;When process becomes disconnected from outcomes, it feels like friction. It feels inefficient. It feels like something standing between an engineer and meaningful progress.&lt;/p&gt;

&lt;p&gt;And sometimes that's exactly what it is.&lt;/p&gt;

&lt;p&gt;But I've come to believe that the answer isn't &lt;em&gt;no process&lt;/em&gt;. The answer is &lt;em&gt;intentional process&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The best processes aren't there to slow us down. They're there to keep us pointed in the right direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conventional Commits as a Case Study
&lt;/h2&gt;

&lt;p&gt;One example I appreciate is the idea of Conventional Commits.&lt;/p&gt;

&lt;p&gt;The specification can be found here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.conventionalcommits.org/" rel="noopener noreferrer"&gt;https://www.conventionalcommits.org/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At a high level, Conventional Commits provide a standardized structure for Git commit messages.&lt;/p&gt;

&lt;p&gt;Some common examples include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feat: add customer search endpoint
fix: correct null pointer exception during login
refactor: simplify order validation workflow
test: add coverage for customer creation service
docs: update API documentation
chore: upgrade build dependencies
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conventional Commits transform a commit message from a description into a declaration of intent.&lt;/p&gt;

&lt;p&gt;Once intent is captured consistently, all sorts of useful things become possible. Changelogs can be generated automatically. Release tooling becomes easier to automate. Teams can analyze repository history to better understand the types of work being performed over time. How frequently are features being shipped? How much effort is spent on refactoring? Are bug-fix rates increasing or decreasing? Meaningful trends can emerge when commit intent is captured consistently.&lt;/p&gt;

&lt;p&gt;Those are valuable benefits.&lt;/p&gt;

&lt;p&gt;But they're not the reason I've grown to appreciate the convention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The real value is what happens in the engineer's mind before the commit is ever created.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pause That Creates Clarity
&lt;/h2&gt;

&lt;p&gt;When you're forced to classify a commit, you're forced to think about what you're actually shipping.&lt;/p&gt;

&lt;p&gt;Is this really a feature?&lt;/p&gt;

&lt;p&gt;If it's a feature, why am I also including a &lt;a href="https://medium.com/@andy.tarpley/software-engineering-pineapples-6a53c8e813ea" rel="noopener noreferrer"&gt;large amount of cleanup code&lt;/a&gt;?&lt;/p&gt;

&lt;p&gt;Should that cleanup be a separate &lt;code&gt;refactor&lt;/code&gt; commit?&lt;/p&gt;

&lt;p&gt;If I'm changing formatting throughout the project, is that actually part of the feature, or should it be a separate commit?&lt;/p&gt;

&lt;p&gt;If I'm updating dependencies while implementing business logic, should those concerns be separated?&lt;/p&gt;

&lt;p&gt;These questions seem small, but they matter.&lt;/p&gt;

&lt;p&gt;The process introduces a deliberate &lt;strong&gt;pause&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A moment where the engineer stops moving code around and starts thinking about intent.&lt;/p&gt;

&lt;p&gt;And &lt;strong&gt;intent is one of the most valuable things we can preserve in a codebase&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Commit Purity
&lt;/h2&gt;

&lt;p&gt;One of the healthiest habits a team can develop is pursuing what I like to call &lt;em&gt;commit purity&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;A commit should have a single reason to exist.&lt;/p&gt;

&lt;p&gt;A future engineer should be able to look at a commit and answer a simple question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why was this change made?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When multiple concerns become tangled together, that answer becomes difficult.&lt;/p&gt;

&lt;p&gt;A feature commit that also contains refactoring, formatting changes, dependency updates, test rewrites, and miscellaneous cleanup becomes harder to review, harder to understand, harder to revert, and harder to trust.&lt;/p&gt;

&lt;p&gt;Small, focused commits create a cleaner narrative.&lt;/p&gt;

&lt;p&gt;They allow reviewers to focus on one concern at a time.&lt;/p&gt;

&lt;p&gt;They make deployments safer.&lt;/p&gt;

&lt;p&gt;They make debugging easier.&lt;/p&gt;

&lt;p&gt;They make history more useful.&lt;/p&gt;

&lt;p&gt;Most importantly, they encourage engineers to think in terms of discrete, intentional changes instead of giant collections of loosely related modifications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small Bullets, Not Missiles
&lt;/h2&gt;

&lt;p&gt;At first, this can feel slower.&lt;/p&gt;

&lt;p&gt;Stopping to think about commit boundaries.&lt;/p&gt;

&lt;p&gt;Splitting work into multiple commits.&lt;/p&gt;

&lt;p&gt;Writing meaningful messages.&lt;/p&gt;

&lt;p&gt;Separating unrelated changes.&lt;/p&gt;

&lt;p&gt;All of it feels like lost momentum.&lt;/p&gt;

&lt;p&gt;But over time, something interesting happens.&lt;/p&gt;

&lt;p&gt;You stop shipping giant missiles.&lt;/p&gt;

&lt;p&gt;You start firing tiny bullets.&lt;/p&gt;

&lt;p&gt;Each one is small.&lt;/p&gt;

&lt;p&gt;Each one is intentional.&lt;/p&gt;

&lt;p&gt;Each one is easier to test.&lt;/p&gt;

&lt;p&gt;Each one is easier to review.&lt;/p&gt;

&lt;p&gt;Each one is easier to deploy.&lt;/p&gt;

&lt;p&gt;And when something goes wrong, each one is easier to understand.&lt;/p&gt;

&lt;p&gt;The apparent slowdown often results in faster delivery because the system itself stays healthier.&lt;/p&gt;

&lt;p&gt;Reviews become more effective.&lt;/p&gt;

&lt;p&gt;Deployments become less risky.&lt;/p&gt;

&lt;p&gt;Debugging becomes less painful.&lt;/p&gt;

&lt;p&gt;The codebase becomes easier to navigate.&lt;/p&gt;

&lt;p&gt;And the team spends less time untangling yesterday's decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Good Process Creates Better Habits
&lt;/h2&gt;

&lt;p&gt;The best engineering processes are rarely about compliance.&lt;/p&gt;

&lt;p&gt;They're about creating opportunities for reflection.&lt;/p&gt;

&lt;p&gt;A code review checklist encourages us to think about edge cases.&lt;/p&gt;

&lt;p&gt;A testing strategy encourages us to think about correctness.&lt;/p&gt;

&lt;p&gt;A deployment process encourages us to think about risk.&lt;/p&gt;

&lt;p&gt;And conventions like Conventional Commits encourage us to think about intent.&lt;/p&gt;

&lt;p&gt;None of these activities directly produce features.&lt;/p&gt;

&lt;p&gt;But they help ensure we're building the right things, shipping them safely, and leaving the codebase healthier than we found it.&lt;/p&gt;

&lt;p&gt;That's not bureaucracy.&lt;/p&gt;

&lt;p&gt;That's discipline.&lt;/p&gt;

&lt;p&gt;And sometimes a little discipline is exactly what helps us move faster.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>git</category>
      <category>productivity</category>
      <category>career</category>
    </item>
    <item>
      <title>When Every Tool Is Callable, the Chat Becomes the Join Layer</title>
      <dc:creator>Lovanaut </dc:creator>
      <pubDate>Mon, 01 Jun 2026 23:51:38 +0000</pubDate>
      <link>https://dev.to/lovanaut55/when-every-tool-is-callable-the-chat-becomes-the-join-layer-1jl0</link>
      <guid>https://dev.to/lovanaut55/when-every-tool-is-callable-the-chat-becomes-the-join-layer-1jl0</guid>
      <description>&lt;p&gt;Every software stack has a hidden layer that no vendor sells: the join. It is the place where two products that do not know about each other are made to work together — where one tool's numbers are reconciled against another's outcomes. For two decades that layer lived outside every product, in a human reconciling exports or in a pipeline someone built and maintained. It belonged to no vendor, and it was nobody's screen.&lt;/p&gt;

&lt;p&gt;That layer is moving. Not because a vendor built a grand integration, but because tools are becoming callable. When a product exposes its functions over MCP, it stops being a screen you visit and becomes something a client can invoke. And the moment a client can invoke more than one product at once, it can do the thing the join layer always did — pull from one, pull from another, and reconcile them on the fly. The integration point does not get built into the products. It rises one level, out of every product and dashboard, into the chat client where the user already is.&lt;/p&gt;

&lt;p&gt;This is a change in topology, not a new feature. The shape of where integration lives is shifting, and the consequence runs straight through vendor strategy: a tool engineered to be a complete, self-contained island — the most polished screen, the fullest dashboard — is optimizing for a topology that is dissolving underneath it. To see the shift concretely, take the most familiar stack of separate-vendor tools there is: acquisition. Ads, forms, analytics — separate products, separate logins, separate mental models, with the most decisive work, lining up what the ad cost against what the form produced, left as a manual chore in the gap between them. Watch what happens to that chore when each tool becomes callable inside the same conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the join used to live
&lt;/h2&gt;

&lt;p&gt;Start with a question most product people never ask out loud: when two tools need to work together, where does that work physically happen?&lt;/p&gt;

&lt;p&gt;For two decades the answer was "in a human, or in glue someone wrote." If an ad platform and a form tool both held numbers that needed comparing, a person was the integration — opening both, exporting both, reconciling both. A more sophisticated team wired up a pipeline: an ETL job, a data warehouse, a dashboard that pulled from each API and joined them downstream. Either way, the join was a structure that had to be built and maintained, and it sat outside both products, in territory that belonged to neither.&lt;/p&gt;

&lt;p&gt;This is the heart of workflow topology — the shape of where integration lives. For a long time that shape was fixed. Each product owned its own screen and its own data, and any connection between products was external scaffolding — a person, a script, a warehouse — bolted across the gap. Vendors competed on their own surfaces. The seams between them were the customer's problem.&lt;/p&gt;

&lt;p&gt;That topology is what is changing. Not the products. The location of the seams.&lt;/p&gt;

&lt;h2&gt;
  
  
  When every tool is callable, the client becomes the join
&lt;/h2&gt;

&lt;p&gt;Here is the mechanism, stated plainly.&lt;/p&gt;

&lt;p&gt;When a tool exposes its functions over MCP — the open protocol that lets an AI client reach an outside service safely — it stops being a screen you go to and becomes a thing that can be called. That alone is now widely understood. What is less discussed is what happens when &lt;em&gt;several&lt;/em&gt; tools become callable at once, inside the same chat client.&lt;/p&gt;

&lt;p&gt;The client gains the ability to call all of them. And the moment it can call more than one, it can do the thing that used to be your chore: it can pull from one, pull from another, and join them. The orchestration that used to live in a human or a pipeline relocates into the conversation. The chat client becomes the cross-vendor join layer — the place where the ad platform's numbers and the form tool's outcomes finally meet, without either vendor having built a bridge to the other.&lt;/p&gt;

&lt;p&gt;This is the part that surprises people. No vendor integrated with any other vendor. The ad tool does not know the form tool exists. The form tool does not know about the ad tool. Neither one built a connector. And yet they join — because the layer above them, the client, can reach both and reconcile them on the fly. The integration didn't get built into the products. It moved out of the products entirely, up into the conversation where the user already is.&lt;/p&gt;

&lt;p&gt;That is a topology change, not a feature. The seams left the gaps between products and gathered into one surface. And the surface is a chat window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Eight days where the join moved into the chat
&lt;/h2&gt;

&lt;p&gt;A topology claim is easy to wave at and harder to prove, so here is a concrete instance of the join relocating.&lt;/p&gt;

&lt;p&gt;The setup was a small ad test over eight days, with about ¥6,597 of spend, and then an attempt to run the whole acquisition loop from a single conversation in an AI client without opening a dashboard. Two connectors were live in that chat at once: one for the ads, and FORMLOVA's, for the form side. Neither vendor had built a bridge to the other.&lt;/p&gt;

&lt;p&gt;First the ad side. The metrics came back into the chat from a plain request — spend, clicks, the rate of clicks. Across those eight days there were 704 clicks at a CTR of 12.62%. On the ad screen, that reads as a clean result. A high click rate, cheap clicks, money apparently well spent.&lt;/p&gt;

&lt;p&gt;Then the move that the old topology never let me make in one place. In the same conversation, I turned to the form side and asked it to break the responses down by which ad they came from. Ad spend on one side, form outcomes on the other, and the join performed by the client between them — right there, no export, no spreadsheet, no second screen. From that join, acquisition cost is simply spend divided by the form-side conversions, derived inside the chat. The number is not the point of this essay; the &lt;em&gt;location&lt;/em&gt; of the calculation is. It happened in the conversation, not in my head.&lt;/p&gt;

&lt;p&gt;And the join is what revealed the truth the ad screen had hidden. Splitting delivery by placement showed the skew immediately: roughly 96% of impressions and about 83% of spend had concentrated in Audience Network, not the feeds I had meant to reach. The cheap, high-CTR clicks were largely a mirage. They looked efficient on the surface and had mostly flowed somewhere I never intended. One side alone would never have told me that. The ad numbers said "good clicks." Only the join, in the conversation, said "good clicks, wrong place."&lt;/p&gt;

&lt;p&gt;Because the problem surfaced inside the same chat, the next move happened there too. I rebuilt the ad set — restricting placement to the feeds, excluding Audience Network — through conversational instructions, and the change was reversible. Reading the data and acting on it were no longer two screens and two tools. They were two sentences in one thread.&lt;/p&gt;

&lt;p&gt;What I want to underline is not the fix. It is that at no point did I reopen a dashboard to perform the join. The seam between "what the ad cost" and "what the form produced" — the seam that used to be my manual chore — had quietly moved into the conversation and dissolved there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is topology, and why it matters more than it looks
&lt;/h2&gt;

&lt;p&gt;It is tempting to file this under "AI makes things convenient." That undersells it. Convenience is a better screen. This is a different map.&lt;/p&gt;

&lt;p&gt;When the join moves into the client, the competitive picture for every tool in the stack changes shape. A product no longer wins by owning the most complete screen, because the user is not assembling the picture on any one screen anymore. The picture is assembled above all of them. What a product can offer, instead, is to be cleanly callable and to return data that joins well with everything else in the conversation — accurate, structured, attributable. The value of a tool starts to depend on how good a &lt;em&gt;part&lt;/em&gt; it is inside a join it does not control, rather than how complete a &lt;em&gt;whole&lt;/em&gt; it is on its own.&lt;/p&gt;

&lt;p&gt;This is adjacent to the question of where control sits in an agent world, which is its own argument, and I will leave it as a single passing note rather than re-run it here. The topology point stands on its own: the integration layer of the acquisition stack used to be external scaffolding you maintained, and it is becoming a property of the chat client you already use. The vendors did not converge. The join migrated.&lt;/p&gt;

&lt;p&gt;For founders building any tool in an acquisition flow, the strategic read is concrete. Stop assuming the integration work happens in your product or in the customer's pipeline. Increasingly it happens one layer up, in a conversation, performed by a client calling you and calling your neighbors. The questions that follow are not cosmetic. Is your tool callable with intent read correctly? Does your data join cleanly against a tool you have never heard of, in a thread you will never see? Are your operations safe to run when the caller is an agent and the join is happening somewhere above you? A tool that is a beautiful island, fully complete on its own screen, is optimizing for a topology that is dissolving. A tool that is a good citizen of a join it does not own is built for the one that is arriving.&lt;/p&gt;

&lt;p&gt;This sits inside a larger movement I have written about elsewhere — software shifting from being chosen to being called. The collapse of the acquisition stack into one conversation is what that shift looks like when you stand in the middle of an actual workflow, watching the seams between your tools vanish in real time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole loop in one thread
&lt;/h2&gt;

&lt;p&gt;What eight days of small spend made clear to me was not about ads. It was about shape.&lt;/p&gt;

&lt;p&gt;The acquisition stack — build the form, build the ad, manage the form, manage the ad, analyze the result — was always carved into separate products on separate screens, with the most valuable work, the join between cost and outcome, left as a chore in the gaps. That chore is collapsing into a conversation. Pull the ad numbers, pull the form outcomes, join them, find the mirage, rebuild the ad — all of it in one thread, with no vendor having built a bridge and no dashboard reopened.&lt;/p&gt;

&lt;p&gt;The tools did not merge. The place where they meet did. And once the join lives in the conversation, the seams that defined the old stack simply stop being visible. That is the quiet, structural thing worth noticing: not that the work got easier, but that the integration layer of acquisition moved out of your scripts and your screens and into the chat where you were already working.&lt;/p&gt;




&lt;h2&gt;
  
  
  Read more
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The eight-day field report this essay is grounded in: &lt;a href="https://formlova.com/en/blog/meta-ads-mcp-formlova-verification-en" rel="noopener noreferrer"&gt;Reconciling ad spend with real form outcomes in one chat&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The broader guide to FORMLOVA as an MCP-ready form service: &lt;a href="https://formlova.com/en/blog/mcp-form-service-guide-en" rel="noopener noreferrer"&gt;The FORMLOVA MCP form service, in brief&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>architecture</category>
      <category>mcp</category>
    </item>
  </channel>
</rss>
