<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sat, 05 Sep 2026 17:41:49 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Scholarship Research MCP Server with Node.js, Express, and MongoDB ]]>
                </title>
                <description>
                    <![CDATA[ Scholarship hunting is a research job, not a single search box. You filter awards by field, GPA, citizenship, and deadline. You keep a shortlist. You write notes about essays and recommenders. Then yo ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-scholarship-research-mcp-server-with-node-js-express-and-mongodb/</link>
                <guid isPermaLink="false">6a9b2db334fe985abf2f91a9</guid>
                
                    <category>
                        <![CDATA[ mcp ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Model Context Protocol ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Express.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ MongoDB ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chinedu Otutu ]]>
                </dc:creator>
                <pubDate>Fri, 04 Sep 2026 20:44:35 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/bc3b99da-15f4-4366-aede-4a2b567724aa.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Scholarship hunting is a research job, not a single search box. You filter awards by field, GPA, citizenship, and deadline. You keep a shortlist. You write notes about essays and recommenders. Then you come back a week later and try to remember why you saved a particular program.</p>
<p>An AI assistant can help with that workflow, but only if it can query a real catalog and persist what you already decided. Chat history isn't a database. A hallucinated deadline is worse than no deadline at all.</p>
<p>The <a href="https://modelcontextprotocol.io/docs/learn/architecture">Model Context Protocol (MCP)</a> is the standard way to give AI apps that kind of access. In this tutorial, you'll build a scholarship research MCP server with Node.js, Express, and MongoDB.</p>
<p>When you finish, Cursor, Claude Desktop, or any other MCP host will be able to search awards, match them to a student profile, bookmark a shortlist, and store research notes. The model stays the reasoning layer while your server owns the data.</p>
<p>You'll build:</p>
<ul>
<li><p>A MongoDB catalog of scholarships, plus saved-list and notes collections</p>
</li>
<li><p>Nine MCP tools for search, matching, deadlines, comparison, and research tracking</p>
</li>
<li><p>Resources so a client can read the catalog without calling a tool</p>
</li>
<li><p>Prompts that turn a student profile into a research plan</p>
</li>
<li><p>An Express app that serves MCP over Streamable HTTP</p>
</li>
</ul>
<p>The sample catalog in this project is a teaching dataset. Amounts, dates, and eligibility rules are simplified. Always confirm details on the official application page before applying.</p>
<h2 id="heading-what-you-need">What You Need</h2>
<p>You should be comfortable with JavaScript and basic Express routing. You don't need prior MCP experience.</p>
<p>Install:</p>
<ul>
<li><p><a href="https://nodejs.org/">Node.js 20</a> or later</p>
</li>
<li><p><a href="https://www.mongodb.com/docs/manual/installation/">MongoDB</a> running locally, or a free <a href="https://www.mongodb.com/atlas">MongoDB Atlas</a> cluster</p>
</li>
<li><p>An MCP client if you want to try the last section. <a href="https://cursor.com/">Cursor</a> and <a href="https://claude.ai/download">Claude Desktop</a> both work.</p>
</li>
</ul>
<p>Docker is enough for MongoDB:</p>
<pre><code class="language-bash">docker run -d --name mongo -p 27017:27017 mongo:7
</code></pre>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-you-need">What You Need</a></p>
</li>
<li><p><a href="#heading-what-is-the-model-context-protocol">What Is the Model Context Protocol?</a></p>
</li>
<li><p><a href="#heading-why-a-scholarship-research-server">Why a Scholarship Research Server?</a></p>
</li>
<li><p><a href="#heading-how-the-architecture-fits-together">How the Architecture Fits Together</a></p>
</li>
<li><p><a href="#heading-how-to-set-up-the-project">How to Set Up the Project</a></p>
</li>
<li><p><a href="#heading-how-to-connect-mongodb">How to Connect MongoDB</a></p>
</li>
<li><p><a href="#heading-how-to-model-scholarship-data">How to Model Scholarship Data</a></p>
</li>
<li><p><a href="#heading-how-to-write-the-scholarship-service">How to Write the Scholarship Service</a></p>
</li>
<li><p><a href="#heading-how-to-register-mcp-tools">How to Register MCP Tools</a></p>
</li>
<li><p><a href="#heading-how-to-expose-resources-and-prompts">How to Expose Resources and Prompts</a></p>
</li>
<li><p><a href="#heading-how-to-serve-mcp-over-express">How to Serve MCP Over Express</a></p>
</li>
<li><p><a href="#heading-how-to-seed-the-catalog">How to Seed the Catalog</a></p>
</li>
<li><p><a href="#heading-how-to-test-the-server">How to Test the Server</a></p>
</li>
<li><p><a href="#heading-how-to-connect-cursor-and-claude-desktop">How to Connect Cursor and Claude Desktop</a></p>
</li>
<li><p><a href="#heading-how-a-research-session-runs">How a Research Session Runs</a></p>
</li>
<li><p><a href="#heading-how-the-matching-logic-works">How the Matching Logic Works</a></p>
</li>
<li><p><a href="#heading-what-you-can-build-next">What You Can Build Next</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-the-model-context-protocol">What Is the Model Context Protocol?</h2>
<p>MCP is an open protocol that lets an AI application talk to external tools and data sources through a shared contract. Anthropic introduced it in 2024, and it's now maintained as an open standard.</p>
<p>Anthropic described MCP as "a USB-C port for AI applications": one connector, many hosts. (Source: <a href="https://www.anthropic.com/news/model-context-protocol">Introducing the Model Context Protocol</a>) Instead of writing one integration for Cursor, another for Claude Desktop, and a third for a custom agent, you implement the protocol once.</p>
<p>The <a href="https://modelcontextprotocol.io/docs/learn/architecture">official architecture overview</a> splits MCP into two layers:</p>
<ul>
<li><p>The <strong>data layer</strong> is JSON-RPC 2.0. Clients and servers exchange requests such as <code>tools/list</code> and <code>tools/call</code>.</p>
</li>
<li><p>The <strong>transport layer</strong> moves those messages. Local servers usually use stdio. Remote or long-running servers use Streamable HTTP.</p>
</li>
</ul>
<p>This tutorial uses Streamable HTTP, because Express is already an HTTP server and you want the catalog available to any client on your machine.</p>
<h3 id="heading-hosts-clients-and-servers">Hosts, Clients, and Servers</h3>
<p>Three roles show up in every MCP setup:</p>
<ul>
<li><p>The <strong>host</strong> is the AI app. Cursor and Claude Desktop are hosts.</p>
</li>
<li><p>The <strong>client</strong> lives inside the host. The host creates one client per connected server.</p>
</li>
<li><p>The <strong>server</strong> is your program. It advertises tools, resources, and prompts, then handles calls.</p>
</li>
</ul>
<p>Your scholarship app is the server. You never talk to the model SDK directly. The host does that.</p>
<h3 id="heading-tools-resources-and-prompts">Tools, Resources, and Prompts</h3>
<p>MCP servers expose three primitives. You'll use all three.</p>
<p><strong>Tools</strong> are actions. The model decides to call them, the way it might call a function in a regular tool-calling API. Search, save, and compare belong here.</p>
<p><strong>Resources</strong> are data the host can read and attach as context. A catalog URI and a per-scholarship URI belong here. The model doesn't have to "take an action" to see them.</p>
<p><strong>Prompts</strong> are named templates. People usually invoke them from a slash command or a menu. A "research plan" prompt belongs here, because it's a workflow you want to start on purpose.</p>
<p>That split matters. If you put everything in tools, the model has to guess when to look things up. Resources and prompts give the host better knobs.</p>
<h2 id="heading-why-a-scholarship-research-server">Why a Scholarship Research Server?</h2>
<p>A weather MCP demo is a single API call. Scholarship research is closer to a real product:</p>
<ul>
<li><p>The catalog must be queryable. Keyword search, GPA filters, and deadline windows all live in the database.</p>
</li>
<li><p>The workflow must persist. A saved shortlist and research notes should survive a new chat.</p>
</li>
<li><p>Eligibility is logic, not prose. A GPA minimum is a number. First-generation-only is a boolean. Put those checks in code so the model can't invent a match.</p>
</li>
<li><p>The output must be inspectable. Students should be able to open the official URL and verify every claim.</p>
</li>
</ul>
<p>MongoDB fits this well. Each scholarship is a document with nested arrays for fields of study, citizenship, and requirements. Saved items and notes are separate collections with references back to the catalog.</p>
<p>You could wrap a public API instead of storing documents. That's a good follow-up. Starting with your own catalog keeps the tutorial self-contained and makes the MCP contract obvious.</p>
<h2 id="heading-how-the-architecture-fits-together">How the Architecture Fits Together</h2>
<p>The finished project looks like this:</p>
<pre><code class="language-text">MCP host (Cursor or Claude Desktop)
        |
        |  Streamable HTTP  POST /mcp
        v
Express app  (createMcpExpressApp)
        |
        |  createMcpHandler factory
        v
McpServer  tools / resources / prompts
        |
        v
Scholarship service
        |
        v
MongoDB  scholarships, savedScholarships, researchnotes
</code></pre>
<p>A few design choices are worth calling out before you write code.</p>
<p>The MCP handler is <strong>stateless</strong>. The SDK runs your server factory once per HTTP request. That's the recommended v2 pattern for Streamable HTTP. Don't keep tool state on the <code>McpServer</code> instance. Keep it in MongoDB.</p>
<p>The database connection is <strong>process-wide</strong>. Connecting on every request would be slow and pointless. You connect once at startup and close over that pool from the tools.</p>
<p>The HTTP surface is small on purpose. <code>/health</code> is for you. <code>/mcp</code> is for the protocol. You don't need a REST API in front of the same data unless you want one later.</p>
<h2 id="heading-how-to-set-up-the-project">How to Set Up the Project</h2>
<p>Create a folder and initialize a Node.js project. ESM is required because the MCP SDK is ESM-first.</p>
<pre><code class="language-bash">mkdir scholarship-research-mcp-server
cd scholarship-research-mcp-server
npm init -y
</code></pre>
<p>Open <code>package.json</code> and set <code>"type": "module"</code>. Then install the SDK, Express, Mongoose, Zod, and dotenv:</p>
<pre><code class="language-bash">npm install @modelcontextprotocol/server @modelcontextprotocol/express @modelcontextprotocol/node express mongoose dotenv zod
</code></pre>
<p>The SDK split into packages in v2:</p>
<ul>
<li><p><code>@modelcontextprotocol/server</code> is the <code>McpServer</code> class and <code>createMcpHandler</code></p>
</li>
<li><p><code>@modelcontextprotocol/express</code> gives you <code>createMcpExpressApp</code>, including DNS rebinding protection</p>
</li>
<li><p><code>@modelcontextprotocol/node</code> adapts the web-standard handler to Node's <code>req</code>/<code>res</code></p>
</li>
</ul>
<p>Create a <code>.env</code> file:</p>
<pre><code class="language-bash">MONGODB_URI=mongodb://127.0.0.1:27017/scholarship_research
PORT=3000
HOST=127.0.0.1
</code></pre>
<p>Add a <code>.gitignore</code> that excludes <code>node_modules</code> and <code>.env</code>.</p>
<p>Your source layout can stay small:</p>
<pre><code class="language-text">src/
  index.js
  config.js
  db.js
  models/
  services/
  mcp/
  utils/
data/
  scholarships.json
scripts/
  seed.js
  smoke-test.js
</code></pre>
<p><code>src/config.js</code> reads environment variables with defaults:</p>
<pre><code class="language-javascript">export const config = {
  mongodbUri: process.env.MONGODB_URI ?? "mongodb://127.0.0.1:27017/scholarship_research",
  port: Number(process.env.PORT ?? 3000),
  host: process.env.HOST ?? "127.0.0.1",
};
</code></pre>
<p>Keep configuration in one file. Tools shouldn't read <code>process.env</code> directly.</p>
<h2 id="heading-how-to-connect-mongodb">How to Connect MongoDB</h2>
<p>Mongoose 9 works cleanly with ESM. A short <code>src/db.js</code> is enough:</p>
<pre><code class="language-javascript">import mongoose from "mongoose";
import { config } from "./config.js";

export async function connectDatabase() {
  mongoose.set("strictQuery", true);
  await mongoose.connect(config.mongodbUri);
  return mongoose.connection;
}
</code></pre>
<p>Call this once in <code>src/index.js</code> before <code>app.listen</code>. If the connection fails, the process should exit. A running Express server with a dead database is harder to debug than a failed startup.</p>
<h2 id="heading-how-to-model-scholarship-data">How to Model Scholarship Data</h2>
<p>You need three collections.</p>
<h3 id="heading-scholarship">Scholarship</h3>
<p>This is the catalog. Store the fields a matching engine actually uses, not a blob of markdown.</p>
<pre><code class="language-javascript">import mongoose from "mongoose";

const scholarshipSchema = new mongoose.Schema(
  {
    title: { type: String, required: true, trim: true },
    provider: { type: String, required: true, trim: true },
    description: { type: String, required: true },
    amountMin: { type: Number, default: 0 },
    amountMax: { type: Number, default: 0 },
    currency: { type: String, default: "USD" },
    deadline: { type: Date, default: null },
    rolling: { type: Boolean, default: false },
    educationLevels: { type: [String], default: ["undergraduate"] },
    fieldsOfStudy: { type: [String], default: ["any"] },
    gpaMinimum: { type: Number, default: null },
    citizenship: { type: [String], default: ["any"] },
    countries: { type: [String], default: ["any"] },
    firstGenerationOnly: { type: Boolean, default: false },
    womenOnly: { type: Boolean, default: false },
    numberOfAwards: { type: Number, default: 1 },
    renewable: { type: Boolean, default: false },
    applicationUrl: { type: String, required: true },
    applicationRequirements: { type: [String], default: [] },
  },
  { timestamps: true },
);

scholarshipSchema.index({
  title: "text",
  provider: "text",
  description: "text",
  fieldsOfStudy: "text",
});
scholarshipSchema.index({ deadline: 1 });
scholarshipSchema.index({ amountMax: -1 });

export const Scholarship = mongoose.model("Scholarship", scholarshipSchema);
</code></pre>
<p>A few field choices are doing real work:</p>
<ul>
<li><p><code>fieldsOfStudy: ["any"]</code> means the award is field-open. The matcher treats <code>any</code> as a wildcard.</p>
</li>
<li><p><code>deadline: null</code> plus <code>rolling: true</code> covers programs that accept applications year-round.</p>
</li>
<li><p><code>applicationUrl</code> is mandatory. Every tool result should point back to a human-verifiable source.</p>
</li>
<li><p><code>gpaMinimum: null</code> means the provider didn't publish a cutoff. That's different from <code>0</code>.</p>
</li>
</ul>
<h3 id="heading-saved-scholarship">Saved Scholarship</h3>
<p>A research list is a join between a person and a catalog row.</p>
<pre><code class="language-javascript">const savedScholarshipSchema = new mongoose.Schema(
  {
    researcherId: { type: String, required: true, default: "default" },
    scholarship: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "Scholarship",
      required: true,
    },
    status: {
      type: String,
      enum: ["saved", "applying", "submitted", "won", "rejected"],
      default: "saved",
    },
  },
  { timestamps: true },
);

savedScholarshipSchema.index({ researcherId: 1, scholarship: 1 }, { unique: true });
</code></pre>
<p>The unique index makes <code>save_scholarship</code> idempotent. Saving the same award twice updates the status instead of creating duplicates.</p>
<p><code>researcherId</code> is a plain string. For a tutorial, that's enough. In production you would take it from an auth token.</p>
<h3 id="heading-research-note">Research Note</h3>
<p>Notes are a separate collection so one scholarship can have many of them.</p>
<pre><code class="language-javascript">const researchNoteSchema = new mongoose.Schema(
  {
    researcherId: { type: String, required: true, default: "default" },
    scholarship: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "Scholarship",
      required: true,
    },
    body: { type: String, required: true, trim: true },
  },
  { timestamps: true },
);
</code></pre>
<p>You now have a catalog, a shortlist, and a notebook. That's the whole product surface the MCP tools will wrap.</p>
<h2 id="heading-how-to-write-the-scholarship-service">How to Write the Scholarship Service</h2>
<p>Keep MongoDB queries out of the MCP layer. Tools should call a service, get plain objects back, and format text. That makes the same functions reusable from a seed script, a smoke test, or a future REST route.</p>
<h3 id="heading-search">Search</h3>
<p>Search is a filter builder. Each optional argument adds a clause. Open or rolling awards stay in the result set. Closed deadlines drop out.</p>
<pre><code class="language-javascript">const OPEN_DEADLINE_FILTER = {
  $or: [{ rolling: true }, { deadline: null }, { deadline: { $gte: new Date() } }],
};

export async function searchScholarships(filters) {
  const query = { ...OPEN_DEADLINE_FILTER };
  const and = [query];

  if (filters.keyword) {
    and.push({
      $or: [
        { title: { $regex: escapeRegex(filters.keyword), $options: "i" } },
        { provider: { $regex: escapeRegex(filters.keyword), $options: "i" } },
        { description: { $regex: escapeRegex(filters.keyword), $options: "i" } },
        { fieldsOfStudy: { $regex: escapeRegex(filters.keyword), $options: "i" } },
      ],
    });
  }

  if (filters.fieldOfStudy) {
    and.push({
      $or: [
        { fieldsOfStudy: { $regex: `^any$`, $options: "i" } },
        { fieldsOfStudy: { $regex: escapeRegex(filters.fieldOfStudy), $options: "i" } },
      ],
    });
  }

  // educationLevel, citizenship, country, minAmount, gpa, flags...

  const results = await Scholarship.find({ $and: and })
    .sort({ deadline: 1, amountMax: -1 })
    .limit(100)
    .lean();

  return rankByFieldMatch(results, filters.fieldOfStudy).slice(0, filters.limit ?? 10);
}
</code></pre>
<p>Two details are easy to skip and worth keeping.</p>
<p>Escape user input before you drop it into <code>$regex</code>. A keyword of <code>(</code> shouldn't become a broken regular expression.</p>
<p>When a student searches for <code>computer science</code>, field-open awards (<code>any</code>) are eligible, but the specific CS scholarships should appear first. Rank in memory after the query. MongoDB already did the eligibility filter. You're only adjusting display order.</p>
<h3 id="heading-match">Match</h3>
<p>Matching is not the same as search. Search is "find documents that look like this." Matching is "here is a student, score every open award."</p>
<p>The service loads open scholarships, then applies hard filters and a score:</p>
<table>
<thead>
<tr>
<th>Signal</th>
<th>Effect</th>
</tr>
</thead>
<tbody><tr>
<td>Education level mismatch</td>
<td>Skip</td>
</tr>
<tr>
<td>GPA below the minimum</td>
<td>Skip</td>
</tr>
<tr>
<td>Citizenship mismatch</td>
<td>Skip</td>
</tr>
<tr>
<td>First-generation-only and student is not</td>
<td>Skip</td>
</tr>
<tr>
<td>Women-only and student is not</td>
<td>Skip</td>
</tr>
<tr>
<td>Education level match</td>
<td>+20</td>
</tr>
<tr>
<td>GPA eligible</td>
<td>+15</td>
</tr>
<tr>
<td>Citizenship eligible</td>
<td>+15</td>
</tr>
<tr>
<td>Field of study match</td>
<td>+30</td>
</tr>
<tr>
<td>Preferred country match</td>
<td>+10</td>
</tr>
<tr>
<td>Amount meets the student's minimum</td>
<td>+10</td>
</tr>
<tr>
<td>First-generation or women-only match</td>
<td>+10</td>
</tr>
</tbody></table>
<p>Hard filters prevent false hope. Soft scores rank the rest. Each result also returns a <code>reasons</code> array, so the model can explain the match instead of inventing one.</p>
<p>That last point is the whole reason to put matching on the server. If you only return raw documents, the model will sometimes "helpfully" include an award the student can't apply for. Returning <code>score</code> and <code>reasons</code> keeps the explanation grounded in code.</p>
<h3 id="heading-save-notes-deadlines-compare">Save, Notes, Deadlines, Compare</h3>
<p>The remaining functions are thin:</p>
<ul>
<li><p><code>saveScholarship</code> upserts by <code>(researcherId, scholarshipId)</code></p>
</li>
<li><p><code>addResearchNote</code> inserts a note after confirming the scholarship exists</p>
</li>
<li><p><code>getUpcomingDeadlines</code> queries <code>deadline</code> between now and <code>now + N days</code></p>
</li>
<li><p><code>compareScholarships</code> loads two or three documents and returns the same fields for each</p>
</li>
</ul>
<p>Validate MongoDB ids with <code>mongoose.Types.ObjectId.isValid</code> before you query. An LLM will occasionally pass a title where you asked for an id. Fail clearly. Don't throw a CastError into the MCP transport.</p>
<h2 id="heading-how-to-register-mcp-tools">How to Register MCP Tools</h2>
<p>Create <code>src/mcp/server.js</code> as a factory. The HTTP handler will call it on every request.</p>
<pre><code class="language-javascript">import { McpServer } from "@modelcontextprotocol/server";
import { registerPrompts } from "./prompts.js";
import { registerResources } from "./resources.js";
import { registerTools } from "./tools.js";

export function createScholarshipServer() {
  const server = new McpServer({
    name: "scholarship-research",
    version: "1.0.0",
  });

  registerTools(server);
  registerResources(server);
  registerPrompts(server);

  return server;
}
</code></pre>
<p>Keep this factory cheap. No database connections, no file reads, and no caches that belong at module scope. The <a href="https://ts.sdk.modelcontextprotocol.io/v2/serving/http.html">HTTP serving guide</a> is explicit about this: create connection pools once at startup, and close over them.</p>
<h3 id="heading-one-tool-fully">One Tool, Fully</h3>
<p><code>registerTool</code> takes a name, a config object, and a handler. The <code>inputSchema</code> is a Zod object. The SDK turns that schema into JSON Schema for <code>tools/list</code>, validates arguments before your handler runs, and infers types if you're on TypeScript.</p>
<pre><code class="language-javascript">import * as z from "zod/v4";

server.registerTool(
  "search_scholarships",
  {
    title: "Search scholarships",
    description:
      "Search the scholarship catalog by keyword, field of study, education level, citizenship, country, GPA, and award amount.",
    inputSchema: z.object({
      keyword: z.string().min(1).optional().describe("Free-text search across title, provider, description, and fields"),
      fieldOfStudy: z.string().optional().describe("For example computer science, public health, or engineering"),
      educationLevel: z.enum(["undergraduate", "graduate", "doctoral"]).optional(),
      citizenship: z.string().optional(),
      country: z.string().optional(),
      minAmount: z.number().nonnegative().optional(),
      gpa: z.number().min(0).max(4).optional(),
      firstGeneration: z.boolean().optional(),
      womenOnly: z.boolean().optional(),
      limit: z.number().int().min(1).max(25).optional(),
    }),
    annotations: { readOnlyHint: true, openWorldHint: false },
  },
  async (args) =&gt; {
    const results = await searchScholarships(args);
    return toolText(formatScholarshipList(results));
  },
);
</code></pre>
<p>Write descriptions as if the model is the only docs the tool will ever get. <code>.describe()</code> on a Zod field survives conversion to JSON Schema. That's how the host tells the model what <code>fieldOfStudy</code> means.</p>
<p><code>title</code> is the human label. <code>description</code> is the model-facing contract. They're not the same string.</p>
<h3 id="heading-annotations">Annotations</h3>
<p>Annotations don't change how the SDK runs the tool. Hosts use them to decide how cautious to be.</p>
<ul>
<li><p><code>readOnlyHint: true</code> for search, get, match, list, compare, and deadlines</p>
</li>
<li><p><code>readOnlyHint: false</code> for save and add-note</p>
</li>
<li><p><code>idempotentHint: true</code> on save, because of the unique index</p>
</li>
<li><p><code>openWorldHint: false</code> because this server talks to your database, not the open web</p>
</li>
</ul>
<p>A host can auto-approve a read-only search and ask the user before a write. That's worth five extra keys in the config.</p>
<h3 id="heading-return-shape">Return Shape</h3>
<p>Every tool returns MCP content blocks:</p>
<pre><code class="language-javascript">export function toolText(text, isError = false) {
  return {
    content: [{ type: "text", text }],
    isError,
  };
}
</code></pre>
<p>Return <code>isError: true</code> for domain failures such as "scholarship not found." Throw only for unexpected failures. The spec treats those differently. A validation error from Zod never reaches your handler. The SDK already converts it into an <code>isError</code> result.</p>
<p>Format lists for a person who is skimming a chat transcript. Include the MongoDB id on every row. Later tools need that id, and the model can't invent a valid ObjectId.</p>
<h3 id="heading-the-full-tool-set">The Full Tool Set</h3>
<p>The server registers nine tools:</p>
<table>
<thead>
<tr>
<th>Tool</th>
<th>What it does</th>
</tr>
</thead>
<tbody><tr>
<td><code>search_scholarships</code></td>
<td>Filter the catalog</td>
</tr>
<tr>
<td><code>get_scholarship</code></td>
<td>Return one full record</td>
</tr>
<tr>
<td><code>match_scholarships</code></td>
<td>Score awards against a student profile</td>
</tr>
<tr>
<td><code>save_scholarship</code></td>
<td>Bookmark an award</td>
</tr>
<tr>
<td><code>list_saved_scholarships</code></td>
<td>Show the shortlist</td>
</tr>
<tr>
<td><code>add_research_note</code></td>
<td>Attach a note</td>
</tr>
<tr>
<td><code>list_research_notes</code></td>
<td>Read notes back</td>
</tr>
<tr>
<td><code>get_upcoming_deadlines</code></td>
<td>Deadline window</td>
</tr>
<tr>
<td><code>compare_scholarships</code></td>
<td>Side-by-side of two or three ids</td>
</tr>
</tbody></table>
<p>That's enough for a research loop: find, inspect, match, save, annotate, and compare.</p>
<p>Resist the urge to add a <code>delete_everything</code> tool. Destructive tools need extra confirmation and aren't part of this workflow.</p>
<h2 id="heading-how-to-expose-resources-and-prompts">How to Expose Resources and Prompts</h2>
<p>Tools aren't the only way a host gets context.</p>
<h3 id="heading-resources">Resources</h3>
<p>A static resource is a fixed URI. The catalog fits that:</p>
<pre><code class="language-javascript">server.registerResource(
  "scholarship-catalog",
  "scholarship://catalog",
  {
    title: "Scholarship catalog",
    description: "Open scholarships currently stored in MongoDB",
    mimeType: "application/json",
  },
  async (uri) =&gt; {
    const catalog = await listCatalog();
    return {
      contents: [
        {
          uri: uri.href,
          mimeType: "application/json",
          text: JSON.stringify(catalog, null, 2),
        },
      ],
    };
  },
);
</code></pre>
<p>A resource template covers a family of URIs. Use <code>scholarship://item/{id}</code> rather than <code>scholarship://{id}</code>. If the pattern is <code>scholarship://{id}</code>, the URI <code>scholarship://catalog</code> becomes ambiguous.</p>
<pre><code class="language-javascript">import { ResourceTemplate } from "@modelcontextprotocol/server";

server.registerResource(
  "scholarship-record",
  new ResourceTemplate("scholarship://item/{id}", {
    list: async () =&gt; {
      const catalog = await listCatalog(20);
      return {
        resources: catalog.map((scholarship) =&gt; ({
          uri: `scholarship://item/${scholarship._id}`,
          name: scholarship.title,
          mimeType: "text/plain",
        })),
      };
    },
  }),
  {
    title: "Scholarship record",
    description: "Full details for one scholarship",
    mimeType: "text/plain",
  },
  async (uri, { id }) =&gt; {
    const scholarship = await getScholarshipById(id);
    return {
      contents: [
        {
          uri: uri.href,
          mimeType: "text/plain",
          text: scholarship
            ? formatScholarship(scholarship)
            : `No scholarship found with id ${id}.`,
        },
      ],
    };
  },
);
</code></pre>
<p><code>list</code> is required on a template. Pass <code>undefined</code> if you can't enumerate instances. Here you can, so the host can show a picker.</p>
<h3 id="heading-prompts">Prompts</h3>
<p>Prompts are workflows you want a person to start. The research plan prompt doesn't query MongoDB itself. It tells the model to use the tools, then structure the answer.</p>
<pre><code class="language-javascript">server.registerPrompt(
  "research-plan",
  {
    title: "Scholarship research plan",
    description: "Build a week-by-week research and application plan from a student profile.",
    argsSchema: z.object({
      fieldOfStudy: z.string(),
      educationLevel: z.string(),
      citizenship: z.string(),
      gpa: z.string(),
      weeks: z.string().optional(),
    }),
  },
  ({ fieldOfStudy, educationLevel, citizenship, gpa, weeks }) =&gt; ({
    messages: [
      {
        role: "user",
        content: {
          type: "text",
          text: `Create a ${weeks || "6"}-week scholarship research plan for this student.

Field of study: ${fieldOfStudy}
Education level: ${educationLevel}
Citizenship: ${citizenship}
GPA: ${gpa}

Use the scholarship research tools to find real awards first. Then produce a shortlist, a week-by-week plan, and risks. Name actual scholarships and dates from the tool results.`,
        },
      },
    ],
  }),
);
</code></pre>
<p>Notice the instruction "use the scholarship research tools." A prompt isn't a substitute for tools. It's a script that makes tool use more likely and the output shape more consistent.</p>
<p>A second prompt, <code>application-checklist</code>, takes a scholarship id and asks for a document list and a backward calendar. That's the kind of repetitive work MCP prompts are good at.</p>
<p>Prompt arguments are strings in many hosts, even when the value is a number. Typing <code>gpa</code> as a string avoids a frustrating <code>expected number, received string</code> error from a slash-command form.</p>
<h2 id="heading-how-to-serve-mcp-over-express">How to Serve MCP Over Express</h2>
<p>This is the part that used to be a page of session-handling code. In SDK v2 it's a factory plus one route.</p>
<pre><code class="language-javascript">import "dotenv/config";
import { createMcpExpressApp } from "@modelcontextprotocol/express";
import { toNodeHandler } from "@modelcontextprotocol/node";
import { createMcpHandler } from "@modelcontextprotocol/server";
import { config } from "./config.js";
import { connectDatabase } from "./db.js";
import { createScholarshipServer } from "./mcp/server.js";

const mcpHandler = createMcpHandler(() =&gt; createScholarshipServer());
const nodeHandler = toNodeHandler(mcpHandler);

const app = createMcpExpressApp({
  host: config.host,
  allowedHosts: ["127.0.0.1", "localhost"],
});

app.get("/health", (_req, res) =&gt; {
  res.json({
    status: "ok",
    service: "scholarship-research-mcp",
    transport: "streamable-http",
  });
});

app.all("/mcp", (req, res) =&gt; {
  void nodeHandler(req, res, req.body);
});

async function start() {
  await connectDatabase();
  app.listen(config.port, config.host, () =&gt; {
    console.log(`Scholarship research MCP server listening on http://${config.host}:${config.port}/mcp`);
  });
}

start();
</code></pre>
<p>Walk through what each helper is doing.</p>
<p><code>createMcpHandler</code> takes a function that returns a fresh <code>McpServer</code>. It exposes a web-standard <code>fetch</code>. That's the same handler you would export from a Cloudflare Worker.</p>
<p><code>toNodeHandler</code> adapts that <code>fetch</code> to Express <code>(req, res)</code>. You pass <code>req.body</code> as the third argument because <code>createMcpExpressApp</code> already ran <code>express.json()</code>. If you omit the body, the adapter tries to read a stream Express already consumed.</p>
<p><code>createMcpExpressApp</code> is <code>express()</code> with two extras: JSON parsing, and Host/Origin checks. Those checks exist because of DNS rebinding. A malicious page can point its own domain at <code>127.0.0.1</code> and, without a Host check, your browser would treat the local MCP server as same-origin. The default bind is <code>127.0.0.1</code> for that reason. The <a href="https://ts.sdk.modelcontextprotocol.io/v2/serving/express.html">Express serving guide</a> covers this in more detail.</p>
<p><code>app.all("/mcp", ...)</code> is intentional. Streamable HTTP uses POST for JSON-RPC, and GET for SSE streams. Registering only POST will break some clients.</p>
<p>Shut the handler down on <code>SIGINT</code>:</p>
<pre><code class="language-javascript">process.on("SIGINT", async () =&gt; {
  await mcpHandler.close();
  process.exit(0);
});
</code></pre>
<p><code>close()</code> waits for in-flight requests. Then you can exit.</p>
<p>Add npm scripts:</p>
<pre><code class="language-json">{
  "scripts": {
    "start": "node src/index.js",
    "dev": "node --watch src/index.js",
    "seed": "node scripts/seed.js",
    "smoke": "node scripts/smoke-test.js"
  }
}
</code></pre>
<p><code>node --watch</code> is enough for local development. You don't need nodemon for this project.</p>
<h2 id="heading-how-to-seed-the-catalog">How to Seed the Catalog</h2>
<p>MCP tools against an empty database will work and return "no scholarships matched." That's correct, and also a bad first impression.</p>
<p>Put 20 to 30 realistic records in <code>data/scholarships.json</code>. Mix:</p>
<ul>
<li><p>Undergraduate and graduate awards</p>
</li>
<li><p>STEM and field-open awards</p>
</li>
<li><p>Country-specific programs and global ones</p>
</li>
<li><p>Rolling deadlines and hard dates</p>
</li>
<li><p>First-generation and women-only flags</p>
</li>
</ul>
<p>A seed script should replace the catalog, not append:</p>
<pre><code class="language-javascript">import "dotenv/config";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { connectDatabase } from "../src/db.js";
import { Scholarship } from "../src/models/index.js";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

async function seed() {
  await connectDatabase();
  const raw = await readFile(path.join(__dirname, "..", "data", "scholarships.json"), "utf8");
  await Scholarship.deleteMany({});
  const inserted = await Scholarship.insertMany(JSON.parse(raw));
  console.log(`Seeded ${inserted.length} scholarships.`);
  process.exit(0);
}

seed();
</code></pre>
<p>Run it:</p>
<pre><code class="language-bash">npm run seed
</code></pre>
<p>Treat the JSON as sample data. Names of well-known programs help the tutorial feel real. They also create a duty to say, clearly, that students must verify every number and date on the official site. The <code>applicationUrl</code> field exists so that reminder has somewhere to point.</p>
<p>If you later replace the JSON with a live source, keep the same schema. The MCP tools shouldn't care where the documents came from.</p>
<h2 id="heading-how-to-test-the-server">How to Test the Server</h2>
<p>Start MongoDB, seed, then start the process:</p>
<pre><code class="language-bash">npm run seed
npm start
</code></pre>
<p>You should see:</p>
<pre><code class="language-text">Scholarship research MCP server listening on http://127.0.0.1:3000/mcp
</code></pre>
<h3 id="heading-health-check">Health Check</h3>
<pre><code class="language-bash">curl -s http://127.0.0.1:3000/health
</code></pre>
<p>A JSON <code>status: ok</code> means Express is up. It doesn't mean MCP is wired correctly. For that, send a JSON-RPC request.</p>
<h3 id="heading-list-tools">List Tools</h3>
<pre><code class="language-bash">curl -s -X POST http://127.0.0.1:3000/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
</code></pre>
<p>The response is an SSE event whose <code>data:</code> line contains the JSON-RPC result. You should see all nine tools, each with a JSON Schema derived from Zod.</p>
<p>The <code>Accept</code> header matters. MCP Streamable HTTP can return JSON or an event stream. Asking for both is the compatible choice.</p>
<h3 id="heading-call-a-tool">Call a Tool</h3>
<pre><code class="language-bash">curl -s -X POST http://127.0.0.1:3000/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{
    "jsonrpc":"2.0",
    "id":2,
    "method":"tools/call",
    "params": {
      "name": "search_scholarships",
      "arguments": {
        "fieldOfStudy": "computer science",
        "limit": 3
      }
    }
  }'
</code></pre>
<p>You should get a numbered list with ids, deadlines, and GPA minimums. Copy one id and pass it to <code>get_scholarship</code>.</p>
<p>A small Node smoke test is nicer than raw curl once you're calling several methods. Parse the <code>data:</code> line, then print <code>result.content[0].text</code>. The repo includes <code>scripts/smoke-test.js</code> for that.</p>
<h3 id="heading-inspector">Inspector</h3>
<p>The <a href="https://modelcontextprotocol.io/docs/tools/inspector">MCP Inspector</a> is the official GUI for servers. Point it at <code>http://127.0.0.1:3000/mcp</code> and you can list tools, fill in arguments, and read resources without a host app in the way.</p>
<p>Use Inspector when a host "can't see" your server. If Inspector works and the host doesn't, the bug is in the host config. If Inspector fails, the bug is in your process.</p>
<h2 id="heading-how-to-connect-cursor-and-claude-desktop">How to Connect Cursor and Claude Desktop</h2>
<p>Keep <code>npm start</code> running. MCP over HTTP is a live server, not a one-shot CLI.</p>
<h3 id="heading-cursor">Cursor</h3>
<p>Add a server entry in Cursor's MCP settings. A Streamable HTTP server looks like this:</p>
<pre><code class="language-json">{
  "mcpServers": {
    "scholarship-research": {
      "url": "http://127.0.0.1:3000/mcp"
    }
  }
}
</code></pre>
<p>Restart the MCP session if Cursor had a previous failed connection cached. Then ask:</p>
<blockquote>
<p>I am a first-generation undergraduate studying computer science in the United States, GPA 3.6. Use the scholarship research tools to build a shortlist and a six-week plan.</p>
</blockquote>
<p>You should see the host call <code>match_scholarships</code> or <code>search_scholarships</code>, then <code>get_scholarship</code> for the interesting rows, then maybe <code>save_scholarship</code>. If it never calls a tool, the server isn't actually connected. Check the MCP logs in Cursor before you change code.</p>
<p>You can also invoke the <code>research-plan</code> prompt from the host's prompt menu if it surfaces prompts.</p>
<h3 id="heading-claude-desktop">Claude Desktop</h3>
<p>Claude Desktop's config file lives at:</p>
<ul>
<li><p>macOS: <code>~/Library/Application Support/Claude/claude_desktop_config.json</code></p>
</li>
<li><p>Windows: <code>%APPDATA%\Claude\claude_desktop_config.json</code></p>
</li>
</ul>
<p>Claude Desktop prefers stdio. Bridge your HTTP server with <a href="https://www.npmjs.com/package/mcp-remote"><code>mcp-remote</code></a>:</p>
<pre><code class="language-json">{
  "mcpServers": {
    "scholarship-research": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "http://127.0.0.1:3000/mcp"]
    }
  }
}
</code></pre>
<p>Restart Claude Desktop after you save the file. The scholarship tools should appear in the tool list.</p>
<p>Don't put MongoDB credentials in the Claude config. The Node process already loaded <code>.env</code>. The host only needs the URL.</p>
<h2 id="heading-how-a-research-session-runs">How a Research Session Runs</h2>
<p>Here's a realistic session against the seed catalog. The student is a first-generation undergraduate in computer science, a US citizen, GPA 3.6.</p>
<p>The host calls <code>match_scholarships</code> with that profile. The server returns ranked rows. A women-in-technology award and a first-generation program both score well, for different reasons. A graduate-only award never appears.</p>
<p>The host then calls <code>get_scholarship</code> on the top two ids. Each result includes the official URL, the requirement list, and the deadline. That's the moment to tell the student to open the URL. The model shouldn't be the last word on eligibility.</p>
<p>If an award is worth pursuing, the host calls <code>save_scholarship</code> with a <code>researcherId</code> such as <code>ada</code> and status <code>saved</code>. Later it can set <code>applying</code>. <code>list_saved_scholarships</code> is how a new chat picks up the shortlist. Persistence is the whole point of MongoDB here. Without it, every conversation starts from zero.</p>
<p><code>add_research_note</code> is for the messy human details: "Ask Dr. Chen for a recommendation by October 1." <code>get_upcoming_deadlines</code> is the weekly sweep. <code>compare_scholarships</code> is for the moment the student has two finalists and needs amount, GPA, and requirements in one view.</p>
<p>The <code>research-plan</code> prompt packages that loop. It injects the profile into a user message that tells the model to call tools first and then produce a week-by-week plan. If you invoke the prompt without a connected server, you get a generic essay. If you invoke it with this server running, you get named awards and real dates.</p>
<p>That's the product: a catalog the model can query, a shortlist it can't forget, and prompts that make the workflow repeatable.</p>
<h2 id="heading-how-the-matching-logic-works">How the Matching Logic Works</h2>
<p>It's worth slowing down on matching, because this is the part people are tempted to hand to the model.</p>
<p>Suppose the student is:</p>
<ul>
<li><p>GPA 3.6</p>
</li>
<li><p>Computer science</p>
</li>
<li><p>Undergraduate</p>
</li>
<li><p>United States citizen</p>
</li>
<li><p>First-generation</p>
</li>
<li><p>A woman</p>
</li>
</ul>
<p>The matcher walks every open award.</p>
<p>A women-in-technology scholarship with a 3.3 GPA minimum, CS as a listed field, and US eligibility scores high: education, GPA, citizenship, women-only flag, and field all hit. A first-generation program that is field-open also scores high, because <code>any</code> counts as a field match. A graduate-only award is skipped, even if the title looks relevant. A 3.8 GPA cutoff is skipped, even if everything else fits.</p>
<p>The tool then returns ranked rows with reasons:</p>
<pre><code class="language-text">1. Palantir Women in Technology Scholarship — score 90
   Why: education level matches; GPA 3.6 meets the 3.3 minimum; citizenship is eligible; women-only award matches; field of study matches
</code></pre>
<p>The model can still write a warm paragraph around that. It shouldn't be the component that decided eligibility.</p>
<p>If you extend this later, keep the same split. New eligibility rules belong in the service. New prose belongs in the prompt.</p>
<h2 id="heading-what-you-can-build-next">What You Can Build Next</h2>
<p>The server you have is complete enough to use. It's also a base for a more serious research tool.</p>
<p>First, you could replace the seed file with a live source. Official feeds such as <a href="https://www.grants.gov/">Grants.gov</a> and college-maintained lists are safer than scraping commercial aggregators. Keep your schema. Write an importer that upserts by a stable external id.</p>
<p>You could also add authentication. <code>createMcpExpressApp</code> works with <code>requireBearerAuth</code>. Map <code>researcherId</code> from the verified token instead of a tool argument. The <a href="https://www.npmjs.com/package/@modelcontextprotocol/express">Express adapter</a> documents that middleware.</p>
<p>Try adding full-text search. The schema already has a text index. For a large catalog, Atlas Search or a dedicated search engine will beat a pile of regex filters.</p>
<p>You can track documents, not just notes. A <code>documents</code> collection for transcripts, recommendation status, and essay drafts turns the shortlist into an application tracker.</p>
<p>You could also write tests around the matcher. Eligibility code is where silent bugs hurt people. A table of profiles and expected include/exclude lists will pay for itself.</p>
<p>And you could deploy it. Bind to <code>127.0.0.1</code> on your laptop. If you put this on a network, set <code>allowedHosts</code>, terminate TLS, and require a bearer token. An open MCP server is an open database with a helpful English interface.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this guide, you built a scholarship research MCP server that's more than a toy tool list.</p>
<p>You stored awards, shortlists, and notes in MongoDB. You exposed search, matching, and research-tracking as MCP tools, with Zod schemas the host can advertise to a model. You added resources for the catalog and prompts for repeatable workflows. You served the whole thing over Streamable HTTP with Express, including the Host header checks the SDK enables for localhost.</p>
<p>The pattern transfers. Any research workflow with a catalog and a personal working set can use the same three layers: a service that owns the rules, an <code>McpServer</code> factory that registers primitives, and a small Express app that speaks the protocol.</p>
<p>If you take one idea from this tutorial, take this one: let the model write the plan, and let your server decide what's true.</p>
<p>The sample catalog is for learning. Confirm every scholarship on its official application page before you apply, and before you tell someone else to apply.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How AI Receptionists Work: The Architecture Behind AI Phone Agents ]]>
                </title>
                <description>
                    <![CDATA[ An AI receptionist may sound simple from the outside: a caller speaks, the system responds, and the conversation continues until the caller gets an answer or reaches a person. Behind that conversation ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-ai-receptionists-work-the-architecture-behind-ai-phone-agents/</link>
                <guid isPermaLink="false">6a9b251247b584b8b59d4b97</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ai agents ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Fri, 04 Sep 2026 20:07:46 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/b902c73d-3593-4fe6-8f64-8180636cb58b.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>An AI receptionist may sound simple from the outside: a caller speaks, the system responds, and the conversation continues until the caller gets an answer or reaches a person.</p>
<p>Behind that conversation sits a pipeline of telephony infrastructure, speech recognition, language models, application logic, APIs, databases, and call routing.</p>
<p>The interesting part isn't just the AI model. It's how these components work together to turn an audio stream into useful business actions.</p>
<p>Businesses that want these capabilities have two paths.</p>
<p>They can buy a finished product. The market now includes dedicated AI receptionists such as <a href="https://www.nextiva.com/products/xbert">XBert from Nextiva</a>, along with tools from <a href="https://www.goodcall.com/">Goodcall</a>, <a href="https://dialzara.com/">Dialzara</a>, and others.</p>
<p>Or they can build one, which is what this article walks you through. Understanding the architecture helps either way: it shows what a commercial product is doing under the hood, and what a custom build needs to assemble.</p>
<p>A typical architecture looks something like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/66c6d8f04fa7fe6a6e337edd/cf8e4995-77b2-4d23-bcc6-639e1379d69b.png" alt="AI Receptionist architecture" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>In this article, we'll walk through the architecture behind an AI phone agent, from the moment a caller dials a business number to what happens after the call ends.</p>
<p>You'll see how telephony systems connect calls, how speech becomes text, how AI identifies intent and maintains conversation context, and how function calls connect the agent to calendars, CRMs, and other business systems. We'll also look at how agents decide when to hand a call to a human and what information can be passed along during that handoff.</p>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-a-phone-call-enters-the-system">A Phone Call Enters the System</a></p>
</li>
<li><p><a href="#heading-speech-becomes-text">Speech Becomes Text</a></p>
</li>
<li><p><a href="#heading-the-system-determines-what-the-caller-wants">The System Determines What the Caller Wants</a></p>
</li>
<li><p><a href="#heading-the-conversation-runs-as-a-loop">The Conversation Runs as a Loop</a></p>
</li>
<li><p><a href="#heading-the-ai-calls-business-systems">The AI Calls Business Systems</a></p>
</li>
<li><p><a href="#heading-booking-an-appointment">Booking an Appointment</a></p>
</li>
<li><p><a href="#heading-capturing-lead-information">Capturing Lead Information</a></p>
</li>
<li><p><a href="#heading-knowing-when-to-involve-a-human">Knowing When to Involve a Human</a></p>
</li>
<li><p><a href="#heading-what-happens-after-the-call">What Happens After the Call</a></p>
</li>
<li><p><a href="#heading-what-the-business-sees">What the Business Sees</a></p>
</li>
</ul>
<h2 id="heading-a-phone-call-enters-the-system">A Phone Call Enters the System</h2>
<p>The process starts like a regular phone call. A customer dials a business number, and the telecommunications provider receives the call.</p>
<p>The provider then needs to connect that call to an application capable of handling it.</p>
<p>One common approach is a webhook. The telephony provider sends an HTTP request to an application when an incoming call arrives. The application can then return instructions describing how the call should be handled.</p>
<p>The call itself needs carrier connectivity. Production systems typically use SIP trunking, which connects a voice application or phone system to the public telephone network over the internet.</p>
<p>Providers such as Nextiva, Twilio, and Bandwidth offer SIP trunking that supplies the numbers and call capacity an AI voice system runs on. From there, the application layer takes over.</p>
<p>For example, <a href="https://www.twilio.com/docs/voice/twiml">Twilio</a> sends an HTTP request to a configured application when an incoming voice call arrives. The application can respond with TwiML instructions that control the call.</p>
<p>A simplified Node.js endpoint might look like this:</p>
<pre><code class="language-js">app.post("/incoming-call", (req, res) =&gt; {
  const response = new VoiceResponse();

  response.say("Hello. How can I help you today?");

  res.type("text/xml");
  res.send(response.toString());
});
</code></pre>
<p>This example doesn't contain any AI yet. It simply shows the first architectural boundary.</p>
<p>The phone network handles the call. The application receives an event and decides what happens next.</p>
<p>From here, the application needs to process the caller's audio.</p>
<h2 id="heading-speech-becomes-text">Speech Becomes Text</h2>
<p>People communicate with the system through audio, but most application logic works with structured data and text.</p>
<p>An <a href="https://huggingface.co/tasks/automatic-speech-recognition">automatic speech recognition system</a>, or ASR system, converts the caller's speech into text.</p>
<p>For example, a caller might say:</p>
<blockquote>
<p>"I need to move my appointment from Friday to Monday afternoon."</p>
</blockquote>
<p>The speech recognition layer might produce:</p>
<pre><code class="language-json">{
  "text": "I need to move my appointment from Friday to Monday afternoon."
}
</code></pre>
<p>The exact response depends on the speech recognition system. Some systems can also provide timestamps, confidence information, speaker information, or partial transcripts.</p>
<p>The application can now pass the recognized text to its conversational layer.</p>
<p>This separation is useful because the AI reasoning layer doesn't need to understand raw telephone audio. It receives text and returns a decision or response.</p>
<h2 id="heading-the-system-determines-what-the-caller-wants">The System Determines What the Caller Wants</h2>
<p>The next challenge is understanding intent.</p>
<p>Suppose three callers say:</p>
<blockquote>
<p>"I want to book a consultation."</p>
</blockquote>
<blockquote>
<p>"Can I move my appointment to next week?"</p>
</blockquote>
<blockquote>
<p>"Where is your office?"</p>
</blockquote>
<p>The system needs to recognize that these requests require different workflows.</p>
<p>An application could represent the detected intent as structured data:</p>
<pre><code class="language-json">{
  "intent": "reschedule_appointment",
  "entities": {
    "current_day": "Friday",
    "requested_day": "Monday",
    "time_preference": "afternoon"
  }
}
</code></pre>
<p>The language model can produce this structure, or the application can derive it through another classification layer.</p>
<p>The important architectural point is that the application turns natural language into information that downstream systems can process.</p>
<p>The model might understand that the caller wants to reschedule an appointment, but it shouldn't directly modify a calendar simply because it generated that interpretation.</p>
<p>The application needs to control what happens next.</p>
<h2 id="heading-the-conversation-runs-as-a-loop">The Conversation Runs as a Loop</h2>
<p>An AI phone agent doesn't normally process an entire conversation in a single request.</p>
<p>Instead, it operates as a loop.</p>
<p>The caller speaks. Speech recognition converts the audio into text. The application sends the text and relevant context to the AI system. The AI determines what it needs to say or what action it needs to perform. The application generates audio and sends it back to the caller.</p>
<p>Then the caller speaks again.</p>
<p>A simplified version looks like this:</p>
<pre><code class="language-js">while (callIsActive) {
  const audio = await receiveAudio();

  const text = await speechToText(audio);

  const result = await processConversation({
    text,
    context: conversationContext
  });

  conversationContext = result.updatedContext;

  const audioResponse = await textToSpeech(result.response);

  await sendAudio(audioResponse);
}
</code></pre>
<p>This is conceptual code, not a complete phone implementation. Real systems need to handle streaming audio, interruptions, timeouts, errors, authentication, and provider-specific protocols.</p>
<p>Context is also important.</p>
<p>If the caller says:</p>
<blockquote>
<p>"I want to book an appointment."</p>
</blockquote>
<p>The system might ask:</p>
<blockquote>
<p>"What type of appointment do you need?"</p>
</blockquote>
<p>The caller then says:</p>
<blockquote>
<p>"An initial consultation."</p>
</blockquote>
<p>That second statement only makes sense because the application remembers the previous exchange.</p>
<p>Conversation state might contain information such as:</p>
<pre><code class="language-json">{
  "intent": "book_appointment",
  "appointment_type": "initial_consultation",
  "customer_name": "Jane Smith",
  "preferred_date": null
}
</code></pre>
<p>The system can add to this state as the conversation progresses.</p>
<h2 id="heading-the-ai-calls-business-systems">The AI Calls Business Systems</h2>
<p>This is where an AI receptionist becomes more than a voice chatbot.</p>
<p>Suppose a caller asks:</p>
<blockquote>
<p>"Do you have anything available tomorrow afternoon?"</p>
</blockquote>
<p>The AI can't reliably answer that from the conversation alone. It needs current information from a calendar or scheduling system.</p>
<p>This is where function calling, also called tool calling, becomes useful.</p>
<p>The application can expose a limited set of functions to the AI:</p>
<pre><code class="language-js">const tools = [
  {
    name: "check_calendar",
    description: "Find available appointment slots",
    parameters: {
      date: "string",
      appointmentType: "string"
    }
  },
  {
    name: "book_appointment",
    description: "Book an available appointment",
    parameters: {
      slotId: "string",
      customerId: "string"
    }
  }
];
</code></pre>
<p>The model can determine that it needs <code>check_calendar</code>.</p>
<p>The application then executes the function:</p>
<pre><code class="language-js">const slots = await checkCalendar({
  date: "2026-08-21",
  appointmentType: "consultation"
});
</code></pre>
<p>The result goes back into the conversation context:</p>
<pre><code class="language-json">{
  "available_slots": [
    "2026-08-21T14:00:00",
    "2026-08-21T15:30:00"
  ]
}
</code></pre>
<p>The AI can then tell the caller which options are available.</p>
<p>The important architectural boundary is that the AI decides what action may be needed, while application code controls how that action is performed.</p>
<p>That gives developers a place to enforce permissions, validate inputs, handle failures, and control access to business systems.</p>
<h2 id="heading-booking-an-appointment">Booking an Appointment</h2>
<p>Now consider the final step.</p>
<p>The caller selects one of the available times.</p>
<p>The AI can request an appointment booking:</p>
<pre><code class="language-json">{
  "tool": "book_appointment",
  "arguments": {
    "slotId": "slot_123",
    "customerId": "customer_456"
  }
}
</code></pre>
<p>The application validates those values before calling the calendar system.</p>
<p>For example:</p>
<pre><code class="language-js">async function bookAppointment(slotId, customerId) {
  const slot = await getAvailableSlot(slotId);

  if (!slot || slot.booked) {
    throw new Error("Appointment slot is no longer available");
  }

  return calendar.createEvent({
    customerId,
    start: slot.start,
    end: slot.end
  });
}
</code></pre>
<p>The application then returns the actual result to the AI.</p>
<p>This distinction matters.</p>
<p>The AI shouldn't tell the caller that an appointment has been booked merely because it decided to call <code>book_appointment</code>.</p>
<p>The calendar system needs to confirm that the operation succeeded.</p>
<p>Calendar APIs commonly expose operations for creating events. For example, <a href="https://developers.google.com/workspace/calendar/api/v3/reference/events/insert">Google Calendar</a> provides an <code>events.insert</code> method for creating an event.</p>
<p>Only after receiving a successful response should the conversational layer tell the caller that the appointment is confirmed.</p>
<h2 id="heading-capturing-lead-information">Capturing Lead Information</h2>
<p>The same architecture can capture information during a sales conversation.</p>
<p>A caller might provide a name, email address, company, phone number, service requirement, and preferred follow-up time.</p>
<p>The conversation can gradually populate a structured lead object:</p>
<pre><code class="language-json">{
  "name": "Jane Smith",
  "email": "jane@example.com",
  "company": "Example Corp",
  "interest": "enterprise consultation",
  "appointment_booked": true
}
</code></pre>
<p>The application can then send this information to a CRM.</p>
<p>This creates an important distinction between conversation data and business data.</p>
<p>The transcript represents what the caller said.</p>
<p>The CRM record represents the structured information that the business needs to act on.</p>
<p>A CRM might contain the caller's contact information, inquiry type, qualification data, appointment details, and follow-up status.</p>
<p>The exact fields depend on the company's CRM and sales process.</p>
<h2 id="heading-knowing-when-to-involve-a-human">Knowing When to Involve a Human</h2>
<p>Not every conversation should remain with an AI system.</p>
<p>A production system needs escalation rules.</p>
<p>An escalation might happen when the caller asks for a person, when the request falls outside the agent's supported workflows, or when the business has decided that a particular type of request requires human involvement.</p>
<p>The application can represent this decision explicitly:</p>
<pre><code class="language-js">if (shouldEscalate(conversation)) {
  return transferToHuman({
    callerId,
    reason,
    conversationContext
  });
}
</code></pre>
<p>The important part is what happens during the transfer.</p>
<p>A useful handoff should carry context rather than forcing the employee to start from zero.</p>
<p>The human agent might receive:</p>
<pre><code class="language-json">{
  "caller": {
    "name": "Jane Smith",
    "phone": "+1-555-0100"
  },
  "reason": "Complex billing question",
  "summary": "Caller needs help resolving an invoice discrepancy.",
  "actionsCompleted": [
    "Customer identity verified"
  ]
}
</code></pre>
<p>The exact handoff data depends on the system.</p>
<p>Telephony platforms can also support call transfers and callbacks through their voice APIs. For example, Twilio's voice documentation describes call routing and <code>&lt;Dial&gt;</code> functionality for connecting calls to another destination.</p>
<h2 id="heading-what-happens-after-the-call">What Happens After the Call</h2>
<p>The conversation doesn't necessarily end when the caller hangs up.</p>
<p>Depending on the system and its configuration, the application can retain the transcript and other call data.</p>
<p>A simplified call record might look like this:</p>
<pre><code class="language-json">{
  "callId": "call_123",
  "duration": 342,
  "customerId": "customer_456",
  "intent": "book_appointment",
  "appointmentId": "appointment_789",
  "leadCaptured": true,
  "escalated": false
}
</code></pre>
<p>The transcript can provide the detailed conversation, while structured fields provide information that downstream applications can query.</p>
<p>A call can therefore trigger several business actions.</p>
<ul>
<li><p>A sales call can create a lead.</p>
</li>
<li><p>An appointment call can update a calendar.</p>
</li>
<li><p>A support call can create a ticket.</p>
</li>
<li><p>A complex conversation can result in a human handoff.</p>
</li>
</ul>
<p>Telephony systems can also notify applications about call lifecycle events through status callbacks. Twilio, for example, sends a request to a number's StatusCallback URL when a call ends, and supports a <code>statusCallbackEvent</code> attribute for subscribing to lifecycle events such as initiated, ringing, answered, and completed on dialed legs.</p>
<h2 id="heading-what-the-business-sees">What the Business Sees</h2>
<p>From the employee's perspective, all of this infrastructure can be hidden behind a few business records.</p>
<p>An employee might see a new CRM lead with contact information and the reason for the call.</p>
<p>The calendar might contain a newly booked appointment.</p>
<p>The conversation system might contain the transcript and a summary.</p>
<p>If the call was transferred, the employee can receive the relevant context before speaking with the customer.</p>
<p>That's the main architectural idea behind AI phone agents.</p>
<p>The voice interface is only one layer. Underneath it is a collection of systems that convert speech into text, interpret the caller's request, maintain conversation state, call external services, validate business actions, and return results to the caller.</p>
<p>The language model provides the conversational reasoning. The surrounding application provides the state, tools, permissions, integrations, and business rules that turn that conversation into an actual workflow.</p>
<p>Hope you enjoyed this article. You can <a href="https://linkedin.com/in/manishmshiva">connect with me on LinkedIn</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use Discord with a Screen Reader: A Quick Guide ]]>
                </title>
                <description>
                    <![CDATA[ Discord is one of those technologies that came out of nowhere several years ago and is now everywhere. A huge variety of servers around all sorts of communities, efforts, and initiatives have been pop ]]>
                </description>
                <link>https://www.freecodecamp.org/news/using-discord-with-a-screen-reader/</link>
                <guid isPermaLink="false">6a9af102c3078d7ebb3a01fa</guid>
                
                    <category>
                        <![CDATA[ Accessibility ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Screen Reader ]]>
                    </category>
                
                    <category>
                        <![CDATA[ guide ]]>
                    </category>
                
                    <category>
                        <![CDATA[ discord ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Florian Beijers ]]>
                </dc:creator>
                <pubDate>Fri, 04 Sep 2026 16:25:38 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/364f60fe-def1-4b62-9252-ed59c2b79eb1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Discord is one of those technologies that came out of nowhere several years ago and is now everywhere.</p>
<p>A huge variety of servers around all sorts of communities, efforts, and initiatives have been popping up and are still appearing. And they're largely taking the place of forums, chat rooms and, at times, documentation sites and news boards.</p>
<p>To what degree this is a good thing is up for debate, but the long and short of it is that Discord is likely here to stay.</p>
<p>When it comes to accessibility, Discord's had a bit of a rocky road. For years, it was very painful to use for screen reader users due to an apparent lack of forethought regarding accessibility.</p>
<p>Over the last few years, this situation has improved substantially. While it's by no means fully accessible in 2026, it can be used relatively efficiently once you know the tricks of the trade.</p>
<p>freeCodeCamp uses Discord, and the community has often seen that screen reader users struggle to use this communication tool comfortably. This is where this article comes in.</p>
<p>In this article, I'll go over the basics you'll need to use Discord as a platform to send and receive messages and contribute to communities that use Discord as a communication platform. It's relatively easy to learn but can be difficult to fully grasp due to the various interwoven things it does. Still, these basics should get you up and running and will equip you to learn about all the other features it offers by yourself going forward.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-screen-reader-prerequisites">Screen Reader Prerequisites</a></p>
</li>
<li><p><a href="#heading-the-broad-strokes-how-to-navigate-efficiently">The Broad Strokes: How to Navigate Efficiently</a></p>
</li>
<li><p><a href="#heading-speeding-things-up-how-to-navigate-and-interact-efficiently">Speeding Things Up: How to Navigate and Interact Efficiently</a></p>
</li>
<li><p><a href="#heading-when-is-browse-mode-more-efficient">When is Browse Mode More Efficient?</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-screen-reader-prerequisites">Screen Reader Prerequisites</h2>
<p>Discord is an application that, because of the way it was built, has both web app and desktop app aspects to it. I think this is one of the main issues people run into, so I'll give a brief rundown about why this matters for screen reader users.</p>
<p>Particularly on Windows, screen readers tend to operate in two modes when a web app is encountered: you're either in browse/virtual mode or you're in forms/focus mode.</p>
<p>Discord, being essentially a web app, gives you both of these modes as well. In "browse" mode, you have access to keys that navigate by heading, form field, button, and so on. In focus mode, you only have access to the keyboard shortcuts that work with the app. By default, these are tab/shift+tab, the arrow keys, space, enter, and escape.</p>
<p>Knowing when to use which mode is a bit of a mine field, but in general, a good rule of thumb is that you read/browse in browse mode, and you act/type in forms/focus mode.</p>
<p>Many web apps, Discord included, generally gracefully switch you between modes when required, but this isn't always the case. When this doesn't happen, you need to know how and when to switch, which I'll point out when required in the upcoming sections of this article.</p>
<h2 id="heading-the-broad-strokes-how-to-navigate-efficiently">The Broad Strokes: How to Navigate Efficiently</h2>
<p>The tricky bit with learning an app like this is generally that documentation can be really scarce and hard to find. Discord probably has articles on this, but you'd need to know what to look for. And at the end of the day, all we want to do is use the app for what we're trying to do and move on with our lives. So here's the Cliff's notes.</p>
<p>Discord has a pretty rich list of keyboard shortcuts that allow you to do all sorts of things, but some of the keys aren't super obvious, and some of the navigation patterns are inconsistent.</p>
<p>In general, when navigating or skimming, you want to be in your screen reader's focus or forms mode. NVDA toggles this with NVDA+space, JAWS with JAWS+z. This allows Discord's own navigation keys to work correctly, which makes things go a little faster.</p>
<p>A key that works in a lot of applications on Windows is f6. This jumps you between specific regions of an application and is a bit of a hold-over from Ye olden Days of Windows 98 and XP. File explorer, office apps and browsers generally use this key in this way, but other apps like VS Code, Slack, and Discord do as well. It's a bit of a super power for keyboard-only navigation, as it gets you places a lot quicker.</p>
<p>In Discord, it bounces you between the server list, the message list, and a few other spots. But, oddly enough, it doesn't take you to the actual message entry field. To go there, the quickest way is to just start typing. This will zip your cursor to the right place.</p>
<p>With Discord having essentially its own keyboard navigation layer, I would generally recommend that you stay in focus/forms mode for most interactions, unless you need the conveniences of browse mode. More on that below.</p>
<p>When in focus mode, f6 moves you between the various regions of the screen, as I mentioned above. Tab will navigate you between those regions, as well as the interactable elements within those regions.</p>
<p>This, together with the arrow keys to navigate lists of channels, messages, and servers, will get you to most places you'll need to go for basic Discord usage.</p>
<h2 id="heading-speeding-things-up-how-to-navigate-and-interact-efficiently">Speeding Things Up: How to Navigate and Interact Efficiently</h2>
<p>If you know where you're going, the best way to get there quickly is the ctrl+k or cmd+k hotkeys, depending on your operating system. This hotkey will bring up a search field where you can type part of a channel name, user name, or server name to search for it. A list of results will be focused after a brief pause, and then you can use the arrow keys to move to the correct result. Pressing enter takes you straight to the server, channel, or user you selected.</p>
<p>On Discord servers, it can be difficult to keep up with everything happening, given how many channels a lot of servers have. Here's a few tips to keep track of it all:</p>
<ul>
<li><p>Ctrl+i / cmd+i will open your so-called "inbox". This is where you can get a list of your mentions on the various servers you're in, which can be a great way to keep track of people trying to get your attention.</p>
</li>
<li><p>There are a number of hotkeys that also let you cycle between various types of channels. For example, shift+alt+up and down will navigate between channels with unread messages. This is another quick way to get through a large amount of unread channels efficiently.</p>
</li>
</ul>
<h2 id="heading-when-is-browse-mode-more-efficient">When is Browse Mode More Efficient?</h2>
<p>I'd say there are two scenarios in which browse mode may be beneficial.</p>
<p>First, if you want to look at a message more granularly (for example to see how a word is spelled), dropping into browse mode will let you do that.</p>
<p>You can also use review cursors/object navigation shortcuts if you want, but browse mode tends to be easier in these cases. It also allows for more convenient copying of text if you need to do that.</p>
<p>To my knowledge, there's no quick way to jump to the earliest unread message in focus mode, which can be a bit annoying if you're scrolling back to see what messages you did and didn't read yet.</p>
<p>There is a "NEW" indicator above that message, which you can find with a reverse NVDA search, or by scrolling and listening out for it. Discord does have a hotkey (shift+page-up) to do this as well, but in my testing it doesn't always work as advertised.</p>
<p>Apart from that, barring a few niche edge cases, you can generally stay in focus mode unless you want to NVDA or JAWS search for button labels. This can be useful to quickly, say, find the disconnect button in a voice channel, which currently doesn't appear to have a hotkey associated with it.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Discord is a pretty complicated app, and this guide touches on the basics to get you started. With these general strategies, you should be able to deal with day-to-day messaging, channel management, and voice channels.</p>
<p>I always encourage anyone to just explore and work out your own strategies though. You can rarely break things irreparably, so see how things work for you personally and develop a way of working that works for you, specifically. This is always the best strategy for any kind of app, particularly when using assistive technology.</p>
<p>If you want to learn about all the other hotkeys Discord offers, you can find <a href="https://support.discord.com/hc/en-us/articles/225977308--Windows-Discord-Hotkeys">a keyboard reference</a> on the Discord website. They offer both visual charts, which are inaccessible for screen reader users in this instance, or a readable table.</p>
<p>I hope this was helpful. Go forth and Discord!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How AI Is Changing Patching and What Devs Need to Know About Exposure Management ]]>
                </title>
                <description>
                    <![CDATA[ When a vulnerability scanner reports 23 vulnerabilities in your application, of which 4 are critical, 7 are high, and the remaining 12 are medium, at first glance the answer seems clear: start patchin ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-ai-is-breaking-traditional-patch-management/</link>
                <guid isPermaLink="false">6a9aefa26eac286787fb7078</guid>
                
                    <category>
                        <![CDATA[ Artificial Intelligence ]]>
                    </category>
                
                    <category>
                        <![CDATA[ cybersecurity ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Vulnerability management ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Patch management ]]>
                    </category>
                
                    <category>
                        <![CDATA[ DevSecOps ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Exposure Management ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Reetain Raina ]]>
                </dc:creator>
                <pubDate>Fri, 04 Sep 2026 16:19:46 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/ce5b87e6-1941-493c-a2c9-7822138160d6.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When a vulnerability scanner reports 23 vulnerabilities in your application, of which 4 are critical, 7 are high, and the remaining 12 are medium, at first glance the answer seems clear: start patching. But which one should you fix first?</p>
<p>This has always been an issue in vulnerability management. While a security team might find out about the vulnerable dependency, fixing it may not always be possible at once. Developers need to ensure that the vulnerable code is in use and perform all necessary checks before releasing the fix into production.</p>
<p>Recently, though, there have been some solid advancements in the use of AI for discovering software vulnerabilities and exploits. This <a href="https://dl.acm.org/doi/10.1145/3708522">research</a>, for example, details some of the findings and the path forward.</p>
<p>But how will this really help the development community? We need to fix things more quickly, but more importantly, we need to be able to figure out which vulnerabilities actually matter and which ones need attention first.</p>
<p>In this article, we'll examine what the classic patching process looks like, how AI is decreasing the amount of time security teams have to react, and why it's not always reasonable just to address vulnerabilities by their severity score.</p>
<p>We'll also discuss exposure management and the difference between it and traditional vulnerability management. Then we'll cover how developers can analyze dependencies, code reachability, and Software Bill of Materials (SBOMs) to figure out the actual vulnerabilities in their applications.</p>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-patching-vs-exposure-management-whats-the-difference">Patching vs. Exposure Management: What's the Difference?</a></p>
<ul>
<li><p><a href="#heading-what-is-patching">What Is Patching?</a></p>
</li>
<li><p><a href="#heading-what-is-exposure-management">What Is Exposure Management?</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-the-old-patch-management-workflow-was-built-around-time">The Old Patch Management Workflow Was Built Around Time</a></p>
</li>
<li><p><a href="#heading-ai-is-shrinking-the-time-between-found-and-exploited">AI Is Shrinking the Time Between "Found" and "Exploited"</a></p>
</li>
<li><p><a href="#heading-why-patch-everything-doesnt-work-at-scale">Why "Patch Everything" Doesn't Work at Scale</a></p>
</li>
<li><p><a href="#heading-exposure-management-moving-from-flaw-counts-to-contextual-risk">Exposure Management: Moving from Flaw Counts to Contextual Risk</a></p>
</li>
<li><p><a href="#heading-the-dependency-tree-as-an-attack-surface">The Dependency Tree as an Attack Surface</a></p>
</li>
<li><p><a href="#heading-practical-takeaways-for-developers">Practical Takeaways for Developers</a></p>
<ul>
<li><p><a href="#heading-audit-transitive-dependencies">Audit Transitive Dependencies</a></p>
</li>
<li><p><a href="#heading-check-code-reachability">Check Code Reachability</a></p>
</li>
<li><p><a href="#heading-generate-an-sbom-in-cicd">Generate an SBOM in CI/CD</a></p>
</li>
<li><p><a href="#heading-use-compensating-controls-when-a-patch-isnt-ready">Use Compensating Controls When a Patch Isn't Ready</a></p>
</li>
<li><p><a href="#heading-apply-least-privilege-at-runtime">Apply Least Privilege at Runtime</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-wrap-up">Wrap Up</a></p>
</li>
</ul>
<h2 id="heading-patching-vs-exposure-management-whats-the-difference">Patching vs. Exposure Management: What's the Difference?</h2>
<p>Before we look at how AI is changing vulnerability response, it helps to understand two important concepts: patching and exposure management.</p>
<h3 id="heading-what-is-patching">What Is Patching?</h3>
<p>Patching is a process of updating software to address an existing issue, including a security vulnerability, bug, or a stability problem. This may involve upgrading a library with a known vulnerability, applying a security update for your operating system, or using a new release of an application with a vulnerability fixed.</p>
<p>For instance, if your application uses a particular library with a known security vulnerability, you can upgrade the library once the patched version is available. After that, you'll need to test the update, ensure that the application still operates as intended, and release the upgraded version into production.</p>
<p>So patching isn't only about installing the latest version of the package in question. A dependency update can break an API, some functionality, or even other dependent packages. That's why teams typically use patch management strategies when identifying vulnerabilities, updating decision-making, testing, deploying, and verifying that the patches work.</p>
<h3 id="heading-what-is-exposure-management">What Is Exposure Management?</h3>
<p>While vulnerability management is concerned mainly with identifying vulnerabilities, exposure management focuses more broadly on whether those vulnerabilities can actually provide a realistic route for an attack.</p>
<p>For example, a vulnerable library, limited in use to a development system, poses less immediate threat than a similarly vulnerable library used in an internet-facing system that's capable of accessing the database.</p>
<p>Some aspects to consider include accessibility via network, asset exposure, vulnerable code paths, identity and access controls, cloud environments, and the sensitivity of systems and data.</p>
<p>Simply put, vulnerability management is concerned with the discovery and tracking of vulnerabilities, whereas exposure management is focused on determining which of those vulnerabilities pose a true or larger risk.</p>
<h2 id="heading-the-old-patch-management-workflow-was-built-around-time">The Old Patch Management Workflow Was Built Around Time</h2>
<p>The classic process of vulnerability mitigation depended on step-by-step actions.</p>
<ol>
<li><p>CVE discovered</p>
</li>
<li><p>Security team assesses the severity</p>
</li>
<li><p>Maintainer releases an upstream patch</p>
</li>
<li><p>Developer updates the dependency</p>
</li>
<li><p>CI/CD pipeline runs regression tests</p>
</li>
<li><p>Production deployment</p>
</li>
<li><p>Remediation verified</p>
</li>
</ol>
<p>The process wasn't flawed by nature, but it operated under the unspoken premise that the defenders were granted enough room to operate through each step.</p>
<p>Let's take a dependency vulnerability example for practice. If the automated scanner detects a vulnerability within a popular utility package such as <strong>lodash</strong>, engineers don't immediately bump the dependency version in the production environment. They need to confirm if the application code leverages the vulnerable function, check the presence of breaking API changes after the upgrade, and run build validation through integration tests.</p>
<p>Each security patch is essentially a change to the code and needs to be safely pushed through the development and deployment cycle.</p>
<h2 id="heading-ai-is-shrinking-the-time-between-found-and-exploited">AI Is Shrinking the Time Between "Found" and "Exploited"</h2>
<p>The buffer between vulnerability discovery and exploitation that used to exist is being eliminated. This is because automated programs can scan through codebases, generate proofs of concept, and discover edge cases.</p>
<p>Modern AI systems help researchers and would-be attackers alike perform tasks like static binary analysis, detecting vulnerabilities, creating exploit payloads, and finding logical issues in complicated software designs.</p>
<p>Programs such as <a href="https://www.darpa.mil/news/2024/ai-cyber-challenge-cybersecurity">DARPA’s Artificial Intelligence Cyber Challenge</a> (AIxCC) show how AI systems can be used to automatically find and patch vulnerabilities in complex open-source software. During the 2024 semifinal competition, autonomous Cyber Reasoning Systems were tested against projects based on real-world software such as Jenkins, the Linux kernel, Nginx, SQLite3, and Apache Tika. The systems discovered 22 unique synthetic vulnerabilities and successfully patched 15 of them. They also identified one real-world bug in SQLite3, which was responsibly disclosed.</p>
<p>In the context of a real development process, certain tasks in the patching process can be performed by AI. The AI could perform code analysis and dependency analysis in order to detect potential vulnerabilities. It could also help trace the usage of vulnerable functions, recommend changes in code and dependencies, and generate tests to make sure that the suggested patch doesn’t break the existing functionality. The security team could also use AI for pattern detection.</p>
<p>As AI tools get better at assessing software and detecting vulnerabilities, the window of time between vulnerability detection and its mitigation becomes smaller and more important. This change in paradigm also affects how security professionals approach <a href="https://www.axonius.com/blog/from-vulnpocalypse-to-patchmageddon-security-ops-in-the-ai-era">AI and exposure management</a>, especially as the exploit window gets smaller and vulnerabilities need proper prioritization.</p>
<p>AI can also support exposure management by connecting vulnerability information with the environment in which the vulnerable software is running. For example, an AI-assisted security system could correlate a vulnerable dependency with an internet-facing application, its network connections, cloud permissions, and the data or services it can access. This helps security teams move from simply asking whether a vulnerability exists to asking <strong>what an attacker could realistically reach through it</strong>.</p>
<h2 id="heading-why-patch-everything-doesnt-work-at-scale">Why "Patch Everything" Doesn't Work at Scale</h2>
<p>When an organization-wide scanner generates a list of 500 vulnerabilities spread across multiple microservices, reacting to each one with urgency starts to seem impossible. Developers can suffer from alert fatigue and get overwhelmed pretty easily.</p>
<p>The <a href="https://nvd.nist.gov/vuln-metrics/cvss">Common Vulnerability Scoring System</a> (CVSS) is a standardized framework used to describe the severity of a vulnerability. CVSS v3.1 uses the following severity ranges:</p>
<table style="min-width:50px"><colgroup><col style="min-width:25px"><col style="min-width:25px"></colgroup><tbody><tr><td><p><strong>CVSS Score&nbsp;</strong></p></td><td><p><strong>Severity&nbsp;</strong></p></td></tr><tr><td><p>0.0&nbsp;</p></td><td><p>None&nbsp;</p></td></tr><tr><td><p>0.1–3.9&nbsp;</p></td><td><p>Low&nbsp;</p></td></tr><tr><td><p>4.0–6.9&nbsp;</p></td><td><p>Medium&nbsp;</p></td></tr><tr><td><p>7.0–8.9&nbsp;</p></td><td><p>High&nbsp;</p></td></tr><tr><td><p>9.0–10.0&nbsp;</p></td><td><p>Critical&nbsp;</p></td></tr></tbody></table>

<p>CVSS is useful because it provides both developers and security teams with a common language that describes the severity of a vulnerability. Nevertheless, the rating describes the vulnerability but not the environment where this vulnerability appears. In other words, CVSS doesn't tell you if the functionality used by the vulnerability is really used by your application or if the affected system is exposed to the Internet.</p>
<p>To see why CVSS alone isn't always enough, let's say we have two hypothetical vulnerabilities in an organization's environment:</p>
<ul>
<li><p><strong>Vulnerability A:</strong> This critical remote code execution vulnerability is part of an isolated testing harness or development-only dependency that's never included in the production environment and doesn't have any external network accessibility.</p>
</li>
<li><p><strong>Vulnerability B:</strong> A high-severity input validation vulnerability is found in an internet-facing API gateway that processes malicious user input and has access to a backend database with customer information.</p>
</li>
</ul>
<p>Looking at just the CVSS score would require the team to focus on Vulnerability A before Vulnerability B. But it's clear that Vulnerability B poses the greater threat to operations. Security studies show us that very few vulnerabilities get exploited once they're known. Telemetry data from the <a href="https://www.runzero.com/resources/kevology/">CISA KEV Catalog</a> clearly indicates that attackers focus on a subset of vulnerabilities that have a real path of exploitation.</p>
<p>In reality, teams must consider the CVSS score in addition to many contextual factors when deciding what to fix. A particular vulnerability might have a higher priority if the following conditions are true:</p>
<ul>
<li><p>it has an impact on an internet-facing production system,</p>
</li>
<li><p>there's a known exploit for the vulnerability,</p>
</li>
<li><p>there's sensitive information exposed,</p>
</li>
<li><p>it impacts an important business function,</p>
</li>
<li><p>or it offers an attacker a means of gaining access to other privileged systems.</p>
</li>
</ul>
<p>But vulnerabilities that occur only in development or are inaccessible for some reason likely don't need to be fixed immediately.</p>
<p>The most appropriate method for determining the importance of vulnerabilities is asking some straightforward questions: Is the vulnerable system accessible? Is the vulnerable code accessible? Is there any exploit for this vulnerability? What privileges does the affected service have? What will an attacker be able to access after exploiting the vulnerability?</p>
<p>Assigning the same level of priority to all alerts wastes engineering efforts on vulnerabilities that might pose no or little risk at all.</p>
<h2 id="heading-exposure-management-moving-from-flaw-counts-to-contextual-risk">Exposure Management: Moving from Flaw Counts to Contextual Risk</h2>
<p>Exposure management shifts focus from simply cataloging static vulnerabilities to evaluating an organization's actual operational risk posture.</p>
<p>Instead of asking "How many CVEs exist in our repositories?", exposure management asks "Which vulnerable components, misconfigurations, and reachable network paths create exploitable risk across our running assets?"</p>
<p>The difference becomes easier to see when you look at what each approach focuses on:</p>
<table style="min-width:75px"><colgroup><col style="min-width:25px"><col style="min-width:25px"><col style="min-width:25px"></colgroup><tbody><tr><td><p><strong>Dimension&nbsp;</strong></p></td><td><p><strong>Traditional Vulnerability Management&nbsp;</strong></p></td><td><p><strong>Exposure Management&nbsp;</strong></p></td></tr><tr><td><p>Primary Question&nbsp;</p></td><td><p>What software bugs and CVEs exist?&nbsp;</p></td><td><p>What paths can an attacker exploit to access critical assets?&nbsp;</p></td></tr><tr><td><p>Data Scope&nbsp;</p></td><td><p>Isolated dependency scans and static vulnerability databases&nbsp;</p></td><td><p>Code repositories, cloud runtime, network routing and IAM permissions&nbsp;</p></td></tr><tr><td><p>Prioritization Metric&nbsp;</p></td><td><p>CVSS base scores and static severity ratings&nbsp;</p></td><td><p>Reachability, exploitability, asset sensitivity and environment context&nbsp;</p></td></tr><tr><td><p>Primary Action&nbsp;</p></td><td><p>Upstream package upgrades and direct software patches&nbsp;</p></td><td><p>Risk-based triage: network isolation, configuration changes, or targeted patching&nbsp;</p></td></tr></tbody></table>

<p>When there are 10,000 cloud assets managed by an engineering environment and 1,000 vulnerable libraries found through dependency scanners, the combined numbers don't represent the actual security situation. In order to focus on the right level of risks, you should have some knowledge about the context within which each of these vulnerabilities exists.</p>
<ul>
<li><p>Is the container exposed to the internet or is it hidden behind the internal load balancer?</p>
</li>
<li><p>Is the code actually invoking the risky symbol or library function?</p>
</li>
<li><p>What are the identity permissions, cloud roles, and databases that are accessible through the vulnerable service?</p>
</li>
</ul>
<p>Having an understanding of this denominator (total numbers of assets that should receive a specific patch) helps teams identify the exposures that are actually threats so that engineering time is spent on solving the problems that impact production data.</p>
<h2 id="heading-the-dependency-tree-as-an-attack-surface">The Dependency Tree as an Attack Surface</h2>
<p>Modern software delivery depends on multi-layered packages such as npm, PyPI, Maven, NuGet, base operating system layer packages, GitHub Actions, and third-party APIs. The application logic is written by developers, but the final runtime software contains numerous levels of packages:</p>
<p>Your Application Logic → Direct Dependency (Declared in manifest) → Transitive Dependency (Pulled in automatically) → Underlying OS System Packages → Base Container Image / Cloud Runtime.</p>
<p>If a vulnerability is three levels down in the transitive dependencies and the transitive dependency is unmaintained, but can be accessed via external inputs, then that's an essential part of your application's attack surface.</p>
<p>That's why software development teams have started to use Software Bill of Materials (SBOMs). An SBOM is an inventory of software components that constitute the software or application. Depending on the technology used to create the SBOM, different information can be available including component name, version, dependencies and package ID.</p>
<p>It's helpful in cases where a new vulnerability has been identified. For example, if a vulnerability is discovered in a specific version of lodash, the security team can use its SBOMs to identify which applications or container images contain the affected version. They'll then be able to investigate if there's an exploitable exposure.</p>
<p>An SBOM alone doesn't provide security for the application. Its significance lies in increasing visibility to developers and security personnel regarding what is present in their applications.</p>
<h2 id="heading-practical-takeaways-for-developers">Practical Takeaways for Developers</h2>
<p>These principles will be relevant once you integrate them into your team's routine development process. There are some practical ways you and your team can employ these best practices and strategies:</p>
<h3 id="heading-audit-transitive-dependencies">Audit Transitive Dependencies</h3>
<p>The first thing to do is to verify what dependencies are included in your application. This is very useful when it comes to transitive dependencies, because these dependencies may have been automatically added when installing some other package directly.</p>
<p>For Node.js applications, the command <code>npm ls</code> will display the dependency tree. Python programmers may use <code>pipdeptree</code>, while Java programs created using Maven can use the <code>mvn dependency:tree</code> command. These commands can help you understand the origins of packages and the direct dependency that introduced a vulnerable transitive dependency into your project.</p>
<h3 id="heading-check-code-reachability">Check Code Reachability</h3>
<p>Finding a weak point in a dependency doesn't automatically imply that you're using it within your application. Don't take every vulnerability report as a critical production blocker. Instead, you should investigate if the impacted functionality is actually accessible from your application.</p>
<p>Suppose you find a vulnerability in a certain library function. In this case, you need to look through your codebase for any usage of this function and figure out if there's any possibility of passing user-controlled data to it. You may use either the search function provided by your IDE or command-line utilities such as grep.</p>
<p>An unused or inaccessible from the outside function reduces the urgency of the finding. But it doesn't automatically imply that you should ignore it.</p>
<h3 id="heading-generate-an-sbom-in-cicd">Generate an SBOM in CI/CD</h3>
<p>You can also produce a Software Bill of Materials using your <strong>CI/CD pipeline</strong>. Creating an SBOM will help you identify the components used in the software and make it easier to identify affected components once a vulnerability is found.</p>
<p>For instance, using Syft, you can generate an SBOM from a container image by executing the command: <code>syft my-app:latest -o cyclonedx-json &gt; sbom.json</code>.</p>
<p>This will create a CycloneDX JSON file with information on the components within the container image. This SBOM will then be stored along with the build artifacts. Once a new vulnerability is identified in a particular package version, it becomes easy for the security team to know which applications and container images contain this particular component.</p>
<h3 id="heading-use-compensating-controls-when-a-patch-isnt-ready">Use Compensating Controls When a Patch Isn't Ready</h3>
<p>Sometimes there may be no patch available or it may be too risky to implement it straight away since doing so might introduce breaking changes that need further testing. In such cases, you can use compensatory controls to lessen the exposure of the application until it's patched properly.</p>
<p>Depending on the environment, this may involve limiting the network access to the vulnerable component, isolating the workload from sensitive resources, disabling the feature that has been compromised, or minimizing the privileges of the application.</p>
<p>These controls don't take the place of the security patch but only minimize the risk of exploitation until a patch is implemented.</p>
<h3 id="heading-apply-least-privilege-at-runtime">Apply Least Privilege at Runtime</h3>
<p>Lastly, restrict the amount of access your applications have at run time. When a compromise is made due to exploitation, it prevents the spread of that breach to other applications or systems.</p>
<p>When deploying container-based applications, you can leverage read-only filesystems, as well as remove capabilities that aren't required in Linux. For instance, Docker provides the option to use <code>--read-only</code> and <code>--cap-drop=ALL</code> when running containers.</p>
<p>Cloud applications also need to adopt the same concept by ensuring the use of IAM permissions, giving access only to what the application requires.</p>
<p><strong>The goal is simple:</strong> if one element has been breached, the attacker should be able to gain access to as few components of the environment around it as possible.</p>
<p>The future of software security doesn't rely on how fast organizations can update their packages without knowing the underlying reasons for doing so. With vulnerability detection becoming more efficient through automation, effective mitigation relies on knowledge about the relationship between the source code, its dependencies, and infrastructure at runtime.</p>
<p>The purpose here isn't just finding new vulnerabilities but recognizing the ones that pose real risks to the application.</p>
<h2 id="heading-wrap-up">Wrap Up</h2>
<p>While artificial intelligence is helping teams detect vulnerabilities quicker, modern applications keep becoming increasingly dependent on numerous software layers. This doesn't mean that patching becomes unnecessary. It means that all vulnerabilities don't require the same immediate attention.</p>
<p>Developers also still need to look at where those vulnerabilities exist, whether they're reachable by attackers, and what they could affect. As the time between vulnerability detection and exploitation continues to change, understanding exposure becomes just as important as the patch itself.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Agentic AI Engineering in Practice: How AI Engineers and Forward-Deployed Engineers Build with Claude Code, Codex, and Gemini ]]>
                </title>
                <description>
                    <![CDATA[ A practical, three-tool guide to the AI-native software development life cycle (SDLC): Plan, Design, Build, Test, Deploy, Maintain, reimagined for agentic coding. In March 2025, a small nonprofit rese ]]>
                </description>
                <link>https://www.freecodecamp.org/news/agentic-ai-engineering-in-practice-how-to-build-with-claude-code-codex-and-gemini/</link>
                <guid isPermaLink="false">6a9aee151eb1bdc38db233af</guid>
                
                    <category>
                        <![CDATA[ Developer Tools ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Software Engineering ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Artificial Intelligence ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Devops ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ai agents ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Rudrendu Paul ]]>
                </dc:creator>
                <pubDate>Fri, 04 Sep 2026 16:13:09 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/e8e99bfe-2630-487f-9031-319aae986a32.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>A practical, three-tool guide to the AI-native software development life cycle (SDLC): Plan, Design, Build, Test, Deploy, Maintain, reimagined for agentic coding.</p>
<p>In March 2025, a small nonprofit research group called METR published a chart that made many engineering leaders sit up straighter than usual.</p>
<p>Working backward through six years of model releases, METR measured the length of the software task an AI agent could complete on its own, which they defined as the amount of time a skilled human professional would need for the same task, and found that number has been doubling roughly every seven months since 2019 (<a href="https://metr.org/blog/2025-03-19-measuring-ai-ability-to-complete-long-tasks/">METR</a>).</p>
<p>That curve isn't about autocomplete getting a little better: it's about agents crossing from "finishes a function" to "finishes a feature" to, on the current trajectory, "finishes a sprint."</p>
<p>The adoption numbers already reflect that shift. Google Cloud and DORA's 2025 State of AI-assisted Software Development Report found that 90 percent of developers now use AI at work and more than 80 percent say it has increased their productivity, even though roughly three in ten still report low trust in the code the models produce (<a href="https://cloud.google.com/blog/products/ai-machine-learning/announcing-the-2025-dora-report">DORA</a>).</p>
<p>Stack Overflow's 2025 Developer Survey puts a similar number on habitual use: 84 percent of developers now use or plan to use AI tools, up from 76 percent the year before, and about half of professional developers reach for one daily (<a href="https://survey.stackoverflow.co/2025/ai">Stack Overflow</a>).</p>
<p>AI-assisted coding skipped the novelty phase and arrived as the default way software gets written. At the same time, most teams still haven't updated the software development lifecycle they built for the previous default.</p>
<p>That mismatch is the subject of this piece. Anthropic's Applied AI team published a framework in 2026 called the AI-native SDLC, built around a single observation: once an agent can write and revise code faster than a human can review a pull request, the bottleneck in software delivery doesn't disappear so much as relocate (<a href="https://claude.com/blog/the-ai-native-sdlc-playbook">Anthropic</a>).</p>
<p>This guide walks through that framework stage by stage (Plan, Design, Build, Test, Deploy, Maintain), and shows you how to implement each stage with whichever agentic coding tool you have access to: Claude Code, OpenAI Codex, or Gemini CLI. You'll see configuration files, markdown artifact templates, and CI workflows for all three tools, plus a worked example of how a single forward-deployed engineer uses this pattern to cover work that used to require a five-person team.</p>
<p>This guide is one framework, implemented three ways: a practitioner's playbook grounded in config files and command output.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-is-an-ai-native-sdlc">What is an AI-Native SDLC?</a></p>
</li>
<li><p><a href="#heading-where-the-bottleneck-moves-once-code-gets-cheap">Where the Bottleneck Moves Once Code Gets Cheap</a></p>
</li>
<li><p><a href="#heading-claude-code-codex-and-gemini-cli-one-framework-three-vocabularies">Claude Code, Codex, and Gemini CLI: One Framework, Three Vocabularies</a></p>
</li>
<li><p><a href="#heading-how-to-run-the-plan-stage">How to Run the Plan Stage</a></p>
</li>
<li><p><a href="#heading-how-to-run-the-design-stage">How to Run the Design Stage</a></p>
</li>
<li><p><a href="#heading-how-to-run-the-build-stage">How to Run the Build Stage</a></p>
</li>
<li><p><a href="#heading-how-to-run-the-test-stage">How to Run the Test Stage</a></p>
</li>
<li><p><a href="#heading-how-to-run-the-deploy-stage">How to Run the Deploy Stage</a></p>
</li>
<li><p><a href="#heading-how-to-run-the-maintain-stage">How to Run the Maintain Stage</a></p>
</li>
<li><p><a href="#heading-how-one-engineer-covers-a-five-person-team">How One Engineer Covers a Five-Person Team</a></p>
<ul>
<li><p><a href="#heading-the-plan-stage">The Plan Stage</a></p>
</li>
<li><p><a href="#heading-the-design-stage">The Design Stage</a></p>
</li>
<li><p><a href="#heading-the-build-stage">The Build Stage</a></p>
</li>
<li><p><a href="#heading-the-test-stage">The Test Stage</a></p>
</li>
<li><p><a href="#heading-the-deploy-stage">The Deploy Stage</a></p>
</li>
<li><p><a href="#heading-the-maintain-stage">The Maintain Stage</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-pre-flight-checklist-before-you-go-all-in-on-an-agentic-ai-native-sdlc">Pre-flight Checklist Before You Go All-in on an Agentic AI-Native SDLC</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-what-to-explore-next">What to Explore Next</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you start, make sure you have the following:</p>
<ul>
<li><p><strong>One agentic coding CLI installed</strong>: Claude Code, OpenAI Codex, or Gemini CLI. You only need one to follow along. The sections below give you the equivalent command or file for each.</p>
</li>
<li><p><strong>Node.js 18 or later</strong> (verify with <code>node --version</code>), since all three tools distribute as npm packages.</p>
</li>
<li><p><strong>Git 2.30 or later</strong> (verify with <code>git --version</code>).</p>
</li>
<li><p><strong>A GitHub repository with Actions enabled</strong>, since the Deploy and Maintain sections use CI workflows.</p>
</li>
<li><p><strong>Basic familiarity with CI/CD concepts</strong>: pull requests, branch protection, and what a build pipeline does. You don't need deep GitHub Actions expertise.</p>
</li>
</ul>
<p>Install whichever tool you plan to use:</p>
<pre><code class="language-bash">npm install -g @anthropic-ai/claude-code
npm install -g @openai/codex
npm install -g @google/gemini-cli
</code></pre>
<p>Here's what each one gives you:</p>
<ul>
<li><p><code>claude-code</code> puts a <code>claude</code> command on your path that runs an agentic session against your local repository, with permission modes, subagents, and skills.</p>
</li>
<li><p><code>codex</code> puts a <code>codex</code> command on your path with its own sandboxing and approval model, plus a hosted cloud-task mode.</p>
</li>
<li><p><code>gemini-cli</code> puts a <code>gemini</code> command on your path, built around Extensions that bundle prompts, MCP servers, and slash commands into one installable unit.</p>
</li>
</ul>
<p>You don't need all three. Pick whichever your employer already pays for, or whichever free tier fits your project, and follow that column through the rest of this guide.</p>
<h2 id="heading-what-is-an-ai-native-sdlc">What is an AI-Native SDLC?</h2>
<p>The diagram below shows how the six phases are connected as a continuous system rather than a series of isolated steps. The following sections explain how each stage hands off a substantial named artifact to the next, with the final stage looping directly back to the beginning instead of stopping at deployment.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69cc82ffe4688e4edd796adb/1e5a3c15-389f-4b26-831f-1edb462f82c7.png" alt="Circular diagram of the AI-native software development life cycle showing six stages, Plan, Design, Build, Test, Deploy, and Maintain, arranged clockwise, with the artifact each stage hands to the next (intent.md, spec.md, plan.md, test results, review.md, bands.yaml) and a loop-closing arrow from Maintain back to Plan." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p><em>Figure 1: The AI-native</em> <em>Software Development Life Cycle (SDLC)</em> <em>drawn as a closed hexagonal loop rather than a straight pipeline. The six stages, Plan, Design, Build, Test, Deploy, Maintain, run clockwise around the outside. The artifact each stage hands to the next (</em><code>intent.md</code><em>,</em> <code>spec.md</code><em>,</em> <code>plan.md</code> <em>plus code, test results,</em> <code>review.md</code><em>,</em> <code>bands.yaml</code><em>) sits inside the loop next to the arrow it travels on. The doubled arrow from Maintain back to Plan is the detail worth noticing first: it's what turns six individual stages into one self-triggering cycle instead of six improvements that happen to sit next to each other.</em></p>
<p>If you follow along the arrows:</p>
<ul>
<li><p>Plan hands the next stage an <code>intent.md</code>.</p>
</li>
<li><p>Design hands Build a <code>spec.md</code>.</p>
</li>
<li><p>Build hands Test and Deploy a <code>plan.md</code> and eventually a diff.</p>
</li>
<li><p>Deploy hands Maintain a merged pull request with its review findings attached.</p>
</li>
<li><p>Maintain when something breaks in production, write a new <code>intent.md</code> and start the loop again. That closed loop is the innovation, more than any individual stage.</p>
</li>
</ul>
<p>A traditional SDLC diagram is usually drawn as a waterfall or a horizontal pipeline. This one is drawn as a circle because the entire point is that operational data becomes the next planning input automatically, instead of sitting in a dashboard nobody opens until the next planning offsite.</p>
<p>Engineers built the traditional six-stage software development lifecycle of plan, design, build, test, deploy, and maintain around an assumption that held for fifty years: writing and implementing code was the most expensive, most time-consuming part of the process.</p>
<p>Anthropic's Applied AI team named this assumption explicitly when it published its AI-native SDLC framework in 2026, and framed the entire model around what happens when that assumption stops being true (<a href="https://claude.com/blog/the-ai-native-sdlc-playbook">Anthropic</a>). The framework keeps the same six stage names software engineers already know. What's different is how each stage produces and consumes work.</p>
<p>Anthropic calls the mechanism artifact-driven development. Every stage commits a durable, version-controlled, machine-readable document that the next stage reads: no meeting, no Slack thread, and no shared understanding that lives only in someone's head.</p>
<ul>
<li><p>Plan produces <code>intent.md</code>, a plain description of the problem in the requester's own words.</p>
</li>
<li><p>Design produces <code>spec.md</code>, the requirements and constraints with any open concerns flagged inline.</p>
</li>
<li><p>Build produces <code>plan.md</code>, an implementation plan naming the files that will change, the order of changes, and the tests that will confirm them, followed by the diff.</p>
</li>
<li><p>Test and Deploy produce a pull request carrying multiple layers of automated review findings.</p>
</li>
<li><p>Maintain produces incident records that feed back into a new <code>intent.md</code> when something in production breaches an expected threshold.</p>
</li>
</ul>
<p>Anthropic's own phrasing captures the design intent well:</p>
<blockquote>
<p>"Every stage commits an artifact the next stage can read. Together, the intent, the spec, the plan, the diff and the review findings are the audit trail." (<a href="https://claude.com/blog/the-ai-native-sdlc-playbook">Anthropic</a>)</p>
</blockquote>
<p>That audit trail matters for a reason beyond compliance. When an agent can produce a working diff in minutes, what determines whether the software is good is whether the plan it worked from was good, and whether someone checked its output against a requirement before it shipped.</p>
<p>Artifacts make that checking possible without slowing the agent down: a spec document takes ten minutes to review and can be cached, reused, and diffed the way code is diffed. A verbal handoff can't.</p>
<h2 id="heading-where-the-bottleneck-moves-once-code-gets-cheap">Where the Bottleneck Moves Once Code Gets Cheap</h2>
<p>Anthropic titles the relevant section of its framework, "Code is no longer the bottleneck," then explains why in the next line:</p>
<blockquote>
<p>"Organizations have started using AI to write code at a speed unthinkable one year ago, yet the processes around the code haven't changed at the same pace." (<a href="https://claude.com/blog/the-ai-native-sdlc-playbook">Anthropic</a>)</p>
</blockquote>
<p>That observation is worth walking through slowly, because it's the part of the framework that changes how you organize your day, beyond which tool you buy. The chart below puts a number on that reallocation across a sprint's calendar time.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69cc82ffe4688e4edd796adb/45220555-3142-421f-90e1-3fa7994af3ab.png" alt="Two stacked timeline bars comparing a traditional software development life cycle to an AI-native one. The traditional bar splits time evenly across six stages. The AI-native bar shows Build and Test compressed into thin slivers while Plan, Design, and Deploy stay wide, illustrating how the bottleneck shifts away from the build phase." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p><em>Figure 2: Two stacked timelines, drawn to the same total width, showing how a sprint's calendar time gets reallocated. The top bar, Traditional SDLC, splits time into six roughly equal segments. The bottom bar, AI-native SDLC, keeps Plan, Design, and Deploy wide (labeled "stays human-paced") while Build and Test collapse into thinner slivers (labeled "compresses to hours"). The two bars are the same overall length on purpose: the point isn't that everything gets faster. It's that the time that used to go into Build now has to go somewhere, and that somewhere is Plan and Review.</em></p>
<p>The diagram above shows the traditional SDLC's time allocation next to the AI-native one. In the traditional model, the Build bar dominates the chart: most of a sprint's calendar time goes into writing and debugging code, while Plan, Test, and Deploy are comparatively thin. In the AI-native version, Build shrinks to a sliver, an agent can produce a working implementation in the time it used to take to schedule the kickoff meeting, and the bars for Plan, Test/Review, and Deploy grow to fill the space Build used to occupy. The chart's total width barely changes. What's changing is which stages are now doing the rate-limiting work.</p>
<p>Anthropic's own framing names the same three stages:</p>
<blockquote>
<p>"The bottleneck moves to the steps to the left and right of the build phase. This is mainly plan, review/test, and deploy, which still run at human speed." (<a href="https://claude.com/blog/the-ai-native-sdlc-playbook">Anthropic</a>)</p>
</blockquote>
<p>That's a specific, falsifiable claim, and it's worth taking seriously instead of writing it off as a slogan. Speed at Build breaks in three specific, avoidable ways:</p>
<ul>
<li><p><strong>Fast Build, vague Plan.</strong> Because there's now less time between "wrong" and "shipped" to notice, an agent confidently implementing the wrong thing at high speed is counterintuitively worse than a human making the same mistake slowly.</p>
</li>
<li><p><strong>Fast Build, unscaled Test and Review.</strong> Nobody redesigned review to handle the higher volume of changes a fast agent produces, so either review quality drops or reviewers become the new queue, and the speed gain evaporates when a human has to read the diff.</p>
</li>
<li><p><strong>Fast Build, manual Deploy.</strong> A human still has to promote a build through three environments by hand, so the agent produces work faster than the organization can absorb it.</p>
</li>
</ul>
<p>The argument here is that the stages surrounding the agent, more than the agent itself, are where an AI-native SDLC earns its name. Agentic coding tools aren't the problem. The rest of this guide treats Plan, Design, Test, Deploy, and Maintain with the same engineering rigor teams have historically reserved for Build, because that's where the constraint lives now.</p>
<h2 id="heading-claude-code-codex-and-gemini-cli-one-framework-three-vocabularies">Claude Code, Codex, and Gemini CLI: One Framework, Three Vocabularies</h2>
<p>Every stage below gives you a command or file for Claude Code, Codex, and Gemini CLI side by side: that only works once you know what each tool calls the mechanism you're about to use. All three vendors ship a genuinely capable agentic coding tool, and the right one for you is largely the one your employer licenses or the one whose free tier matches your workload.</p>
<p>The table below is the reference to come back to while reading the stage-by-stage sections.</p>
<table>
<thead>
<tr>
<th>Capability</th>
<th>Claude Code</th>
<th>OpenAI Codex</th>
<th>Gemini CLI</th>
</tr>
</thead>
<tbody><tr>
<td>Memory/context file</td>
<td><code>CLAUDE.md</code>, at the project root, <code>~/.claude/</code>, or <code>.claude/</code> (<a href="https://code.claude.com/docs/en/memory">Anthropic</a>)</td>
<td><code>AGENTS.md</code>, walked from the Codex home directory down to the project root (<a href="https://learn.chatgpt.com/docs/agent-configuration/agents-md">OpenAI</a>)</td>
<td><code>GEMINI.md</code>, concatenated across global, project, and subdirectory levels (<a href="https://geminicli.com/docs/cli/gemini-md/">Google</a>)</td>
</tr>
<tr>
<td>Reusable prompt/extension system</td>
<td>Subagents (own context window, restricted tools) plus Skills, folder-based and auto-invoked (<a href="https://code.claude.com/docs/en/sub-agents">Anthropic</a>, <a href="https://code.claude.com/docs/en/skills">Anthropic</a>)</td>
<td>Skills, which supersede the now-deprecated custom prompts. MCP servers handle external tool access separately.(<a href="https://learn.chatgpt.com/docs/custom-prompts">OpenAI</a>)</td>
<td>Extensions bundle prompts, MCP servers, slash commands, hooks, and subagents into one installable unit (<a href="https://geminicli.com/docs/extensions/">Google</a>)</td>
</tr>
<tr>
<td>Approval/sandbox model</td>
<td>Permission modes: Manual, Auto, and Plan mode, switched with Shift+Tab (<a href="https://code.claude.com/docs/en/permission-modes">Anthropic</a>)</td>
<td>Two independent axes: sandbox mode (read-only, workspace-write, danger-full-access) and approval policy (untrusted, on-request, never) (<a href="https://learn.chatgpt.com/docs/agent-approvals-security">OpenAI</a>)</td>
<td>Approval modes: default, auto_edit, yolo (<code>--yolo</code> or Ctrl+Y), and a still-maturing plan mode (<a href="https://github.com/google-gemini/gemini-cli/blob/main/docs/reference/configuration.md">Google</a>)</td>
</tr>
<tr>
<td>First-party CI action</td>
<td><code>anthropics/claude-code-action</code>, triggered by <code>@claude</code> mentions or scheduled events (<a href="https://code.claude.com/docs/en/github-actions">Anthropic</a>)</td>
<td><code>openai/codex-action</code>, runs <code>codex exec</code> inside a CI job and can apply patches or post reviews (<a href="https://learn.chatgpt.com/docs/github-action">OpenAI</a>)</td>
<td><code>google-github-actions/run-gemini-cli</code>, triggered by PR and issue events (<a href="https://github.com/google-github-actions/run-gemini-cli">Google</a>)</td>
</tr>
<tr>
<td>Native PR code review</td>
<td>Code Review: a managed, multi-agent service with a local<code>/code-review</code> command and severity-tagged inline comments (<a href="https://code.claude.com/docs/en/code-review">Anthropic</a>)</td>
<td><code>/review</code> in the CLI, <code>@codex review</code> on GitHub, or an "automatic reviews" setting that flags P0/P1 issues on every new PR (<a href="https://learn.chatgpt.com/docs/third-party/github">OpenAI</a>)</td>
<td>Gemini Code Assist for GitHub, configurable through a checked-in<code>.gemini/config.yaml</code> file across five review dimensions (<a href="https://docs.cloud.google.com/gemini/docs/code-review/style-guide">Google</a>)</td>
</tr>
<tr>
<td>Always-on chat surface</td>
<td>Claude Tag in Slack, a shared org identity that routes coding intent to Claude Code on the web (<a href="https://code.claude.com/docs/en/slack">Anthropic</a>)</td>
<td>An official Codex Slack app:<code>@Codex</code> in a channel, creates a cloud task and posts results back (<a href="https://slack.com/marketplace/A09F5C369E3-openai-codex">Slack</a>)</td>
<td>No confirmed first-party Slack-native identity as of this writing. Only third-party bridges exist.</td>
</tr>
</tbody></table>
<p>A few things stand out when you lay the mechanisms side by side. All three tools now converge on the same core idea: a plain-text memory file the agent reads before doing anything else, a packaging system for reusable prompts and tools, an approval layer that decides how much autonomy the agent gets, and a first-party GitHub Action for running the agent in CI.</p>
<p>Once Build stops being the constraint, every vendor has to build the same surrounding infrastructure or their tool becomes fast but unmanageable.</p>
<p>The one place the three tools are not symmetric is the last row. Claude Code and Codex each ship an official, vendor-built Slack presence with a persistent tag identity that turns a channel mention into an asynchronous coding task. Gemini CLI doesn't have a documented equivalent as of this research (only community-built bridges connect it to Slack).</p>
<p>If your Maintain-stage workflow depends on an agent picking up an incident directly from a chat mention, that's a capability gap to plan around rather than a preference.</p>
<p>Because it undercuts the idea that you have to pick a tool and live with it forever, one more piece of interoperability is worth calling out: Claude Code's own memory documentation describes importing an existing <code>AGENTS.md</code> file with an <code>@AGENTS.md</code> reference or a symlink, so a Claude Code session can read the same conventions file a Codex session already uses (<a href="https://code.claude.com/docs/en/memory">Anthropic</a>).</p>
<p>AGENTS.md itself has become a cross-vendor open standard, adopted well beyond Codex, and stewarded outside any single company (<a href="https://learn.chatgpt.com/docs/agent-configuration/agents-md">OpenAI</a>). A team that standardizes its conventions file on AGENTS.md and has Claude Code import it gets most of the benefit of a single shared memory file, regardless of which tool an individual engineer prefers that day.</p>
<h2 id="heading-how-to-run-the-plan-stage">How to Run the Plan Stage</h2>
<p>The Plan stage answers one question before any code gets touched: what's the problem, in the words of the person who has it?</p>
<p>Anthropic's framework calls the artifact this stage produces <code>intent.md</code>, and it's deliberately unglamorous. It's not a Jira ticket with acceptance criteria already reverse-engineered from a solution. It's closer to a transcript: what the requester said they needed, in their own language, captured before an engineer or an agent starts interpreting it.</p>
<p>Here's a minimal <code>intent.md</code> template you can commit to a repository and reuse for every new piece of work:</p>
<pre><code class="language-markdown"># intent.md

## Requested by
Name, role, date

## What they said
Paste the raw request. Do not clean it up yet. If it came from a support
ticket, a Slack thread, or an incident, link it.

## What problem this solves
One or two sentences, written after the raw request above, translating it
into a problem statement. This is the first place interpretation is allowed.

## Why now
What triggered this request. If it came out of an incident, name the
incident record it traces back to.

## Constraints already known
Anything the requester specified: deadline, budget, systems that cannot
change, regulatory requirement.

## Explicitly out of scope
What this request does not include, stated as what it does.
</code></pre>
<p>Here's what's happening:</p>
<ul>
<li><p>The "what they said" section is deliberately unedited, so the next stage can catch a misread request before it propagates</p>
</li>
<li><p>Separating "what they said" from "what problem this solves" forces the interpretation step to happen once, in writing, instead of inside whoever reads the request next</p>
</li>
<li><p>The "why now" field is what closes the loop from the Maintain stage: an intent that originated from a production incident should say so explicitly, linking back to the incident record that triggered it</p>
</li>
</ul>
<p>Filled in for a request, the same template stays this short:</p>
<pre><code class="language-markdown"># intent.md

## Requested by
Maria, independent contractor and beta user, 2026-08-14

## What they said
"I have to check four different calendars every morning before I can tell
a client when I'm free. I've double-booked myself twice this month."

## What problem this solves
Contractors working across multiple clients cannot see a unified view of
their own availability without giving each client's calendar system
access to the others.

## Why now
Direct customer feedback during the private beta, not a production
incident.

## Constraints already known
Beta ships in six weeks. No budget for a dedicated calendar-sync vendor.

## Explicitly out of scope
Two-way sync or write access to any client's calendar. Read-only overlay only, for this release.
</code></pre>
<p>A spec written from this intent would name the technical shape: which calendar providers to support first, how the overlay handles conflicting time zones, and what happens when a provider's API is unavailable. Notice how little interpretation is left for Build to improvise. That's the entire point of writing the intent down before touching a design document, let alone code.</p>
<p>Each tool gives you a different mechanism for working through this stage without letting an agent jump straight to code. Claude Code's Plan mode is a dedicated, read-only permission mode built for this scenario: the agent can research the codebase and propose an approach, but it can't edit files or run commands until you switch out of it, toggled with Shift+Tab (<a href="https://code.claude.com/docs/en/permission-modes">Anthropic</a>).</p>
<p>Codex separates the same idea into two independent settings rather than one mode switch: a sandbox setting that controls what the agent is technically capable of touching (read-only, workspace-write, or danger-full-access) and an approval policy that controls when it has to stop and ask (untrusted, on-request, or never) (<a href="https://learn.chatgpt.com/docs/agent-approvals-security">OpenAI</a>).</p>
<p>Setting the sandbox to read-only during Plan gets you the same guarantee Claude Code's Plan mode gives you, enforced at a different layer. Gemini CLI has a <code>plan</code> approval mode with the same read-only intent, though Google's own documentation flags it as still maturing relative to its other approval modes (<a href="https://github.com/google-gemini/gemini-cli/blob/main/docs/reference/configuration.md">Google</a>), so treat it as directionally useful rather than a hard guarantee until you've verified its behavior against your own repository.</p>
<p>The output of a Plan-stage session, regardless of which tool ran it, should be the filled-in <code>intent.md</code> plus a short back-and-forth confirming the agent's summary of the problem matches what the requester meant. That confirmation step is the human-speed part of the stage the earlier section warned you about, and it's tempting to skip when the agent's summary already sounds right. Skipping it because the agent produced a plausible-sounding summary quickly is the failure mode the bottleneck-shift argument predicts: fast, confident, but wrong.</p>
<h2 id="heading-how-to-run-the-design-stage">How to Run the Design Stage</h2>
<p>Design is where <code>intent.md</code> becomes <code>spec.md</code>, a document that names the technical shape of the solution, the interfaces it touches, and the tradeoffs someone has to sign off on before an agent starts writing implementation code.</p>
<p>Anthropic's framework describes this stage as one where "requirements and design collapse into one session" (<a href="https://claude.com/blog/the-ai-native-sdlc-playbook">Anthropic</a>), which is a meaningful change from a traditional process where a product spec and a technical design document are often written by different people, days apart, with a meeting in between to reconcile them.</p>
<p>A useful <code>spec.md</code> template keeps that collapse explicit rather than accidental:</p>
<pre><code class="language-markdown"># spec.md

## Source
Link to the intent.md this spec answers.

## Approach
Plain description of the technical approach: which systems change, which
stay the same, and why this approach over the obvious alternative.

## Interfaces affected
API endpoints, database schemas, public function signatures. Anything
another team or another service depends on.

## Open concerns
Anything the agent or the author is not confident about. This section
exists specifically so uncertainty gets written down instead of quietly
resolved by whichever choice was easiest to implement.

## Explicitly rejected alternatives
What else was considered and why it lost. This is what keeps the next
person from re-litigating a decision six months from now.

## Sign-off
Who reviewed this and on what date.
</code></pre>
<p>The "open concerns" and "explicitly rejected alternatives" sections are doing the work here. An agent asked to produce a design document will, by default, present its chosen approach as obvious. A human reviewer's job at this stage is narrow but specific: read those two sections first, because a wrong assumption is far more likely to be hiding there than in the parts of the document the agent is most confident about.</p>
<p>All three tools support this stage the same way they support Plan: keep the session in a read-only or plan-style mode while the spec gets drafted, and switch to a mode that can write files only after a human has read the "open concerns" section and either resolved or explicitly accepted each item. The mechanism differs (Claude Code's Plan mode, Codex's read-only sandbox, Gemini CLI's plan approval mode), but the discipline is identical across all three: nothing gets implemented from a spec until someone has reviewed its open concerns.</p>
<p>One practical note to build into your process: version the spec alongside the code, the same way you'd version a model card alongside the model it documents. A <code>spec.md</code> that lives only in a chat transcript isn't an artifact. A <code>spec.md</code> committed to the repository, in the same pull request as the implementation it describes, is one your Test and Deploy stages can reference later.</p>
<h2 id="heading-how-to-run-the-build-stage">How to Run the Build Stage</h2>
<p>Build is the stage everyone already associates with agentic coding tools, and it's also the stage that changes the least in this framework, because the tools were already built to do this part well.</p>
<p>What changes is that Build now runs from an approved <code>plan.md</code>, rather than from an ad hoc prompt, which is what keeps a fast agent pointed at the right target instead of an interesting-but-wrong one.</p>
<p>A <code>plan.md</code> names the specific files that will change, the order changes happen in, and the tests that confirm each change:</p>
<pre><code class="language-markdown"># plan.md

## Source
Link to spec.md this plan implements.

## Files to change, in order
1. `src/models/user.py`: add the `last_login_at` field
2. `src/api/auth.py`: update login handler to set the new field
3. `tests/test_auth.py`: add coverage for the new field
4. `migrations/0042_add_last_login.py`: schema migration

## Tests that must pass before this plan is considered done
- Existing auth test suite, unmodified tests still green
- New test: login sets last_login_at to the current UTC timestamp
- New test: last_login_at is null for a user who has never logged in

## Rollback
How to revert if this ships broken: a single migration down-step and a
git revert of the three code changes, no data backfill required.
</code></pre>
<p>The memory file each tool reads before touching any of this is what governs how the agent writes the code, not the plan alone. A <code>CLAUDE.md</code> at the root of a repository might look like this:</p>
<pre><code class="language-markdown"># CLAUDE.md

## Commands
- Run tests: `pytest tests/ -x -q`
- Run linter: `ruff check src/`
- Start local server: `python manage.py runserver`

## Codebase layout
Django monolith. Business logic lives in `src/services/`, not in views or
models. Views call services; services call models. Do not put business
logic directly in a view.

## Standards
- All new API endpoints require a corresponding entry in `openapi.yaml`
- Database migrations are one change per file, never bundled
- No new dependencies without an entry in `docs/decisions/`

## Test coverage
Every new function in `src/services/` needs a corresponding test in
`tests/services/`. Coverage below 85% fails CI.
</code></pre>
<p>If your project already standardized on <code>AGENTS.md</code> because it's the cross-vendor format, the file above is nearly a direct port: same structure, same content, different filename, and Codex will read it automatically as it walks from the Codex home directory down to your project root (<a href="https://learn.chatgpt.com/docs/agent-configuration/agents-md">OpenAI</a>).</p>
<p>A <code>GEMINI.md</code> version is the same content again, and Gemini CLI concatenates it with any global <code>~/.gemini/GEMINI.md</code> and subdirectory-level files it finds, so a monorepo can layer a company-wide convention file with per-service overrides (<a href="https://geminicli.com/docs/cli/gemini-md/">Google</a>).</p>
<p>Whichever filename you commit to, the content (commands, architecture, conventions, testing rules) is the part that determines whether the agent's output looks like your codebase or like a generic tutorial.</p>
<p>Reusable behavior beyond a single memory file is where the three tools diverge more visibly. Claude Code splits this into two mechanisms: Subagents, which run in their own context window with restricted tool access for a narrow job like "review this diff for SQL injection," and Skills, folder-based packages that Claude invokes automatically when they're relevant, which absorbed what used to be custom slash commands (<a href="https://code.claude.com/docs/en/sub-agents">Anthropic</a>, <a href="https://code.claude.com/docs/en/skills">Anthropic</a>).</p>
<p>Codex is mid-migration on the same idea: its older custom prompts mechanism is now explicitly deprecated in favor of Skills, which Codex can invoke implicitly and share across a team through the repository (<a href="https://learn.chatgpt.com/docs/custom-prompts">OpenAI</a>).</p>
<p>Gemini CLI takes the broadest approach of the three, packaging prompts, MCP servers, custom slash commands, hooks, and subagents into a single installable Extension, rather than keeping each mechanism as a separately configured feature (<a href="https://geminicli.com/docs/extensions/">Google</a>).</p>
<p>None of these are strictly better than the others. Claude Code and Codex give you finer-grained control over which mechanism does what. Gemini CLI gives you one bundle to install and share, which matters when your team's problem is getting five engineers onto the same conventions, since juggling five separately configured features invites drift.</p>
<p>Every one of the three tools also connects to external systems, databases, ticket trackers, and design tools, through the Model Context Protocol, which Anthropic created and open-sourced as a native, first-class part of Claude Code (<a href="https://www.anthropic.com/news/model-context-protocol">Anthropic</a>) and which has since become a genuinely cross-vendor standard. Codex supports it through its own MCP client, and Gemini CLI supports it with OAuth 2.0 for remote servers.</p>
<p>MCP is worth treating as infrastructure you configure once per project rather than a tool-specific feature, precisely because all three tools now speak it.</p>
<h2 id="heading-how-to-run-the-test-stage">How to Run the Test Stage</h2>
<p>Anthropic frames this stage as "continuous evals woven through implementation" (Anthropic), a deliberate contrast with a traditional model where testing is a phase that starts after Build finishes.</p>
<p>When an agent can produce a diff in minutes, waiting for a separate testing phase means the queue in front of testing grows faster than any team can review. The fix is to make verification a property of every commit the agent produces rather than a gate a human remembers to run afterward.</p>
<p>This is also the stage where the DORA report's less flattering finding becomes relevant: alongside the 90 percent adoption number, roughly three in ten developers still say they have low trust in AI-generated code (<a href="https://cloud.google.com/blog/products/ai-machine-learning/announcing-the-2025-dora-report">DORA</a>). That distrust is rational when testing is a separate phase bolted on after a fast Build stage, because nobody has verified the code yet when someone has to trust it. But it becomes a solvable engineering problem rather than a standing risk once verification runs on every commit instead of waiting for a human to schedule it.</p>
<p>Claude Code implements this with Hooks: shell commands that fire automatically at specific lifecycle events, like right before or right after the agent edits a file or runs a command (<a href="https://code.claude.com/docs/en/hooks-guide">Anthropic</a>). A hook that runs your test suite after every file edit and blocks the agent from proceeding on failure looks like this in <code>.claude/settings.json</code>:</p>
<pre><code class="language-json">{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "pytest tests/ -x -q --timeout=60"
          }
        ]
      }
    ]
  }
}
</code></pre>
<p>Here's what's happening:</p>
<ul>
<li><p><code>PostToolUse</code> fires the hook after the agent finishes an Edit or Write tool call, not before, so it's checking changes on disk.</p>
</li>
<li><p><code>pytest -x</code> stops at the first failure, which keeps the feedback loop short instead of dumping a full failure report the agent has to parse.</p>
</li>
<li><p>A blocking exit code from this command stops the agent from moving on to the next planned file in <code>plan.md</code> until the test suite is green again.</p>
</li>
</ul>
<p>Codex and Gemini CLI don't document a general-purpose local hooks framework with the same maturity as Claude Code's (and if you've come to rely on stopping an agent mid-session on your own laptop). It's less a missing feature than a different point in the pipeline: both tools build their continuous-verification story primarily around CI rather than a local lifecycle-event system, so Codex and Gemini CLI's <code>/review</code> and PR-triggered review products (covered below, under Deploy) catch the same class of problem once the diff reaches a pull request.</p>
<p>If you're running Codex or Gemini CLI locally today, the practical substitute is a pre-commit hook wired through Git itself that calls the same test command a Claude Code hook would call. This gives you most of the same guarantee at a different layer of the toolchain.</p>
<p>The artifact this stage should leave behind, regardless of tool, is a test result attached to the specific commit in <code>plan.md</code> that it verifies, so a reviewer three stages later can see which test proved which claim, rather than trusting a green checkmark that might be testing last week's code.</p>
<h2 id="heading-how-to-run-the-deploy-stage">How to Run the Deploy Stage</h2>
<p>Anthropic describes this stage as "layers of agentic review with human review reserved for regulated and critical code," where "governance is enforced as the AI acts, with hooks as approval gates." (<a href="https://claude.com/blog/the-ai-native-sdlc-playbook">Anthropic</a>)</p>
<p>The word layers does work: the framework doesn't propose replacing human review with agent review. It proposes stacking automated review as an earlier, cheaper layer, so humans focus on findings that survived automated scrutiny rather than catching everything from scratch.</p>
<p>All three vendors now ship a first-party GitHub Action, so you don't have to hand-build a CI integration from scratch. Claude Code's <code>anthropics/claude-code-action</code> responds to <code>@claude</code> mentions in a PR or issue, or runs on any GitHub event or schedule you configure (<a href="https://code.claude.com/docs/en/github-actions">Anthropic</a>):</p>
<pre><code class="language-yaml">name: Claude Code Review
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  claude-review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          prompt: "Review this diff against spec.md and plan.md for this PR."
</code></pre>
<p>Codex's equivalent, <code>openai/codex-action</code>, runs <code>codex exec</code> inside the CI job itself, which means the agent has the full CLI's capability, not a stripped-down review-only mode, and can apply patches or post a review comment depending on how you configure the step (<a href="https://learn.chatgpt.com/docs/github-action">OpenAI</a>):</p>
<pre><code class="language-yaml">name: Codex Review
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  codex-review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: openai/codex-action@v1
        with:
          openai_api_key: ${{ secrets.OPENAI_API_KEY }}
          command: "review this diff against the linked spec.md and flag P0/P1 issues"
</code></pre>
<p>Gemini's <code>google-github-actions/run-gemini-cli</code> fires on the same PR and issue events and runs with full project context asynchronously, rather than as a synchronous blocking check (<a href="https://github.com/google-github-actions/run-gemini-cli">Google</a>):</p>
<pre><code class="language-yaml">name: Gemini Review
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  gemini-review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: google-github-actions/run-gemini-cli@v1
        with:
          gemini_api_key: ${{ secrets.GEMINI_API_KEY }}
          prompt: "Review this pull request for correctness, efficiency, and maintainability."
</code></pre>
<p>Beyond the raw CI action, each vendor also ships a dedicated code review product with its own configuration surface. Claude Code's Code Review is a managed service that runs multiple specialized agents against a diff in parallel. It includes a separate verification step to filter out false positives before anything reaches a human as an inline PR comment, triggered by <code>@claude review</code> or the local <code>/code-review</code> command (<a href="https://code.claude.com/docs/en/code-review">Anthropic</a>).</p>
<p>Codex's review surface is the <code>/review</code> command in the CLI composer, an <code>@codex review</code> mention on a GitHub PR, or an "automatic reviews" setting that runs on every new PR without a mention. In GitHub mode, it deliberately restricts itself to flagging only the most severe P0 and P1 issues rather than every stylistic nit (<a href="https://learn.chatgpt.com/docs/third-party/github">OpenAI</a>).</p>
<p>Unlike the other two, Gemini Code Assist for GitHub is configured primarily through a checked-in file, <code>.gemini/config.yaml</code>, a separate surface from the memory file that governs Build. It posts a summary comment plus inline comments across five review dimensions: correctness, efficiency, maintainability, security, and a miscellaneous catch-all covering testing, scalability, and error logging (<a href="https://docs.cloud.google.com/gemini/docs/code-review/style-guide">Google</a>).</p>
<p>The human gate belongs where these automated layers hand off to a person, and where that point sits should depend on the blast radius of the change, and is not a blanket rule. A change to a database migration, an auth flow, or anything touching payments should require human approval, no matter how clean the automated review looks.</p>
<p>A documentation fix or a config value bump that passed every automated layer is a reasonable candidate for auto-merge. Anthropic frames the diff itself as part of the audit trail: "the chain of commits is also the audit trail: who asked for what, what the agent produced, and who approved it" (<a href="https://claude.com/blog/the-ai-native-sdlc-playbook">Anthropic</a>).</p>
<p>That's the useful mental model: the diff isn't done when the agent stops writing code. It's done when it has accumulated the review evidence a human needs to make a fast, informed approval decision.</p>
<h2 id="heading-how-to-run-the-maintain-stage">How to Run the Maintain Stage</h2>
<p>Maintain is the stage that gives artifact-driven development its name, because that's where the loop closes. Anthropic describes it as "agents monitor live deployments. Any breached control band is diagnosed and written back into the loop as a new <code>intent.md</code>." (<a href="https://claude.com/blog/the-ai-native-sdlc-playbook">Anthropic</a>)</p>
<p>Without that write-back step, Maintain is just monitoring, the same dashboards teams have run for a decade. With it, a production incident becomes the direct input to the next Plan-stage session, instead of a postmortem doc that gets read once and filed away.</p>
<p>A practical way to encode this is a bands-style configuration that defines the acceptable range for a metric and what happens when it's breached:</p>
<pre><code class="language-yaml"># monitoring/bands.yaml

- metric: p99_latency_ms
  service: checkout-api
  band: [0, 400]
  on_breach:
    severity: high
    action: open_incident
    write_intent: true

- metric: error_rate_pct
  service: checkout-api
  band: [0, 1.0]
  on_breach:
    severity: critical
    action: page_oncall
    write_intent: true

- metric: daily_active_users
  service: onboarding-flow
  band: [800, null]
  on_breach:
    severity: medium
    action: open_incident
    write_intent: false
</code></pre>
<p>Here's what's happening:</p>
<ul>
<li><p>Each metric has a band, an acceptable range, rather than a single threshold, which lets you catch a value that has dropped too low as easily as one that has risen too high.</p>
</li>
<li><p><code>write_intent: true</code> is the mechanism that turns a breach into the start of a new Plan-stage cycle automatically, generating a draft <code>intent.md</code> that names the breached metric, the service, and links to the incident.</p>
</li>
<li><p>Not every breach should open a new intent. A dip in daily active users on a low-severity service might warrant an incident for visibility without spinning up new planning work, which is why that flag is explicit rather than assumed.</p>
</li>
</ul>
<p>Where an agent receives the page or the mention matters here: this is the one place the three tools aren't equivalent. Claude Tag gives an organization a shared <code>@Claude</code> identity in Slack that works asynchronously in a channel and routes a mentioned coding task to Claude Code on the web. This makes "someone tags the bot in the incident channel with the failing metric" a workable Maintain-stage pattern out of the box (<a href="https://code.claude.com/docs/en/slack">Anthropic</a>).</p>
<p>OpenAI ships the direct equivalent: an official Codex Slack app where <code>@Codex</code> in a channel or thread creates a cloud task, works in the relevant repository, and posts results back into the same thread it was mentioned in (<a href="https://slack.com/marketplace/A09F5C369E3-openai-codex">Slack</a>).</p>
<p>As covered above, no first-party Google equivalent exists yet; only third-party and community-built bridges connect Gemini CLI to Slack today. The practical workaround for a Gemini-based Maintain stage is routing automation through your existing on-call paging tool's webhook rather than waiting on a chat mention (worth setting up before you're mid-incident and reaching for a bot that isn't there).</p>
<p>Trace that whole write-back mechanism from one breach to the next planning cycle, and it looks like the diagram below:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69cc82ffe4688e4edd796adb/8ad362f1-dcef-4bd6-a223-660410089068.png" alt="Flowchart of the artifact-driven feedback loop: intent.md feeds spec.md, which feeds plan.md, which feeds an agent build, then automated test and review gates, then deploy, then monitoring, with a dashed blue arrow looping back from monitoring to a newly generated intent.md to trigger the next cycle." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p><em>Figure 3: A one-way pipeline drawn as a closed loop instead. Reading left to right and down,</em> <code>intent.md</code> <em>feeds</em> <code>spec.md</code><em>,</em> <code>spec.md</code> <em>feeds</em> <code>plan.md</code><em>,</em> <code>plan.md</code> <em>feeds an agent build, which flows into automated test and review gates, then Deploy, then Monitoring. The dashed blue return arrow, labeled "triggers next cycle," is the part most teams' processes are missing: it routes a monitoring breach straight back into a freshly generated</em> <code>intent.md</code><em>, closing the loop instead of ending at Deploy, as a traditional pipeline diagram would.</em></p>
<p>The diagram above is the payoff of everything in this section: it traces a single breach in <code>bands.yaml</code> through <code>open_incident</code>, into an incident record, into a freshly generated <code>intent.md</code>, and back into the Plan stage this guide started with. That arrow, from Maintain back to Plan, is the one line most teams' engineering processes are missing today, even ones that have adopted an agentic coding tool for the Build stage.</p>
<p>Buying a fast agent for Build without building this feedback arrow gets you fast code (and the same slow, manual incident-to-roadmap process every team already had). The arrow is what makes the six stages a cycle instead of six separate improvements that happen to sit next to each other.</p>
<h2 id="heading-how-one-engineer-covers-a-five-person-team">How One Engineer Covers a Five-Person Team</h2>
<p>Everything above assumes a team large enough to have a dedicated person for planning, one for review, one for release management, and one for on-call. But a growing number of the engineers reading this don't have that team.</p>
<p>GitHub's Octoverse 2025 report found that nearly 80 percent of developers who joined GitHub in the past year used Copilot within their first week, which suggests that AI-assisted development is more and more becoming the default entry point for a new engineer's career, rather than an advanced technique layered on top of years of experience (<a href="https://github.blog/news-insights/octoverse/octoverse-a-new-developer-joins-github-every-second-as-ai-leads-typescript-to-1/">GitHub</a>).</p>
<p>Combine that with Stack Overflow's finding that roughly half of professional developers already use an AI tool daily (<a href="https://survey.stackoverflow.co/2025/ai">Stack Overflow</a>), and the engineers seriously considering a solo or two-person startup with this stack aren't a fringe case. It's close to the median new developer.</p>
<p>Here's what the six stages look like when one person, or a founding pair, runs all of them, using a worked example: a solo engineer building a scheduling tool for independent contractors, aiming to ship a paid beta in six weeks.</p>
<h3 id="heading-the-plan-stage">The Plan Stage:</h3>
<p><strong>Plan</strong> replaces a product manager's job of turning a customer conversation into a spec. The founder talks to three contractors about why existing scheduling tools frustrate them, pastes the raw notes into <code>intent.md</code>, and runs a Plan-mode session to turn three separate rambling conversations into one problem statement: contractors need to see all of their client calendars overlaid without giving each client platform admin access to the others.</p>
<p>That fifteen-minute session replaces what a two-person team would spend a week doing across customer interviews and a requirements doc.</p>
<h3 id="heading-the-design-stage">The Design Stage:</h3>
<p><strong>Design</strong> replaces an architect's whiteboard session. The same session, still in a read-only mode, produces <code>spec.md</code>: a calendar-overlay service, OAuth against each client's calendar provider, a single unified view, explicitly rejecting a real-time sync in favor of a five-minute polling interval for the beta because real-time sync was the thing most likely to blow the six-week deadline. Writing "explicitly rejected: real-time sync, because of timeline" into the spec is what stops the founder from relitigating that decision under pressure in week five.</p>
<h3 id="heading-the-build-stage">The Build Stage:</h3>
<p><strong>Build</strong> replaces a full engineering team. With <code>plan.md</code> naming the calendar integration, the auth flow, and the unified view component in order, an agentic coding tool implements each piece against a <code>CLAUDE.md</code> (or <code>AGENTS.md</code>, or <code>GEMINI.md</code>) that encodes the stack decisions the founder made once: which calendar library, which auth pattern, and where business logic lives. Every reader of this guide already expected this part to be fast. Whether the beta ships on time depends on the parts around it.</p>
<h3 id="heading-the-test-stage">The Test Stage:</h3>
<p><strong>Test</strong> replaces a QA engineer. Because a hook runs the test suite after every file edit, the founder never debugs a week's worth of untested agent output the night before a demo. The discipline of writing the test criteria into <code>plan.md</code> before Build starts, rather than testing after the fact, is what a dedicated QA engineer would've insisted on.</p>
<h3 id="heading-the-deploy-stage">The Deploy Stage:</h3>
<p><strong>Deploy</strong> replaces a release manager. A GitHub Action running an automated code review on every pull request, with a human gate specifically on anything touching the OAuth flow or billing, gives the founder the layered review Anthropic's framework describes without a second engineer to pair with. The founder still reads every diff that touches money or credentials personally.</p>
<h3 id="heading-the-maintain-stage">The Maintain Stage:</h3>
<p><strong>Maintain</strong> replaces an SRE on-call rotation. A <code>bands.yaml</code> watching API error rate and polling job success rate, wired to page the founder's phone directly rather than a shared on-call tool nobody is rotating through, is the entire incident response function for a company this size. When the polling job's error rate breaches its band at 2 a.m., the resulting incident record becomes next week's first <code>intent.md</code> instead of a bug the founder half-remembers by Monday.</p>
<p>Judgment still matters as much as it always did. What disappears is the coordination overhead that used to require a team: the committed file now holds explicitly what a team of specialists used to carry implicitly in separate heads.</p>
<p>That's the argument for bootstrapping with an AI-native SDLC: artifact-driven development lets one person's expertise cover ground that used to require distributing it across several people's job titles.</p>
<h2 id="heading-pre-flight-checklist-before-you-go-all-in-on-an-agentic-ai-native-sdlc">Pre-flight Checklist Before You Go All-in on an Agentic AI-Native SDLC</h2>
<p>Before restructuring a team's workflow around this framework, work through the following, grouped by stage:</p>
<p><strong>Plan and Design</strong></p>
<ul>
<li><p>[ ] An <code>intent.md</code> template exists in the repository, and every new piece of work starts from a filled-in copy of it.</p>
</li>
<li><p>[ ] A <code>spec.md</code> template exists with an explicit "open concerns" section that a human reads before Build starts.</p>
</li>
<li><p>[ ] At least one person other than the requester confirms the agent's summary of the intent before it becomes a spec.</p>
</li>
</ul>
<p><strong>Build</strong></p>
<ul>
<li><p>[ ] A memory file (<code>CLAUDE.md</code>, <code>AGENTS.md</code>, or <code>GEMINI.md</code>) exists, is checked into version control, and names commands, architecture, and conventions, rather than just placeholder text.</p>
</li>
<li><p>[ ] <code>plan.md</code> names the specific files, order of changes, and tests before implementation starts.</p>
</li>
</ul>
<p><strong>Test</strong></p>
<ul>
<li><p>[ ] A hook, pre-commit check, or equivalent local gate runs the test suite automatically and blocks progress on failure.</p>
</li>
<li><p>[ ] Test results are attached to the specific commit they verify, not just reported as a pass or fail in chat.</p>
</li>
</ul>
<p><strong>Deploy</strong></p>
<ul>
<li><p>[ ] A first-party CI action (Claude Code, Codex, or Gemini) runs an automated review on every pull request.</p>
</li>
<li><p>[ ] A human approval gate is explicitly required for changes touching auth, payments, migrations, or infrastructure, regardless of what the automated review found.</p>
</li>
<li><p>[ ] Auto-merge, if enabled at all, is scoped to a defined low-risk category, not the default for every green check.</p>
</li>
</ul>
<p><strong>Maintain</strong></p>
<ul>
<li><p>[ ] A monitoring configuration defines acceptable bands for the metrics that matter to the business, not every metric the platform happens to expose.</p>
</li>
<li><p>[ ] At least the highest-severity breach category is wired to automatically draft a new <code>intent.md</code>, closing the loop back to Plan.</p>
</li>
<li><p>[ ] Someone, even if it's the same person running every other stage, is responsible for reading and acting on the incidents this stage generates.</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You now have a working mental model for restructuring a software team's lifecycle around agentic coding tools, plus three concrete implementations. The six stages (Plan, Design, Build, Test, Deploy, Maintain) haven't changed names. What changed is the artifact each stage produces and the tool that produces it.</p>
<ul>
<li><p><code>intent.md</code> captures the problem before anyone interprets it, whether that interpretation happens in Claude Code's Plan mode, Codex's read-only sandbox, or Gemini CLI's plan approval mode.</p>
</li>
<li><p><code>spec.md</code> <strong>and</strong> <code>plan.md</code> turn an agent's speed into an asset instead of a liability, by giving it a target to build against that a human already reviewed.</p>
</li>
<li><p><strong>Hooks, CI actions, and native code review products</strong> replace a testing phase with continuous verification, woven into every commit rather than bolted on at the end.</p>
</li>
<li><p><strong>Layered review</strong> puts human judgment where it still adds the most value: at the gate for changes with blast radius, rather than at every line of every diff.</p>
</li>
<li><p><strong>A bands-style monitoring configuration</strong> closes the loop, turning a production incident directly into the next cycle's <code>intent.md</code> instead of a postmortem nobody reopens.</p>
</li>
</ul>
<p>The common thread across every stage in this guide is the same one METR's task-doubling curve implied at the start: the constraint on how fast software ships has already moved, whether or not your process has caught up. Teams that keep treating Build as the bottleneck will keep optimizing the one stage that stopped being the problem.</p>
<h2 id="heading-what-to-explore-next">What to Explore Next</h2>
<ul>
<li><p><a href="https://claude.com/blog/the-ai-native-sdlc-playbook">Anthropic's AI-native SDLC playbook</a>, the primary source for the six-stage framework this guide implements</p>
</li>
<li><p><a href="https://cloud.google.com/blog/products/ai-machine-learning/announcing-the-2025-dora-report">DORA's 2025 State of AI-assisted Software Development Report</a>, for the adoption and trust data behind the opening hook</p>
</li>
<li><p><a href="https://metr.org/blog/2025-03-19-measuring-ai-ability-to-complete-long-tasks/">METR's research on AI task-length doubling</a>, for the full methodology behind the seven-month doubling curve</p>
</li>
<li><p><a href="https://code.claude.com/docs/en/memory">Claude Code's documentation hub</a>, starting from the memory file page and branching out to permission modes, hooks, and subagents</p>
</li>
<li><p><a href="https://learn.chatgpt.com/docs/agent-approvals-security">OpenAI's Codex documentation on agent approvals and security</a>, for the full detail on the sandbox and approval-policy split</p>
</li>
<li><p><a href="https://github.com/google-gemini/gemini-cli/blob/main/docs/reference/configuration.md">Gemini CLI's configuration reference</a>, for the current state of its approval modes and extension system</p>
</li>
</ul>
<p>Visit my <a href="https://github.com/RudrenduPaul">GitHub</a> to explore the 30+ open-source software solutions and developer tools I built and shared using the agentic AI-native engineering process.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Claude Code Observability with OpenTelemetry ]]>
                </title>
                <description>
                    <![CDATA[ Agentic coding tools like Claude Code, OpenAI Codex, Google Antigravity, and Cursor have become ubiquitous for everyday software development. As agentic systems mature, much of the work developers hav ]]>
                </description>
                <link>https://www.freecodecamp.org/news/claude-code-observability-with-opentelemetry/</link>
                <guid isPermaLink="false">6a9a0db7c7c0575bd6526dd2</guid>
                
                    <category>
                        <![CDATA[ OpenTelemetry ]]>
                    </category>
                
                    <category>
                        <![CDATA[ claude ]]>
                    </category>
                
                    <category>
                        <![CDATA[ #prometheus ]]>
                    </category>
                
                    <category>
                        <![CDATA[ distributed tracing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ observability ]]>
                    </category>
                
                    <category>
                        <![CDATA[ generative ai ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Puneet Singh ]]>
                </dc:creator>
                <pubDate>Fri, 04 Sep 2026 00:15:51 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/2a729ee5-e1b9-4198-91cd-251b9c12867f.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Agentic coding tools like <a href="https://claude.com/claude-code">Claude Code</a>, <a href="https://openai.com/codex">OpenAI Codex</a>, <a href="https://antigravity.google">Google Antigravity</a>, and <a href="https://cursor.com">Cursor</a> have become ubiquitous for everyday software development.</p>
<p>As agentic systems mature, much of the work developers have them do is delegated, one subagent at a time. Many teams are also exploring and using a shared, multi-tenant Agentic Infrastructure, where cost isn't tied to a single owner. That's where Observability becomes key to monitoring infrastructure costs.</p>
<p>In this guide, you'll learn how observability works, then enable Claude Code's built-in telemetry, run a backend to collect it, and read the metrics, logs, and traces it emits. This will help you start tracking your team's costs more effectively, and it'll only improve as emitted telemetry matures and correlates more cleanly with your sessions.</p>
<p><strong>Note</strong>: In its current state, the emitted telemetry from Claude Code provides no attributes that allow a reliable map to named sessions. Usage can be tracked using session_id, but it's still clumsy in a longer session mixing multiple prompts/skills.</p>
<p>This guide is scoped to <code>Claude Code</code>'s telemetry for metrics, logs, and tracing. Note that it applies to Linux and macOS only.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-observability-with-opentelemetry">Observability with OpenTelemetry</a></p>
<ul>
<li><a href="#heading-telemetry-data">Telemetry Data</a></li>
</ul>
</li>
<li><p><a href="#heading-instrumenting-claude-code">Instrumenting Claude Code</a></p>
<ul>
<li><p><a href="#heading-pull-vs-push-how-telemetry-leaves-an-app">Pull vs Push: How Telemetry Leaves an App</a></p>
</li>
<li><p><a href="#heading-when-to-run-a-collector">When to run a Collector</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-setup">Setup</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-exploring-telemetry">Exploring Telemetry</a></p>
<ul>
<li><p><a href="#heading-metrics">Metrics</a></p>
</li>
<li><p><a href="#heading-logs">Logs</a></p>
</li>
<li><p><a href="#heading-tracing">Tracing</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-references">References</a></p>
</li>
</ul>
<h2 id="heading-observability-with-opentelemetry">Observability with OpenTelemetry</h2>
<p>Observability is the ability to answer questions about a system's runtime behavior from the data it emits. You do this without looking into its internals, attaching a debugger, reading source code, or manually trying to reproduce the behavior.</p>
<p>Here, a system's runtime behavior means what's externally visible. You can ask questions like:</p>
<ul>
<li><p>How much time 95% of all requests take.</p>
</li>
<li><p>What the failure rate is across all requests received.</p>
</li>
<li><p>What the cache hit ratio is for the in-memory cache the service uses.</p>
</li>
<li><p>The difference between the configured and deployed replica counts for a service.</p>
</li>
</ul>
<p>For Claude Code, the inaccessible inner workings are: how it manages context, how work is divided across multiple LLM calls, and how subagents are orchestrated. But you can read the emitted telemetry from Claude code to answer questions like:</p>
<ul>
<li><p>How much a dev or a team spent over a day, week, or month.</p>
</li>
<li><p>How that usage is distributed across the supported models and effort levels.</p>
</li>
<li><p>How many tokens are spent per dollar, and how much that varies by type (input, output, cacheRead, cacheCreation).</p>
</li>
<li><p>When a compaction event kicked in, and by how much it reduced the context's token usage.</p>
</li>
</ul>
<p>Only an instrumented system can answer these questions. Instrumentation is a piece of code added by the developer or built into the tool that records a program's runtime behavior and emits it as telemetry. For example, a measurement like <code>this request spent 100 tokens</code>.</p>
<p>The telemetry data helps avoid silent failures by providing a well-structured data trail of the system's behavior over time. For example, here's a chart from <a href="https://github.blog/news-insights/company-news/the-august-17-outage-and-the-work-ahead/">GitHub's August 17, 2026 outage</a> postmortem explaining a rise in GitHub Actions runs over time from ~30M to ~110M:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/fe9ec420-2137-4a21-93aa-00255638bc32.png" alt="Github Actions Growth" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h3 id="heading-telemetry-data">Telemetry Data</h3>
<p>The emitted telemetry consists of three categories of data:</p>
<ul>
<li><p><a href="https://opentelemetry.io/docs/concepts/signals/metrics/"><strong>Metrics</strong></a>: Numeric measurements aggregated over a time window, like queries per second (QPS).</p>
</li>
<li><p><a href="https://opentelemetry.io/docs/concepts/signals/logs/"><strong>Logs</strong></a>: A detailed record of an individual event, with a timestamp. For example, a compaction event in Claude Code.</p>
</li>
<li><p><a href="https://opentelemetry.io/docs/concepts/signals/traces/"><strong>Traces</strong></a>: The path of one request through the system, split by time into nested requests. For example, an order-placement request on an ecommerce website, showing which internal services it calls to complete the request.</p>
</li>
</ul>
<p>OpenTelemetry (<a href="https://opentelemetry.io/">https://opentelemetry.io/</a>) is an observability framework that helps you generate, collect, and export these signals to a backend, which handles storage, querying, and visualization. Keeping that split makes it tool/vendor agnostic where the backend can be open-source or proprietary. It provides instrumentation SDKs for multiple <a href="https://opentelemetry.io/docs/languages/">programming languages</a>.</p>
<h2 id="heading-instrumenting-claude-code">Instrumenting Claude Code</h2>
<p>Instrumentation code usually runs alongside the application, at the points best for measuring: a middleware with a request arriving or a response going out, or a token being counted.</p>
<p>It is plugged into the application in two ways:</p>
<ol>
<li><p>A Shared Instrumentation Library: Applications using an open source framework can add an instrumentation library as a dependency and link it with the application's lifecycle methods. OpenTelemetry publishes instrumentation libraries for many frameworks (Example: <a href="https://opentelemetry.io/docs/zero-code/java/spring-boot-starter/out-of-the-box-instrumentation/">Spring Framework</a>).</p>
</li>
<li><p>Customized Implementation by Application Developers: Telemetry data emitters are added directly in the codebase using the OpenTelemetry SDK. For a closed-source product, the code is private, but it can still emit telemetry data compatible with OpenTelemetry standards.</p>
</li>
</ol>
<h3 id="heading-example-http-instrumentation">Example: HTTP Instrumentation</h3>
<p>Middleware in HTTP handling is a common codepath for all requests, which is why it's chosen for application-wide settings like authentication. The same reason makes it a good place for instrumentation code: wrap the handler once, measure every request.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/e0699b5b-5459-4a28-befb-87dce792f4e9.png" alt="HTTP Instrumentation example" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>The diagram shows where instrumentation sits relative to the request path.</p>
<ul>
<li><p>A client request passes through an HTTP middleware before reaching the application's request handler and downstream calls. The middleware uses SDK constructs to time the handler and record duration, status, and a count.</p>
</li>
<li><p>The SDK then buffers those measurements and pushes OTLP to the Collector on a background thread, off the request path.</p>
</li>
</ul>
<p>Claude Code is the second case, where both the core app and its instrumentation module are provided by <a href="https://code.claude.com/docs/en/monitoring-usage">Anthropic</a>. The code that measures token usage, cost, and tool calls is built in and emits OpenTelemetry over <a href="https://opentelemetry.io/docs/specs/otlp/">OTLP</a>. As a Claude Code user, you only need to enable the telemetry and prepare a backend to receive and analyze it.</p>
<p><strong>Note</strong>: <a href="https://opentelemetry.io/docs/specs/otlp/">OTLP</a> is a telemetry data delivery protocol designed in the scope of the OpenTelemetry project. This guide assumes end-to-end compatibility with OTLP. Wiring up incompatible telemetry or using incompatible backend components can give unpredictable results and is out of scope.</p>
<p>To collect, store, and read the telemetry, you'll need the following components:</p>
<ul>
<li><p><a href="https://opentelemetry.io/docs/collector/">OpenTelemetry Collector</a>: A vendor-agnostic implementation of how to receive, process, and export telemetry data. This is optional but great to have for personal setup. Must have for a production use case.</p>
</li>
<li><p><a href="https://www.jaegertracing.io/">Jaeger</a>: Distributed tracing backend, released as an open source tool by Uber.</p>
</li>
<li><p><a href="https://prometheus.io/">Prometheus</a>: Collects and stores metrics as Timeseries Data.</p>
</li>
<li><p><a href="https://grafana.com/oss/loki/">Loki</a>: Scalable Log Aggregation system by Grafana.</p>
</li>
<li><p><a href="https://grafana.com/oss/grafana/">Grafana</a>: for UI visualization of logs, metrics, and traces.</p>
</li>
</ul>
<h3 id="heading-pull-vs-push-how-telemetry-leaves-an-app">Pull vs Push: How Telemetry Leaves an App</h3>
<p>Telemetry leaves an application in one of two ways:</p>
<p><strong>Pull (Scrape)</strong>: The app exposes its current metrics on an HTTP endpoint, and a scraper (Prometheus) reads that endpoint periodically. Every running instance needs its own port, and the scraper must know all of those addresses ahead of time. The app is passive: the scraper drives the data movement. This suits long-lived processes with stable addresses.</p>
<p>In OpenTelemetry, pull-based scraping is configured with <code>OTEL_METRICS_EXPORTER=prometheus</code> (see the <a href="https://opentelemetry.io/docs/languages/sdk-configuration/general/">SDK environment variables</a> for the accepted exporter values).</p>
<p>By convention, the app makes its metrics available at <code>http://localhost:9464/metrics</code>. This exporter handles metrics only.</p>
<p><strong>Push (OTLP)</strong>: The app sends its telemetry to a receiving endpoint on a regular interval. This is more flexible: any number of processes can push to the same endpoint with no registration ahead of time, so apps can start and stop freely even as their addresses change.</p>
<p>In OpenTelemetry, push is configured with <code>OTEL_METRICS_EXPORTER=otlp</code>, which ships over the <a href="https://opentelemetry.io/docs/specs/otel/protocol/exporter/">OTLP exporter</a>.</p>
<p>The push model can carry metrics, logs, and traces.</p>
<h3 id="heading-when-to-run-a-collector">When to Run a Collector</h3>
<p>A Collector is deployed when the existing stack isn't enough to handle the system's growing scale and complexity. It helps in the following ways:</p>
<ul>
<li><p><strong>One export config</strong>: every producer points at the Collector instead of each carrying its own per-backend exporter setup.</p>
</li>
<li><p><strong>Outbound-only connections</strong>: enterprise networks often block the inbound connections a pull-based scraper needs. With a Collector, the app pushes out to it and it pushes onward, so nothing has to accept inbound traffic.</p>
</li>
<li><p><strong>Fan-out and translation</strong>: the Collector can convert telemetry into a vendor's storage format and send the same signal to more than one backend.</p>
</li>
<li><p><strong>Buffering</strong>: if a backend goes down, the Collector holds the data and retries, absorbing transient failures.</p>
</li>
<li><p><strong>Processing</strong>: it can apply <a href="https://opentelemetry.io/docs/collector/configuration/#processors">processors</a> before data leaves for storage, such as redacting attributes.</p>
</li>
</ul>
<p><strong>Note</strong>: This guide runs a push-based configuration with a Collector even though the setup is single-user. The stack has three backends that store and query data differently, and letting the Collector receive Claude Code's OTLP once and route each signal to the right place is simpler than wiring the app to all three. That's why the tool list calls it optional for personal use but a must-have in production: its value grows with the number of producers and backends.</p>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>Each section in this guide links relevant docs, but you'll make faster progress if the tools and query languages specified below are already familiar:</p>
<p><strong>You'll need</strong></p>
<ul>
<li><p>Latest version of <a href="https://code.claude.com/docs/en/setup">Claude Code</a></p>
</li>
<li><p><a href="https://docs.docker.com/engine/install/">Docker Engine</a> (29.4.1+) with <a href="https://docs.docker.com/compose/">Docker Compose</a>.</p>
<ul>
<li>Capacity to run 5 Containers (4-core CPU, 8GB RAM, 15GB Disk)</li>
</ul>
</li>
<li><p>A Claude Plan that includes <a href="https://code.claude.com/docs/en/monitoring-usage#traces-beta">Enhanced Telemetry beta</a> (Pro+ / Max)</p>
</li>
<li><p><a href="https://git-scm.com/">Git</a></p>
</li>
<li><p>Ensure these ports are free at localhost:</p>
<ul>
<li><p><code>3000</code> (Grafana)</p>
</li>
<li><p><code>3100</code> (Loki)</p>
</li>
<li><p><code>4317</code>/<code>4318</code> (OTel Collector OTLP gRPC/HTTP)</p>
</li>
<li><p><code>9090</code> (Prometheus)</p>
</li>
<li><p><code>16686</code> (Jaeger UI)</p>
</li>
</ul>
</li>
</ul>
<p><strong>Knowledge that would help</strong></p>
<ul>
<li><p><a href="https://docs.docker.com/reference/cli/docker/compose/">Docker Compose</a>: bringing up containers defined in compose file, reading container status and logs by <code>docker compose ...</code> commands.</p>
</li>
<li><p><a href="https://www.gnu.org/software/bash/manual/">Bash</a> and Config files: Setting environment variables, editing <a href="https://www.json.org/json-en.html">JSON</a> files.</p>
</li>
<li><p><a href="https://prometheus.io/docs/prometheus/latest/querying/basics/">PromQL</a> (Prometheus): How to use counters/gauges, range selectors, and <code>sum</code> / <code>increase</code> / <code>rate</code> / <code>by</code> (label) grouping.</p>
</li>
<li><p>Grafana: <a href="https://grafana.com/docs/grafana/latest/explore/">Explore</a> a datasource, build <a href="https://grafana.com/docs/grafana/latest/panels-visualizations/">dashboards</a> with stat and timeseries panels. Panel <a href="https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/transform-data/">transformations</a> and <a href="https://grafana.com/docs/grafana/latest/visualizations/dashboards/variables/global-variables/">Global variables</a>.</p>
</li>
<li><p><a href="https://grafana.com/docs/loki/latest/query/">LogQL</a> (Loki): stream selectors, logfmt, and label_format.</p>
</li>
<li><p>Jaeger and tracing: the <a href="https://opentelemetry.io/docs/concepts/signals/traces/">trace/span model</a> (parent-child spans, span count, duration) and the <a href="https://www.jaegertracing.io/docs/latest/frontend-ui/">Jaeger UI's tag search</a>.</p>
</li>
<li><p>Claude Code's execution model: sessions, <a href="https://code.claude.com/docs/en/sub-agents">subagents</a>, skills, tools, and context compaction.</p>
</li>
<li><p><a href="https://code.claude.com/docs/en/costs">Claude billing basics</a>: tokens and the <a href="https://platform.claude.com/docs/en/build-with-claude/prompt-caching">prompt-caching tiers</a>.</p>
</li>
</ul>
<h3 id="heading-setup">Setup</h3>
<p>The test observability stack is deployed using Docker Compose. For telemetry export to work, Claude Code must be able to reach Collector's OTLP endpoint which is localhost:4317 (gRPC) or localhost:4318(HTTP) when running on same machine. All backend services run in a container with their own Docker volume for persistence.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/d486a15e-1b71-490a-8f5f-9489ffe0edab.png" alt="Instrumentation Setup" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>The diagram shows how the telemetry components we're using are linked.</p>
<ul>
<li><p>Apart from Claude Code, every component runs in a container managed by Docker Compose.</p>
</li>
<li><p>Multiple instances of Claude Code running on any machine (host or cloud VM) should be able to export telemetry to the Collector as long as those machines have connectivity to it (ports 4317/4318 of the host running the Collector container are reachable).</p>
</li>
</ul>
<p>Moving from top to bottom:</p>
<ul>
<li><p>Claude Code exports all three signals over OTLP to the Collector.</p>
</li>
<li><p>The Collector then splits them by type, pushing traces to Jaeger and logs to Loki, while exposing metrics on port 8889 for Prometheus to scrape.</p>
</li>
<li><p>Jaeger, Prometheus, and Loki each persist to their own Docker volume.</p>
</li>
<li><p>Grafana queries all three as the single dashboard layer.</p>
</li>
</ul>
<p>The goal here is a live stream of telemetry from Claude Code that gets stored in a backend and can be queried on demand, during a session or long after. Two things must be in place:</p>
<ul>
<li><p>Enable telemetry in Claude Code. The instrumentation is built in but emits nothing until telemetry is enabled and its OTLP exporter points at the Collector.</p>
</li>
<li><p>Run the observability backend. The Collector processes each signal, forwarding it to Prometheus, Loki, and Jaeger for storing and serving queries.</p>
</li>
</ul>
<p>You'll start the backend first, so the telemetry has somewhere to go.</p>
<h4 id="heading-start-the-observability-backend">Start the Observability Backend</h4>
<p>Before enabling telemetry in Claude Code, ensure the stack is up to collect, process, and read the data. The code for the test observability backend lives in this <a href="https://github.com/ps-mir/otel-dev-stack">Github repo</a>.</p>
<p>The repo has the following structure:</p>
<pre><code class="language-bash">.
├── README.md
└── compose
    ├── docker-compose.yml # Docker config for 5 containers in the observability stack. Applies pinned image versions, port mappings and named volumes for each service.
    ├── grafana
    │&nbsp;&nbsp; └── provisioning
    │&nbsp;&nbsp;     ├── alerting
    │&nbsp;&nbsp;     ├── dashboards
    │&nbsp;&nbsp;     ├── datasources # datasources(Prometheus, Loki, Jaeger) and dashboards. Empty initially.
    │&nbsp;&nbsp;     └── plugins
    ├── jaeger-config.yaml # Jaeger v2, badger (local-file) storage for traces. Ties to the user: root TIP below.
    ├── loki-config.yaml # single-binary Loki, filesystem storage. Near default settings.
    ├── otel-collector-config.yaml # receive/process/export pipeline: OTLP in on 4317/4318, traces out to Jaeger, logs to Loki, metrics exposed on :8889 for Prometheus.
    └── prometheus.yml # a single scrape job against the Collector's :8889, 30s interval.
</code></pre>
<p>You'll only need <code>docker compose</code> command to start the containers. It reads <a href="https://github.com/ps-mir/otel-dev-stack/blob/690d485fb89491fcd940b550377d9a2cc2dcc084/compose/docker-compose.yml">docker-compose.yml</a> and starts the containers, linking them to their respective config files.</p>
<h4 id="heading-connectivity-between-containers">Connectivity between containers:</h4>
<p>All containers start within the same Docker network, which allows them to communicate using container names directly. For example, <a href="https://github.com/ps-mir/otel-dev-stack/blob/690d485fb89491fcd940b550377d9a2cc2dcc084/compose/otel-collector-config.yaml">collector's exporter config</a> uses container names:</p>
<pre><code class="language-yaml">exporters:
  otlp/jaeger:
    endpoint: jaeger:4317
    tls:
      insecure: true
  prometheus:
    endpoint: 0.0.0.0:8889
  otlphttp/loki:
    endpoint: http://loki:3100/otlp
</code></pre>
<p>Note that it doesn't contain Prometheus config, since Prometheus ends up scraping it from the collector as configured in <a href="https://github.com/ps-mir/otel-dev-stack/blob/690d485fb89491fcd940b550377d9a2cc2dcc084/compose/prometheus.yml">prometheus.yml</a>:</p>
<pre><code class="language-yaml">global:
  scrape_interval: 30s

scrape_configs:
  - job_name: otel-collector
    static_configs:
      - targets: ["otel-collector:8889"]
</code></pre>
<p>Start the containers using Docker compose:</p>
<pre><code class="language-bash">git clone https://github.com/ps-mir/otel-dev-stack.git
cd otel-dev-stack/compose
docker compose up -d

# Output
 ✔ Volume compose_loki_data                          Created                                                                                                                                          0.0s
 ✔ Volume compose_grafana_data                       Created                                                                                                                                          0.0s
 ✔ Volume compose_prometheus_data                    Created                                                                                                                                          0.0s
 ✔ Volume compose_jaeger_data                        Created                                                                                                                                          0.0s
 ✔ Network compose_default                           Created                                                                                                                                          0.1s
 ✔ Container compose-prometheus-1                    Started                                                                                                                                          4.1s
 ✔ Container compose-loki-1                          Started                                                                                                                                          4.2s
 ✔ Container compose-jaeger-1                        Started                                                                                                                                          4.3s
 ✔ Container compose-otel-collector-1                Started                                                                                                                                          3.3s
 ✔ Container compose-grafana-1                       Started                                                                                                                                          2.7s
</code></pre>
<p>Check container status:</p>
<pre><code class="language-bash"># all five services should show "Up"
docker compose ps

# Output
NAME                       IMAGE                                              COMMAND                  SERVICE          CREATED         STATUS         PORTS
compose-grafana-1          grafana/grafana:13.2.0                            "/run.sh"                grafana          3 minutes ago   Up 3 minutes   0.0.0.0:3000-&gt;3000/tcp, [::]:3000-&gt;3000/tcp
compose-jaeger-1           cr.jaegertracing.io/jaegertracing/jaeger:2.20.0   "/go/bin/jaeger --co…"   jaeger           3 minutes ago   Up 3 minutes   0.0.0.0:16686-&gt;16686/tcp, [::]:16686-&gt;16686/tcp
compose-loki-1             grafana/loki:3.7.6                                "/usr/bin/loki -conf…"   loki             3 minutes ago   Up 3 minutes   0.0.0.0:3100-&gt;3100/tcp, [::]:3100-&gt;3100/tcp
compose-otel-collector-1   otel/opentelemetry-collector-contrib:0.159.0      "/otelcol-contrib --…"   otel-collector   3 minutes ago   Up 3 minutes   0.0.0.0:4317-4318-&gt;4317-4318/tcp, [::]:4317-4318-&gt;4317-4318/tcp, 55679/tcp
compose-prometheus-1       prom/prometheus:v3.11.2                           "/bin/prometheus --c…"   prometheus       3 minutes ago   Up 3 minutes   0.0.0.0:9090-&gt;9090/tcp, [::]:9090-&gt;9090/tcp
</code></pre>
<p><strong>TIP</strong>: Jaeger runs as <code>user: root</code> (Compose file) to create the badger dir. Not doing so causes a failure: <code>mkdir /badger/key: permission denied</code>. Jaeger itself doesn't need root permission, but Docker volumes are owned by <code>root:root</code> on first mount.</p>
<h4 id="heading-enable-telemetry">Enable Telemetry</h4>
<p>OpenTelemetry instrumentation, once added to an application, stays disabled until specific configuration enables it.</p>
<p>There are two ways to enable telemetry in Claude Code:</p>
<p>1. Environment Variables</p>
<p>Setting specific environment variables enables telemetry generation. Beyond the standard <code>OTEL_*</code> variables, Claude Code defines its own <code>CLAUDE_CODE_*</code> variables.</p>
<p>This guide uses the following settings:</p>
<pre><code class="language-bash"># master switch: when unset or 0, Claude Code produces no telemetry at all
export CLAUDE_CODE_ENABLE_TELEMETRY=1

# opt into the beta enhanced-telemetry attributes and events (extra session and tool detail)
export CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1

# per-signal exporter selection; "otlp" ships the signal over OTLP.
# other accepted values are "console" (print locally), "prometheus" (metrics only), and "none" (drop the signal)
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_TRACES_EXPORTER=otlp

# OTLP transport: "grpc" talks to the collector's 4317 port; "http/protobuf" would use 4318
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc

# one endpoint for all three signals: the collector's OTLP listener on the local machine
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317

# how often metrics are flushed, in milliseconds; the default is 60000 (60s),
# shortened here so a manual check sees fresh data without a long wait
export OTEL_METRIC_EXPORT_INTERVAL=5000

# emit cumulative counters instead of delta (see "Aggregation Temporality" below)
export OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=cumulative
</code></pre>
<p>Environment variables, however, are process-wide and can affect more than Claude Code. For example,</p>
<ul>
<li><p>Accidentally enabling instrumentation in other applications.</p>
</li>
<li><p>Interfering with OpenTelemetry code/tests if you're developing your own instrumentation or working on any OpenTelemetry SDK.</p>
</li>
</ul>
<p>2. Claude Code <code>settings.json</code></p>
<p>OpenTelemetry defines <a href="https://opentelemetry.io/docs/languages/sdk-configuration/declarative-configuration/">Declarative Config</a>, a YAML based configuration to enable telemetry, but Claude Code doesn't support it. But it lets you set the same env variables in <code>~/.claude/settings.json</code>. That isn't declarative config, but it's better than shell environment variables because it applies only to Claude Code. An example:</p>
<pre><code class="language-json">{
  "effortLevel": "medium",
  "tui": "fullscreen",
  "env": {
     "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
     "CLAUDE_CODE_ENHANCED_TELEMETRY_BETA": "1",
     "OTEL_METRICS_EXPORTER": "otlp",
     "OTEL_LOGS_EXPORTER": "otlp",
     "OTEL_TRACES_EXPORTER": "otlp",
     "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
     "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317",
     "OTEL_METRIC_EXPORT_INTERVAL": "5000",
     "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE": "cumulative"
  }
}
</code></pre>
<p>Only the <code>env</code> block matters for telemetry. <code>effortLevel</code> and <code>tui</code> are unrelated settings you may already have. The variables match the annotated list above.</p>
<h4 id="heading-aggregation-temporality">Aggregation Temporality</h4>
<p>Prometheus <a href="https://prometheus.io/docs/concepts/metric_types/#counter">counter</a> type metrics only increase over time. Their raw value isn't useful, so you read them through per-second growth (<code>rate()</code>) or total growth over a time window (<code>increase()</code>).</p>
<p>Aggregation temporality decides what number a counter reports on each telemetry export: the change since the previous export(Delta), or the running total since the process started(Cumulative).</p>
<p>A short example. Say Claude Code spends tokens over four 5-second export intervals:</p>
<table>
<thead>
<tr>
<th>Export at</th>
<th>Tokens since last export</th>
<th>Delta value sent</th>
<th>Cumulative value sent</th>
</tr>
</thead>
<tbody><tr>
<td>0s (start)</td>
<td>--</td>
<td>--</td>
<td>0</td>
</tr>
<tr>
<td>5s</td>
<td>100</td>
<td>100</td>
<td>100</td>
</tr>
<tr>
<td>10s</td>
<td>0</td>
<td>0</td>
<td>100</td>
</tr>
<tr>
<td>15s</td>
<td>250</td>
<td>250</td>
<td>350</td>
</tr>
<tr>
<td>20s</td>
<td>50</td>
<td>50</td>
<td>400</td>
</tr>
</tbody></table>
<p>By default, Claude Code emits metrics with <code>AggregationTemporality: Delta</code>. This can be inspected and confirmed from the Collector's container logs using the command:</p>
<pre><code class="language-bash"># Command only works from directory containing docker-compose.yml
docker compose logs otel-collector
</code></pre>
<p><strong>Note</strong>: To enable detailed logs in the Collector, you need to add the <code>debug</code> exporter to the <a href="https://github.com/ps-mir/otel-dev-stack/blob/690d485fb89491fcd940b550377d9a2cc2dcc084/compose/otel-collector-config.yaml">Collector config</a>.</p>
<pre><code class="language-yaml">service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp/jaeger, debug]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [prometheus, debug]
</code></pre>
<p>Then restart the container:</p>
<pre><code class="language-bash"># Command only works from directory containing docker-compose.yml
docker compose up -d --force-recreate otel-collector
</code></pre>
<p>Log output with <code>AggregationTemporality: Delta</code>:</p>
<pre><code class="language-bash">otel-collector-1  | Descriptor:
otel-collector-1  |      -&gt; Name: claude_code.active_time.total
otel-collector-1  |      -&gt; Description: Total active time in seconds
otel-collector-1  |      -&gt; Unit: s
otel-collector-1  |      -&gt; DataType: Sum
otel-collector-1  |      -&gt; IsMonotonic: true
otel-collector-1  |      -&gt; AggregationTemporality: Delta &lt;---
otel-collector-1  | NumberDataPoints #0
</code></pre>
<p>Delta doesn't work well with Prometheus functions like <code>rate()</code>/<code>increase()</code>, since they expect cumulative values.</p>
<p>Setting <code>OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=cumulative</code> switches the exported metrics from delta to cumulative temporality. It has been added to both the env and JSON config used in this guide.</p>
<p>As before, you need to restart the Collector container after any config change for it to take effect.</p>
<h2 id="heading-exploring-telemetry">Exploring Telemetry</h2>
<p>With telemetry flowing, you can start querying it. Metrics, logs, and traces each answer a different kind of question about Claude Code usage, so the three sections below are largely independent.</p>
<p>The data behind them comes from two places.</p>
<ul>
<li><p>The Metrics and Logs sections query whatever Claude Code usage has accumulated in the backend, so your panels will show your own sessions and the numbers won't match the screenshots. Give it a few real sessions before expecting much to show.</p>
</li>
<li><p>The Tracing section instead walks a single deliberate run, a custom skill summarizing a batch of meetings, described in enough detail to follow along. You don't need to reproduce it.</p>
</li>
</ul>
<h3 id="heading-metrics">Metrics</h3>
<p>Metrics are the aggregate, time-windowed view of Claude Code usage. Example: total cost, token volume, and how each trends and breaks down by attributes like <code>model</code>, <code>effort</code>, and token <code>type</code>. Use them to watch spend and spot shifts in consumption.</p>
<p>Each metric is a numeric measurement recorded over time, a <a href="https://prometheus.io/docs/concepts/data_model/">time series</a> of timestamped values you can plot or aggregate. Claude Code's metrics are running totals (counters), so a query reports the change over a chosen window rather than the raw value. See <a href="#heading-aggregation-temporality">Aggregation Temporality</a> above for how that works.</p>
<p><a href="https://prometheus.io/docs/introduction/overview/">Prometheus</a> is the metrics backend we're using here. It scrapes the Collector, stores the series, and answers queries written in <a href="https://prometheus.io/docs/prometheus/latest/querying/basics/">PromQL</a>. Grafana reads the same data for dashboards at <code>localhost:3000</code>. The full list of metrics and their attributes is in the <a href="https://code.claude.com/docs/en/monitoring-usage">Claude Code monitoring docs</a>.</p>
<p>Open Prometheus in your browser (localhost:9090), type <code>claude</code> in the query field, and you should see the supported metrics:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/8a4703dc-8b10-47b5-95ca-2cc383ab052a.png" alt="Available Claude Code metrics of counter type in Prometheus." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>For each metric below, you'll explore it first using PromQL, and then use the same query to add it to the Grafana dashboard as a panel.</p>
<h4 id="heading-total-usd-spent">Total USD Spent</h4>
<p><code>claude_code_cost_usage_USD_total</code> represents cumulative usage cost, in USD, tracked per session. It's useful for controlling budgets and spotting sudden spikes in usage.</p>
<p>This is a client-side estimate based on token counts priced at Anthropic's per-model, per-type rates and accumulated. It's completely normal for it to exceed your Claude Code plan's subscription cost.</p>
<p><strong>Note</strong>: This metric is more critical if you're paying per raw API call. A subscription gives you a usage allowance with increased but bounded rate limits.</p>
<p>Test the following query in Prometheus first (<code>localhost:9090/query</code>):</p>
<pre><code class="language-promql">sum(increase(claude_code_cost_usage_USD_total[10m]))
</code></pre>
<p><code>increase(...[10m])</code> gives the counter's growth over the last 10 minutes. <code>sum(...)</code> with no <code>by</code> clause collapses the per-attribute series (<code>model</code>, <code>effort</code>, and others) into one number.</p>
<p>For a Grafana panel, swap the fixed <code>[10m]</code> window for the <a href="https://grafana.com/docs/grafana/latest/visualizations/dashboards/variables/global-variables/"><code>$__range</code></a> built-in variable so the value follows the dashboard's time picker:</p>
<pre><code class="language-promql">sum(increase(claude_code_cost_usage_USD_total[$__range]))
</code></pre>
<p>To add it as a panel, open Explore, select Prometheus as the data source, and run the query. The result depends on how much you've used Claude Code in the window.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/99c0e78a-323b-412b-8952-685359644ad6.png" alt="Grafana USD Total Stat - Metrics explorer view of the query." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>On adding to the dashboard you should get more Panel Options. Select the <code>Stat</code> panel:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/d0e45c4a-f37e-4eb5-aed0-5929efe8a8fb.png" alt="Total USD Stat Panel" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>This panel needs to be added to the Grafana dashboard.</p>
<h4 id="heading-total-token-usage">Total Token Usage</h4>
<p><code>claude_code_token_usage_tokens_total</code> is the cumulative token count, with the same counter shape as the USD cost metric. Read a raw series in Prometheus first to see which labels you can aggregate by. A single series looks like:</p>
<pre><code class="language-text">claude_code_token_usage_tokens_total{effort="high", exported_job="claude-code", instance="otel-collector:8889", job="otel-collector", model="claude-sonnet-5", otel_scope_name="com.anthropic.claude_code", otel_scope_version="2.1.252", query_source="auxiliary", session_id="e0b9795b-4da3-4171-8fa6-a2866bf44d86", terminal_type="ssh-session", type="cacheCreation"}    213730
</code></pre>
<p>The trailing number is the counter value. Key attributes you'll be working with are <code>type</code>, <code>model</code>, and <code>effort</code>. The <a href="#heading-token-usage-by-type">Token Usage by Type</a> chart below groups on <code>type</code>.</p>
<p>The window total is the same query shape as <a href="#heading-total-usd-spent">Total USD Spent</a>, with the token counter:</p>
<pre><code class="language-promql">sum(increase(claude_code_token_usage_tokens_total[$__range]))
</code></pre>
<h4 id="heading-tokens-per-usd">Tokens Per USD</h4>
<p>Unlike the previous two metrics, this is a derived figure, calculated over a time window as Total Tokens / Total Cost.</p>
<pre><code class="language-promql">sum(increase(claude_code_token_usage_tokens_total[$__range])) / sum(increase(claude_code_cost_usage_USD_total[$__range]))
</code></pre>
<p>This one number collapses every attribute combination into a single value. Each distinct combination, for example model A at medium effort versus model B at high effort, is its own time series, and the query sums across all of them.</p>
<p>To analyze a specific combination, run the same ratio split by an attribute and compare.</p>
<pre><code class="language-promql">sum by (model) (increase(claude_code_token_usage_tokens_total[$__range]))
  / sum by (model) (increase(claude_code_cost_usage_USD_total[$__range]))
</code></pre>
<p>Swap <code>model</code> for <code>effort</code> or <code>type</code>; the raw series above lists the rest of the labels.</p>
<p>Overall Result:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/71b728df-e4c9-41bd-9b0b-cc0e0bade04f.png" alt="Stat Panel Aggregated Metrics" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>Stats Panel(6hr window): Total USD Spent ($4.28), Total Tokens Spent (3.02M), and Tokens Per USD (707k).</p>
<h4 id="heading-token-usage-by-type">Token Usage by Type</h4>
<p>You'll see how <code>claude_code_token_usage_tokens_total</code> changes over time, broken down by <code>type</code>. A single stat hides the shape, so use a time-series panel.</p>
<p>The <code>type</code> attribute has four values, which differ a lot in cost:</p>
<ul>
<li><p><code>cacheRead</code>: tokens served from an existing cache entry. They dominate token spend in a long session, and are cheaper than the <a href="https://platform.claude.com/docs/en/about-claude/pricing#prompt-caching">baseline rate</a>.</p>
</li>
<li><p><code>cacheCreation</code>: tokens written into the prompt cache on the first prefix load. Costly.</p>
</li>
<li><p><code>input</code>: new, uncached prompt tokens.</p>
</li>
<li><p><code>output</code>: model-generated tokens.</p>
</li>
</ul>
<p>To also see the total, use two queries: the per-type breakdown and the un-split total for reference.</p>
<pre><code class="language-promql"># per-type breakdown
sum by (type) (increase(claude_code_token_usage_tokens_total[$__rate_interval]))
# total
sum(increase(claude_code_token_usage_tokens_total[$__rate_interval]))
</code></pre>
<p><code>__rate_interval</code> is Grafana's per-step window for time-series panels, the counterpart to the <code>__range</code> used for the stat panels above.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/8f5fabfa-3cb9-4bcb-bd50-6e95c216bb47.png" alt="Graphana Explore Timeseries" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>The two queries running in Explore, before saving them as a panel.</p>
<p>After adding to the dashboard:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/e3cda31c-4741-4710-8bbf-de878a608d0e.png" alt="Running token usage by type" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>Each spike is a burst of Claude Code activity, the flat stretches are idle time. Hovering a point splits the total into the four types: here the total is about 1.0M tokens, of which <code>cacheRead</code> is about 925k (roughly 92%), the rest cacheCreation, output, and input.</p>
<p><strong>TIP</strong>: Token consumption is dominated by <code>cacheRead</code>, which is also the cheapest type.</p>
<h4 id="heading-token-usage-by-model-and-effort">Token Usage by Model and Effort</h4>
<p>This is the concrete version of the breakdown suggested under Tokens Per USD: which <code>model</code> and <code>effort</code> pairs are actually consuming tokens.</p>
<pre><code class="language-promql">sum by (model, effort) (
  increase(claude_code_token_usage_tokens_total[$__rate_interval])
)
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/47ee6a86-79a3-4bd2-9fff-7a2b64c77a7f.png" alt="Running token usage by model and effort" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>Here the token counter is grouped by its <code>model</code> and <code>effort</code> attributes. In this window every series is <code>claude-sonnet-5</code> at either <code>medium</code> or <code>high</code> effort, and one burst of <code>medium</code> effort near 18:13 reaches about 2.6M tokens. The number of unique groupings depends on the cardinality of the chosen attributes.</p>
<h3 id="heading-logs">Logs</h3>
<p>A log record is a timestamped event with its full field set attached. You query logs when you want that specific event and the context around it: what happened, when, and with which values.</p>
<p>Metrics are the pre-aggregated form of the same activity. Anything that means counting, summing, or taking percentiles across many records belongs in a metric. If you're aggregating log output downstream, that data should have been a metric from the start.</p>
<p>Logs are the right tool for:</p>
<ol>
<li><p>Per-event context: the full detail of one occurrence, not a rolled-up number.</p>
</li>
<li><p>Discrete or irregular events: a compaction firing, a session start, or an API error.</p>
</li>
<li><p>Post-incident forensics: reading raw records back while debugging after the fact.</p>
</li>
<li><p>Trace correlation: a log line carrying a trace and span ID drops you into the request it came from.</p>
</li>
</ol>
<p>The log backend we're using here is <a href="https://grafana.com/oss/loki/">Loki</a>, queried with <a href="https://grafana.com/docs/loki/latest/query/">LogQL</a>. Running logs through a backend like this buys you:</p>
<ol>
<li><p>Structured fields: filter and compute on named keys instead of regex over text.</p>
</li>
<li><p>Field indexing: label lookups return without scanning every line.</p>
</li>
<li><p>Trace and span correlation: pivot from a log to its trace, or pull every log for one trace.</p>
</li>
<li><p>Time-bounded queries: each query is scoped to a window, keeping the scan cheap.</p>
</li>
</ol>
<p>Grafana reads Loki for dashboards, the same as it does for Prometheus.</p>
<h4 id="heading-compaction-event">Compaction Event</h4>
<p>Compaction is Claude Code trimming its own context when it grows too large. Each compaction emits a log event (<code>event_name="compaction"</code>) carrying the token counts before and after (<code>pre_tokens</code>, <code>post_tokens</code>) and the <code>span_id</code> it happened under, so a query over those events shows how often it fires and how much it reclaims each time.</p>
<p>In Grafana Explore, select Loki as the data source and paste the LogQL below. It selects the compaction events, parses their fields with <a href="https://grafana.com/docs/loki/latest/query/log_queries/"><code>logfmt</code></a>, and derives a per-event reduction percentage with <a href="https://grafana.com/docs/loki/latest/query/template_functions/"><code>label_format</code></a>. The fields only exist once compaction has actually happened, so trigger a few first.</p>
<pre><code class="language-logql">{service_name="claude-code"} | event_name="compaction"
  | logfmt
  | label_format reduction_pct=`{{ printf "%.1f" (mulf (divf (subf .pre_tokens .post_tokens) .pre_tokens) 100) }}`
</code></pre>
<p>Deriving <code>reduction_pct</code> for each record is fine here because it stays per-event. A running average across compactions would belong in a metric.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/a001cb54-bbeb-4a6c-931c-41ac6b7f7640.png" alt=" LogQL query for compaction event against Loki data source." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>The <code>label_format</code> line adds a <code>reduction_pct</code> label. To show it as a table, switch the panel to Table view and add three Grafana transformations:</p>
<ol>
<li><p>Extract fields from the labels object.</p>
</li>
<li><p>Filter fields by name to keep Time, pre_tokens, post_tokens, reduction_pct, and span_id.</p>
</li>
<li><p>Convert field type to turn pre_tokens, post_tokens, and reduction_pct into numbers.</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/921de4bb-1f19-48de-8447-200fd2d1f875.png" alt="Compaction events with pre/post token counts and the derived reduction percentage." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h3 id="heading-tracing">Tracing</h3>
<p><a href="https://opentelemetry.io/docs/concepts/signals/traces/">Tracing</a> provides a detailed picture of the full path a request takes through an application, from start to completion. Some fundamental concepts behind tracing:</p>
<ul>
<li><p><strong>Span</strong>: a timed operation representing a unit of work. Building block for traces. All trace data is recorded as a sequence of spans, and each has a type, its operation name:</p>
<ul>
<li><p><code>claude_code.interaction</code>: one prompt and everything Claude Code does to answer it. Normally the root span, so one interaction is effectively one trace.</p>
</li>
<li><p><code>claude_code.llm_request</code>: a single model call inside an interaction.</p>
</li>
<li><p><code>claude_code.tool</code>: a single tool call inside an interaction (<code>Bash</code>, <code>Write</code>, <code>Agent</code>, and so on).</p>
</li>
</ul>
</li>
<li><p><strong>Trace</strong>: a tree of spans representing a request path from start to completion.</p>
</li>
<li><p><strong>Session</strong>: one Claude Code run, identified by <code>session.id</code>. It can result in many interactions and traces.</p>
</li>
<li><p><strong>Subagent</strong>: a nested Claude Code instance started by the <a href="https://code.claude.com/docs/en/sub-agents"><code>Agent</code> tool</a>, running its own interactions.</p>
</li>
</ul>
<p><a href="https://www.jaegertracing.io/">Jaeger</a> is the tracing backend used here. The Collector forwards spans to it over OTLP. Jaeger stores them and lets you <a href="https://www.jaegertracing.io/docs/latest/frontend-ui/">search traces</a> by service and span tags and inspect each one as a span tree. Everything below uses its UI at <code>localhost:16686</code>.</p>
<h4 id="heading-generating-traces">Generating Traces</h4>
<p>To generate trace data, this guide will use a test prompt to spawn agents and prepare some text. This prompt was tested with Sonnet 5 at medium effort.</p>
<p>You can paste the prompt directly into Claude Code:</p>
<pre><code class="language-text">Spawn 4 subagents in parallel, one per topic below. Each subagent researches its topic from your own knowledge and returns a ~150-word summary with 3 key points. Do not have them read files or run commands.
Topics:
1. How TCP congestion control works
2. The CAP theorem
3. How DNS resolution works
4. What a Bloom filter is

Once all 4 return, combine the summaries into one markdown document and write it to summary.md
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/ace08f8f-ff14-4e1e-aefa-2c23aea9ec5c.png" alt="Snapshot when running the prompt." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p><strong>Note</strong>: Ask Claude Code for the session_id in the same session after the prompt finishes. This will be used to find related traces in Jaeger.</p>
<h4 id="heading-trace-by-session-id">Trace By Session ID</h4>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/bdbee8ee-99e0-4d2e-add7-b1689193cb4a.png" alt="Finding traces by session_id." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>The search filters by <code>service = claude-code</code> and the tag <code>session.id=&lt;id&gt;</code>. It returns 6 traces, all rooted at <code>claude_code.interaction</code>, with span counts from 1 to 20 and durations from about 1 second to 33 seconds.</p>
<p>The list alone doesn't say which trace did what. Going through them by hand, or scripting it against the trace API for a real session, gives the following:</p>
<table>
<thead>
<tr>
<th>#</th>
<th>Trace Name</th>
<th>Spans</th>
<th>Duration</th>
<th>llm_calls</th>
<th>tools</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>claude_code.interaction</td>
<td>1</td>
<td>1.4s</td>
<td>0</td>
<td>–</td>
</tr>
<tr>
<td>2</td>
<td>claude_code.interaction</td>
<td>3</td>
<td>4.6s</td>
<td>2</td>
<td>–</td>
</tr>
<tr>
<td>3</td>
<td>claude_code.interaction</td>
<td>1</td>
<td>5.4s</td>
<td>0</td>
<td>–</td>
</tr>
<tr>
<td>4</td>
<td>claude_code.interaction</td>
<td>1</td>
<td>2.5s</td>
<td>0</td>
<td>–</td>
</tr>
<tr>
<td>5</td>
<td>claude_code.interaction</td>
<td>20</td>
<td>15.7s</td>
<td>7</td>
<td>Agent(x4)</td>
</tr>
<tr>
<td>6</td>
<td>claude_code.interaction</td>
<td>15</td>
<td>32.5s</td>
<td>5</td>
<td>ScheduleWakeup(x2), Write(x1)</td>
</tr>
</tbody></table>
<p>A few observations:</p>
<ul>
<li><p>Half the traces are noise. Traces 1, 3, and 4 are single-span interactions with no model call or tool, an idle session being pinged. Trace 2 is a brief exchange. Only Traces 5 and 6 are the run.</p>
</li>
<li><p>The parallel dispatch is a single interaction. Trace 5 fires all four <code>Agent</code> calls inside one <code>claude_code.interaction</code>. Their nested model calls (7 to 10 seconds each) overlap, so the interaction finishes in about 16 seconds despite roughly 35 seconds of combined subagent LLM time.</p>
</li>
<li><p>Each subagent's model call is nested under its <code>Agent</code> span and carries an <code>agent_id</code>, so you can tell the four apart.</p>
</li>
<li><p><code>agent_id</code> is opaque. There's no <code>agent.name</code> or <code>skill.name</code>. The trace tells you four subagents ran and how long each took, not which topic each was given.</p>
</li>
<li><p>Spans carry token counts but no USD cost. Each <code>claude_code.llm_request</code> has <code>input_tokens</code>, <code>output_tokens</code>, <code>cache_read_tokens</code>, and <code>cache_creation_tokens</code>, but no USD figure.</p>
</li>
<li><p>The write is a separate, later interaction. Trace 6 has no <code>Agent</code> spans: one <code>claude_code.llm_request</code> of about 23 seconds produces the combined markdown, then a short <code>Write</code>. The two <code>ScheduleWakeup</code> spans are background coordination.</p>
</li>
</ul>
<p><strong>TIP</strong>: From the trace you get the four subagent calls, each with an <code>agent_id</code>, token counts, and timing, but no span says which topic a subagent was handed. In contrast, Metrics can provide attribution by <code>model</code>, <code>effort</code>, and skill.</p>
<p><strong>CAUTION</strong>: <code>user_prompt</code> is redacted by default on interaction spans. OTEL_LOG_USER_PROMPTS=1 disables this and logs raw prompt text. Avoid enabling it in multi-user/tenant environments since it exposes prompt content to anyone with access to the telemetry backend.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This guide was an end-to-end walkthrough of observability in Claude Code, enabling its telemetry and collecting each of the three signals in a local backend for analysis.</p>
<p>Metrics give you the ability to dissect cumulative cost and usage readings by attribute over a chosen time period. That matters most in a shared or multi-tenant setup, where cost isn't tied to a single owner and someone still has to account for it.</p>
<p>Logs are records of individual events, useful for digging into exactly what changed during one, like compaction.</p>
<p>Traces show how one prompt expands into subagents and model calls, with timing and token counts on each. That's the starting point for debugging or tightening a complex or multi-agent prompt, though the spans don't yet record which prompt or skill drove a given call.</p>
<p>Some of this telemetry is behind the Enhanced Telemetry beta, so span names and attributes can still change, and gaps like per-call attribution may close as it matures. It's worth re-checking the <a href="https://code.claude.com/docs/en/monitoring-usage">monitoring docs</a> as the surface settles.</p>
<h2 id="heading-references">References</h2>
<ul>
<li><p><a href="https://platform.claude.com/docs/en/about-claude/pricing#prompt-caching">Claude Pricing: Prompt Caching</a></p>
</li>
<li><p><a href="https://code.claude.com/docs/en/monitoring-usage">Claude Code: Monitoring Usage</a></p>
</li>
<li><p><a href="https://prometheus.io/docs/concepts/metric_types/#counter">Prometheus: Counter Metric Type</a></p>
</li>
<li><p><a href="https://www.jaegertracing.io/docs/latest/frontend-ui/">Jaeger: Finding Traces</a></p>
</li>
<li><p><a href="https://grafana.com/docs/loki/latest/query/log_queries/">Loki: LogQL Log Queries</a></p>
</li>
<li><p><a href="https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/stat/">Grafana: Stat Panel</a></p>
</li>
<li><p><a href="https://opentelemetry.io/docs/collector/configuration/">OpenTelemetry Collector: Configuration</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build AI Systems That Know When They Don't Know: A Practical Guide ]]>
                </title>
                <description>
                    <![CDATA[ Large Language Models (LLMs) have fundamentally changed how we build internal business applications. They allow developers to create intelligent software that can synthesize complex corporate data, an ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-ai-systems-that-know-when-they-don-t-know/</link>
                <guid isPermaLink="false">6a99b7e04f56be68c2dd5b74</guid>
                
                    <category>
                        <![CDATA[ Artificial Intelligence ]]>
                    </category>
                
                    <category>
                        <![CDATA[ llm ]]>
                    </category>
                
                    <category>
                        <![CDATA[ large language models ]]>
                    </category>
                
                    <category>
                        <![CDATA[ generative ai ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mlops ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chidiebere Njoku ]]>
                </dc:creator>
                <pubDate>Thu, 03 Sep 2026 18:09:36 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/2a4852b4-00e8-4a91-aace-3fb67c7bfab3.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Large Language Models (LLMs) have fundamentally changed how we build internal business applications. They allow developers to create intelligent software that can synthesize complex corporate data, answer internal queries, and automate repetitive workflows.</p>
<p>But moving an LLM application from a local prototype to a production enterprise system can reveal a critical reliability issue: <strong>overconfidence</strong>.</p>
<p>Standard language models are optimized to generate the most statistically probable next token, not to evaluate their own baseline certainty. When confronted with ambiguous prompts, incomplete retrieval context, or out-of-domain edge cases, an unguarded model will confidently invent plausible-sounding falsehoods, hallucinating facts without giving the user any indication of uncertainty.</p>
<p>In mission-critical enterprise environments, an AI application that guesses blindly is a severe business risk. In this guide, you'll learn how to build a production-grade uncertainty framework. I'll walk you through an architecture designed to detect knowledge gaps, compute probabilistic confidence metrics, and gracefully route low-certainty requests to human operators or safe fallback responses.</p>
<h3 id="heading-what-well-cover"><strong>What We'll Cover</strong></h3>
<ul>
<li><p><a href="#heading-prerequisites-and-environment-setup">Prerequisites and Environment Setup</a></p>
<ul>
<li><p><a href="#heading-package-installation">Package Installation</a></p>
</li>
<li><p><a href="#heading-local-directory-structure">Local Directory Structure</a></p>
</li>
<li><p><a href="#heading-environment-configuration">Environment Configuration</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-the-challenge-addressing-the-overconfidence-vulnerability">The Challenge: Addressing the Overconfidence Vulnerability</a></p>
</li>
<li><p><a href="#heading-understanding-the-enterprise-request-lifecycle-for-uncertainty-evaluation">Understanding the Enterprise Request Lifecycle for Uncertainty Evaluation</a></p>
<ul>
<li><p><a href="#heading-step-1-implementing-layer-1-input-intent-amp-boundary-detection">Step 1: Implementing Layer 1 – Input Intent &amp; Boundary Detection</a></p>
</li>
<li><p><a href="#heading-step-2-implementing-layer-2-semantic-distance-amp-retrieval-quality-scoring">Step 2: Implementing Layer 2 – Semantic Distance &amp; Retrieval Quality Scoring</a></p>
</li>
<li><p><a href="#heading-step-3-implementing-layer-3-probabilistic-logit-analysis-amp-output-validation">Step 3: Implementing Layer 3 – Probabilistic Logit Analysis &amp; Output Validation</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-operational-insights-from-running-uncertainty-detection-systems">Operational Insights from Running Uncertainty Detection Systems</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites-and-environment-setup">Prerequisites and Environment Setup</h2>
<p>To follow this practical guide and run the implementation code locally, you should meet the following baseline requirements:</p>
<ul>
<li><p>Proficiency in writing clean, structured Python code.</p>
</li>
<li><p>A foundational understanding of Retrieval-Augmented Generation (RAG) concepts and vector embeddings.</p>
</li>
<li><p>Python 3.9 or higher installed on your computer.</p>
</li>
<li><p>An integrated development environment such as Visual Studio Code.</p>
</li>
</ul>
<h3 id="heading-package-installation">Package Installation</h3>
<p>Open your terminal and execute the following command to install the necessary external dependencies:</p>
<pre><code class="language-python">pip install openai sentence-transformers numpy python-dotenv
</code></pre>
<h3 id="heading-local-directory-structure">Local Directory Structure</h3>
<p>Organize your workspace with a clean structure to keep execution reproducible:</p>
<pre><code class="language-python">uncertainty-engine/

│

├── .env

├── README.md

└── app.py
</code></pre>
<h3 id="heading-environment-configuration">Environment Configuration</h3>
<p>Create a .env file in the root directory of your project to store access credentials and threshold configurations:</p>
<p>Code snippet</p>
<pre><code class="language-python">OPENAI_API_KEY=your_actual_api_key_here
ENVIRONMENT=development
CONFIDENCE_THRESHOLD=0.75
</code></pre>
<h2 id="heading-the-challenge-addressing-the-overconfidence-vulnerability">The Challenge: Addressing the Overconfidence Vulnerability</h2>
<p>Standard LLMs lack an internal mechanism to declare "I don't know." When a RAG application encounters missing documentation or receives an out-of-scope query, the core model treats the missing data as a text-completion puzzle to be solved at all costs.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a36420b52a5a7620def7e19/74cab403-4eda-40e2-8d2f-25030bcc034f.png" alt="Figure 1: Vulnerability Architecture of Standard LLM Pipelines" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>Figure 1: Vulnerability architecture of standard LLM pipelines, showing an ambiguous/out of scope request, followed by naive prompt execution, followed by confident hallucination.</p>
<p>Relying on system prompts like <em>"Only answer if you are 100% sure"</em> is ineffective because models easily bypass system prompt constraints when predicting token sequences. Enterprise systems require deterministic code boundaries that evaluate semantic relevance, document distance, and token probabilities independently of the LLM's raw output.</p>
<h2 id="heading-understanding-the-enterprise-request-lifecycle-for-uncertainty-evaluation">Understanding the Enterprise Request Lifecycle for Uncertainty Evaluation</h2>
<p>To prevent uncalibrated outputs, we intercept requests using a deterministic request lifecycle. Every transaction travels through three validation layers before a final output is sent to the end user:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a36420b52a5a7620def7e19/9b518dd5-77b7-4a2e-8311-559b89cba950.png" alt="Figure 2: Safe Enterprise LLM Architecture with Fallback Escalation" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>Figure 2: Safe enterprise LLM architecture with fallback escalation, showing a user request passing through input boundary validation, retrieval quality assessment, and output uncertainty checks before a response is delivered.</p>
<p>By decoupling safety decisions from the LLM, your code acts as the decision-making boundary while the language model operates strictly as an analytical generation engine.</p>
<h3 id="heading-step-1-implementing-layer-1-input-intent-amp-boundary-detection">Step 1: Implementing Layer 1 – Input Intent &amp; Boundary Detection</h3>
<p>The first defensive layer determines whether an incoming query falls within your system's valid domain parameters before calling retrieval pipelines or model APIs.</p>
<pre><code class="language-python">import numpy as np
from sentence_transformers import SentenceTransformer

class BoundaryDetector:
    def __init__(self, target_domains: list, similarity_threshold: float = 0.45):
        self.encoder = SentenceTransformer("all-MiniLM-L6-v2")
        self.domain_embeddings = self.encoder.encode(target_domains)
        self.threshold = similarity_threshold

    def verify_domain_relevance(self, query: str) -&gt; dict:
        query_vector = self.encoder.encode([query])
        
        # Calculate cosine similarity against domain boundaries
        similarities = np.dot(self.domain_embeddings, query_vector.T) / (
            np.linalg.norm(self.domain_embeddings, axis=1, keepdims=True) * np.linalg.norm(query_vector)
        )
        max_similarity = float(np.max(similarities))
        
        if max_similarity &lt; self.threshold:
            return {
                "is_valid": False,
                "score": round(max_similarity, 4),
                "reason": "Query falls outside operational domain boundaries."
            }
            
        return {
            "is_valid": True,
            "score": round(max_similarity, 4),
            "reason": "Query verified within target operational scope."
        }

if __name__ == "__main__":
    approved_topics = [
        "company VPN configuration",
        "employee payroll schedules",
        "internal IT software deployment"
    ]
    detector = BoundaryDetector(target_domains=approved_topics)
    out_of_scope_query = "What is the optimal baking temperature for sourdough bread?"
    result = detector.verify_domain_relevance(out_of_scope_query)
    print(f"Domain Validation Result: {result}")
</code></pre>
<p>This module converts approved operational topics into semantic vector embeddings. When a user submits a query, the script converts the input into an embedding vector and calculates its cosine similarity against defined domain bounds. If the alignment score sits below the threshold, the request stops immediately, saving API compute costs and preventing out-of-domain guessing.</p>
<h3 id="heading-step-2-implementing-layer-2-semantic-distance-amp-retrieval-quality-scoring">Step 2: Implementing Layer 2 – Semantic Distance &amp; Retrieval Quality Scoring</h3>
<p>RAG platforms routinely hallucinate because vector retrieval engines return low-scoring document matches when relevant context is missing. We measure the semantic distance between the query and retrieved context chunks to verify retrieval quality</p>
<pre><code class="language-python">class RetrievalQualityScorer:
    def __init__(self, minimum_relevance: float = 0.60):
        self.encoder = SentenceTransformer("all-MiniLM-L6-v2")
        self.min_relevance = minimum_relevance

    def evaluate_retrieved_context(self, user_query: str, retrieved_chunks: list) -&gt; tuple:
        if not retrieved_chunks:
            return False, 0.0

        query_vec = self.encoder.encode(user_query)
        chunk_vecs = self.encoder.encode(retrieved_chunks)

        # Compute cosine similarity across retrieved chunks
        scores = np.dot(chunk_vecs, query_vec) / (
            np.linalg.norm(chunk_vecs, axis=1) * np.linalg.norm(query_vec)
        )
        top_score = float(np.max(scores))

        is_sufficient = top_score &gt;= self.min_relevance
        return is_sufficient, round(top_score, 4)

if __name__ == "__main__":
    scorer = RetrievalQualityScorer()
    sample_query = "How do I configure mutual TLS for gRPC services?"
    sample_context = [
        "Standard deployment uses isolated network clusters with automated releases."
    ]
    has_context, score = scorer.evaluate_retrieved_context(sample_query, sample_context)
    print(f"Context Sufficient: {has_context} | Top Match Score: {score}")
</code></pre>
<p>This step converts retrieved document chunks into vector embeddings alongside the user query to compute individual similarity scores. If the highest-scoring chunk fails to cross the minimum relevance threshold, the module flags the context as insufficient, blocking the system from sending irrelevant text to the model.</p>
<h3 id="heading-step-3-implementing-layer-3-probabilistic-logit-analysis-amp-output-validation">Step 3: Implementing Layer 3 – Probabilistic Logit Analysis &amp; Output Validation</h3>
<p>The final layer inspects token generation probabilities (log probabilities) returned by model APIs. When an LLM is unsure of its answers, token distribution entropy increases, revealing uncertainty directly in the API payload.</p>
<pre><code class="language-python">import math

class OutputLogprobValidator:
    def __init__(self, logprob_threshold: float = -0.35):
        self.threshold = logprob_threshold

    def evaluate_token_certainty(self, token_logprobs: list) -&gt; dict:
        if not token_logprobs:
            return {"is_confident": False, "avg_logprob": -1.0, "perplexity": 999.0}

        avg_logprob = sum(token_logprobs) / len(token_logprobs)
        perplexity = math.exp(-avg_logprob)
        is_confident = avg_logprob &gt;= self.threshold

        return {
            "is_confident": is_confident,
            "avg_logprob": round(avg_logprob, 4),
            "perplexity": round(perplexity, 4)
        }

if __name__ == "__main__":
    validator = OutputLogprobValidator()
    # Simulated logprob arrays from an API output
    unconfident_logprobs = [-0.12, -0.85, -1.20, -0.45, -0.95]
    result = validator.evaluate_token_certainty(unconfident_logprobs)
    print(f"Generation Certainty Assessment: {result}")
</code></pre>
<p>This class processes the raw log probabilities of generated tokens to compute an average logprob metric alongside text perplexity. By comparing this value against a calibrated threshold, the application objectively determines whether the model was uncertain during text generation.</p>
<h3 id="heading-integrating-the-verification-layers-into-a-single-pipeline">Integrating the Verification Layers into a Single Pipeline</h3>
<p>We now unify these three isolated verification modules into a single orchestration engine that governs the enterprise request pipeline end-to-end.</p>
<pre><code class="language-python">class EnterpriseUncertaintyEngine:
    def __init__(self, approved_domains: list):
        self.boundary_layer = BoundaryDetector(target_domains=approved_domains)
        self.retrieval_layer = RetrievalQualityScorer()
        self.output_layer = OutputLogprobValidator()

    def process_request(self, user_query: str, retrieved_docs: list) -&gt; str:
        print(f"\n--- Processing Query: '{user_query}' ---")

        # Check 1: Input Boundary Evaluation
        boundary_result = self.boundary_layer.verify_domain_relevance(user_query)
        if not boundary_result["is_valid"]:
            return f"Request Rejected: {boundary_result['reason']}"
        print("[Pass] Input verified within operational domain.")

        # Check 2: Retrieval Context Quality
        valid_context, ret_score = self.retrieval_layer.evaluate_retrieved_context(user_query, retrieved_docs)
        if not valid_context:
            return f"Escalated: Insufficient ground-truth data retrieved (Score: {ret_score}). Routing to support team."
        print(f"[Pass] Context quality validated (Score: {ret_score}).")

        # Step 3: Simulated LLM Generation &amp; Logprob Verification
        # In production, replace dummy logprobs with actual API responses
        simulated_logprobs = [-0.08, -0.05, -0.12, -0.04]
        certainty = self.output_layer.evaluate_token_certainty(simulated_logprobs)

        if not certainty["is_confident"]:
            return "Fallback Active: Generated output exhibited low token certainty."
        print(f"[Pass] Output probability verified (Avg Logprob: {certainty['avg_logprob']}).")

        return "Response Generated: Navigate to portal.company.internal to reset your VPN credentials."

if __name__ == "__main__":
    domains = ["VPN credentials", "software provisioning", "network settings"]
    engine = EnterpriseUncertaintyEngine(approved_domains=domains)

    # Test Case: Query with valid retrieved context
    context_data = ["To update VPN credentials, access portal.company.internal."]
    final_output = engine.process_request("How do I update my VPN password?", context_data)
    print(f"System Output: {final_output}")
</code></pre>
<p>This orchestration class combines input validation, retrieval scoring, and logprob checking into a single execution workflow. It routes requests through each verification checkpoint sequentially, blocking out-of-domain queries, escalating under-retrieved contexts to human support, and filtering low-probability generations.</p>
<h2 id="heading-operational-insights-from-running-uncertainty-detection-systems">Operational Insights from Running Uncertainty Detection Systems</h2>
<p>Designing uncertainty-aware LLM architectures yields several practical deployment lessons:</p>
<ul>
<li><p><strong>Decouple confidence checks from system prompts:</strong> Avoid asking the model <em>"Are you confident in this answer?"</em> inside prompt context. Models frequently generate high self-reported confidence for incorrect statements. Use mathematical indicators like logprobs and vector distances instead.</p>
</li>
<li><p><strong>Establish clear escalation workflows:</strong> Treat "I don't know" as an intentional operational outcome rather than a code failure. Route low-confidence queries directly to internal ticketing queues or human-in-the-loop (HITL) review channels.</p>
</li>
<li><p><strong>Monitor retrieval metrics for knowledge gaps:</strong> Track and aggregate requests that fail retrieval scoring. Low-relevance metrics highlight missing, outdated, or poorly indexed corporate documentation.</p>
</li>
<li><p><strong>Tune similarity thresholds continuously:</strong> Embedding distance metrics are sensitive to document length and vocabulary choices. Periodically evaluate sample system logs to adjust relevance boundaries for optimal precision.</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Building production-grade AI applications requires transitioning from naïve prompt engineering to a security-first engineering mindset. While Large Language Models provide powerful natural language capabilities, they're uncalibrated tools that can't natively measure truth or certainty.</p>
<p>By wrapping models in deterministic code boundaries that evaluate input intent, document relevance, and generation probabilities, you transform an unpredictable language model into a reliable enterprise platform: one that delivers helpful answers when confident and knows exactly when to say "I don't know."</p>
<p>Thank you for reading.</p>
<p>I hope this guide offers a clear framework for building uncertainty-aware AI applications within your enterprise environments.</p>
<p>If you would like to discuss AI engineering, Agentic architectures, LLM ops, or AI governance, feel free to connect with me:</p>
<ul>
<li><p>Connect with me on <a href="https://www.linkedin.com/in/chidiebere-njoku-921579142/">LinkedIn</a></p>
</li>
<li><p>Explore my projects on <a href="https://github.com/ChidiebereNjoku?tab=repositories">GitHub</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How AI Chips Are Made ]]>
                </title>
                <description>
                    <![CDATA[ We just published a comprehensive, first-principles course on the semiconductor supply chain on the freeCodeCamp.org YouTube channel. Taught by hardware specialist Kian Kyars, this deep dive explains  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-ai-chips-are-made/</link>
                <guid isPermaLink="false">6a9995f0970203e770d14c8d</guid>
                
                    <category>
                        <![CDATA[ ai chips ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Thu, 03 Sep 2026 15:44:48 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5f68e7df6dfc523d0a894e7c/ef98b64b-db1c-4b54-adca-58a21514b343.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>We just published a comprehensive, first-principles course on the semiconductor supply chain on the freeCodeCamp.org YouTube channel. Taught by hardware specialist Kian Kyars, this deep dive explains the complex journey of an AI accelerator from raw silicon all the way to modern data center deployment.</p>
<p>As capital expenditure on AI infrastructure increases into the hundreds of billions, hardware is no longer a niche topic reserved only for chip architects. Understanding the physical machinery running modern models has become important for developers, researchers, and tech professionals alike.</p>
<p>The course focusses on Nvidia's latest hardware, tracing how a dual-die GB300 Blackwell Ultra GPU is designed, fabricated, packaged, and networked. Across six structured modules, Kian breaks down:</p>
<ul>
<li><p><strong>Semiconductor Physics &amp; Scaling</strong><br>How planar, FinFET, and Gate-All-Around (GAA) architectures work, why GPUs run at lower clock frequencies than CPUs, and how the end of Dennard scaling altered processor design.</p>
</li>
<li><p><strong>Design &amp; EDA</strong><br>The step-by-step pipeline converting Register-Transfer Level (RTL) code into manufacturable mask sets using electronic design automation toolchains like Synopsys, Cadence, and Siemens EDA.</p>
</li>
<li><p><strong>Fabrication &amp; Cleanrooms</strong><br>The mechanics inside leading-edge fabs like TSMC, wafer handling, high-purity North Carolina quartz, and the math behind Poisson defect yield models.</p>
</li>
<li><p><strong>Lithography &amp; Equipment</strong><br>How ASML’s extreme ultraviolet (EUV) scanners bounce 13.5 nm light off atom-flat Carl Zeiss mirrors to pattern features just nanometers wide.</p>
</li>
<li><p><strong>Memory &amp; Advanced Packaging</strong><br>High Bandwidth Memory (HBM3E/HBM4) integration, Through-Silicon Vias (TSVs), and TSMC’s CoWoS-L packaging that binds compute dies together.</p>
</li>
<li><p><strong>Rack-Scale Systems &amp; Geopolitics</strong><br>Scale-up NVLink fabrics, 135 kW power boundaries, export restrictions, and critical single-point-of-failure choke points across the global ecosystem.</p>
</li>
</ul>
<p>You can watch the full course for free <a href="https://youtu.be/FGT7LZbZe-g">on the freeCodeCamp.org YouTube channel</a> (3-hour watch).</p>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/FGT7LZbZe-g" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How OpenTelemetry Works: A Complete Guide ]]>
                </title>
                <description>
                    <![CDATA[ If you’re a software developer or DevOps engineer, you've probably come across OpenTelemetry. It comes up a lot, especially when talking about observability, monitoring, or debugging distributed syste ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-opentelemetry-works/</link>
                <guid isPermaLink="false">6a999540c11d1e5beaccfefc</guid>
                
                    <category>
                        <![CDATA[ OpenTelemetry ]]>
                    </category>
                
                    <category>
                        <![CDATA[ observability ]]>
                    </category>
                
                    <category>
                        <![CDATA[ distributed tracing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Devops ]]>
                    </category>
                
                    <category>
                        <![CDATA[ cloud native ]]>
                    </category>
                
                    <category>
                        <![CDATA[ monitoring ]]>
                    </category>
                
                    <category>
                        <![CDATA[ OTLP ]]>
                    </category>
                
                    <category>
                        <![CDATA[ opentelemetry collector ]]>
                    </category>
                
                    <category>
                        <![CDATA[ metrics ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Logs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ traces ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chosen Vincent ]]>
                </dc:creator>
                <pubDate>Thu, 03 Sep 2026 15:41:52 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/32307170-27b3-463c-bae9-da3dbfd2a634.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you’re a software developer or DevOps engineer,&nbsp;you've probably come across OpenTelemetry. It comes up a lot, especially when talking about observability, monitoring, or debugging distributed systems.</p>
<p>You might even know the basic definition, but knowing what OpenTelemetry is vs how it actually works are two different things.</p>
<p>By the end of this guide, you'll understand how OpenTelemetry works end-to-end, from the moment a request enters your application to the moment you can see it in your observability backend. You'll learn how traces, spans, context propagation, and exporters all fit together into one pipeline.</p>
<p>If you're completely new to OpenTelemetry, don't worry: the next section will get you up to speed before we go any further.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-is-opentelemetry">What is OpenTelemetry?</a></p>
</li>
<li><p><a href="#heading-how-opentelemetry-works">How OpenTelemetry Works</a></p>
<ul>
<li><p><a href="#heading-step-1-instrument-your-application">Step 1: Instrument Your Application</a></p>
</li>
<li><p><a href="#heading-step-2-opentelemetry-creates-telemetry-signals">Step 2: OpenTelemetry Creates Telemetry Signals</a></p>
</li>
<li><p><a href="#heading-step-3-traces-follow-requests-through-your-application">Step 3: Traces Follow Requests Through Your Application</a></p>
</li>
<li><p><a href="#heading-step-4-context-propagation-connects-work-across-services">Step 4: Context Propagation Connects Work Across Services</a></p>
</li>
<li><p><a href="#heading-step-5-the-opentelemetry-sdk-processes-the-telemetry">Step 5: The OpenTelemetry SDK Processes the Telemetry</a></p>
</li>
<li><p><a href="#heading-step-6-exporters-send-the-telemetry">Step 6: Exporters Send the Telemetry</a></p>
</li>
<li><p><a href="#heading-step-7-the-opentelemetry-collector-receives-and-processes-the-data">Step 7: The OpenTelemetry Collector Receives and Processes the Data</a></p>
</li>
<li><p><a href="#heading-step-8-an-observability-backend-stores-and-analyzes-the-telemetry">Step 8: An Observability Backend Stores and Analyzes the Telemetry</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-putting-the-opentelemetry-flow-together">Putting the OpenTelemetry Flow Together</a></p>
</li>
<li><p><a href="#heading-do-you-need-every-opentelemetry-component">Do You Need Every OpenTelemetry Component?</a></p>
</li>
<li><p><a href="#heading-why-use-opentelemetry">Why Use OpenTelemetry?</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-opentelemetry">What is OpenTelemetry?</h2>
<p><a href="https://opentelemetry.io/docs/">OpenTelemetry</a> is an open-source, vendor-neutral observability framework. It gives you a standard way to instrument your application, generate telemetry data, and export that data to any observability backend of your choice.</p>
<p>Before OpenTelemetry, every monitoring tool had its own way of collecting data. If you used Datadog, you’ll have to instrument your app the Datadog way. If you switched to Jaeger, you started over. OpenTelemetry changed that by giving you one standard way to instrument your application, regardless of which backend you use</p>
<p>[!NOTE] OpenTelemetry is not a monitoring platform, dashboard, or data store. It provides the tools and standards for collecting and exporting telemetry from your application to an observability backend, where the data can be stored, queried, and analyzed.</p>
<p>The data OpenTelemetry collects is called telemetry. It's the information your application produces about itself as it runs, and it comes in three forms:</p>
<ul>
<li><p><strong>Traces</strong> tell you how a request traveled through your system.</p>
</li>
<li><p><strong>Metrics</strong> give you numbers, like how many requests per second your app is handling, or how much memory it's using.</p>
</li>
<li><p><strong>Logs</strong> are timestamped records of specific events that happened inside your application.</p>
</li>
</ul>
<h2 id="heading-how-opentelemetry-works">How OpenTelemetry Works</h2>
<p>When a request hits your application, a lot happens behind the scenes. OpenTelemetry's job is to capture all of that activity (the traces, metrics, and logs) and send them to the right place.</p>
<p>Here’s what it looks like:</p>
<img src="https://cdn.hashnode.com/uploads/covers/62bc299d9c913efac56c91a4/238d33f9-2a15-49b4-993b-43c073a48305.png" alt="A flow diagram showing the six stages of the OpenTelemetry pipeline: the application is instrumented, telemetry is processed by the SDK, sent through an exporter, received by the Collector, and finally stored in an observability backend." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>Each stage has a specific job. Let's walk through them one by one.</p>
<h3 id="heading-step-1-instrument-your-application">Step 1: Instrument Your Application</h3>
<p>Before OpenTelemetry can capture anything, your application needs to be instrumented. Instrumentation is simply the process of adding code that tells OpenTelemetry what to watch and what to record.</p>
<p>There are two ways to instrument your application: automatically or manually.</p>
<h4 id="heading-1-automatic-instrumentation">1. Automatic instrumentation</h4>
<p>This is the easiest one to start with. You add a library to your project, and it instruments your application for you with no changes to your existing code.</p>
<p>For example, if you're running a Node.js Express app, you can add the OpenTelemetry auto-instrumentation package, and it will automatically start capturing incoming HTTP requests, outgoing calls, database queries, and more.</p>
<p>Here's what that setup looks like:</p>
<pre><code class="language-javascript">const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');

const sdk = new NodeSDK({
&nbsp; instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();
</code></pre>
<p>Once this runs before your app starts, OpenTelemetry begins capturing telemetry automatically. For a complete setup guide, see <a href="https://opentelemetry.io/docs/languages/js/getting-started/nodejs/">Getting started with OpenTelemetry in Node.js</a>.</p>
<h4 id="heading-2-manual-instrumentation">2. Manual instrumentation</h4>
<p>Automatic instrumentation covers a lot, but it can't capture everything that happens inside your own code. If you want to track what happens inside a specific function, like how long it takes to process a payment or validate a user, you need to add that yourself.</p>
<p>Here's a simple example. Let’s say you have a function that processes an order:</p>
<pre><code class="language-javascript">function processOrder(orderId) {
  // processing logic
}
</code></pre>
<p>With manual instrumentation, you wrap it like this:</p>
<pre><code class="language-javascript">const { trace } = require('@opentelemetry/api');

const tracer = trace.getTracer('order-service');

function processOrder(orderId) {
  return tracer.startActiveSpan('processOrder', (span) =&gt; {
    // processing logic

    span.end();
  });
}
</code></pre>
<p>What happens is that you created a span. That span now records when <code>processOrder</code> started, when it ended, and how long it took. You'll learn more about spans in Step 3.</p>
<p>For the full manual instrumentation reference, see <a href="https://opentelemetry.io/docs/languages/js/instrumentation/#traces">OpenTelemetry JavaScript instrumentation</a>.</p>
<h3 id="heading-step-2-opentelemetry-creates-telemetry-signals">Step 2: OpenTelemetry Creates Telemetry Signals</h3>
<p>Once your application is instrumented, OpenTelemetry starts producing telemetry data about what your application is doing. That data comes in three forms, called signals, which we've already briefly talked about: <a href="https://opentelemetry.io/docs/concepts/signals/traces/">traces</a>, <a href="https://opentelemetry.io/docs/concepts/signals/metrics/">metrics</a>, and <a href="https://opentelemetry.io/docs/concepts/signals/logs/">logs</a>. Each signal answers a different kind of observability question.</p>
<p><strong>Traces</strong> show you how a request moved through your system, which services it touched, and how long each step took. <strong>Metrics</strong> give you numbers over time, things like request rate, error rate, and memory usage. <strong>Logs</strong> are timestamped records of specific events that happened inside your application.</p>
<p>Here's a quick comparison:</p>
<table>
<thead>
<tr>
<th>Signal</th>
<th>What to shows</th>
<th>Example</th>
<th>Question it answers</th>
</tr>
</thead>
<tbody><tr>
<td>Trace</td>
<td>The journey of a request through your system</td>
<td>A checkout request passing through your API, order service, and database</td>
<td>Why is this request slow? Where did it fail?</td>
</tr>
<tr>
<td>Metric</td>
<td>A measured value over time</td>
<td>200 requests per second, 95ms average response time</td>
<td>Is my application healthy right now?</td>
</tr>
<tr>
<td>Log</td>
<td>A record of a specific event</td>
<td><code>ERROR: payment failed for order #1234</code></td>
<td>What exactly happened at this point in time?</td>
</tr>
</tbody></table>
<p>You don't have to choose between them. In practice, you'll use all three together. A metric tells you something is wrong, a trace shows you where, and a log tells you exactly what happened.</p>
<h3 id="heading-step-3-traces-follow-requests-through-your-application">Step 3: Traces Follow Requests Through Your Application</h3>
<p>When a user sends a request to your application, that request usually touches multiple services before a response comes back. A trace is the complete record of that journey, from the moment the request enters your system to the moment it finishes.</p>
<p>A trace is actually made up of smaller units called <strong>spans</strong>. Each span represents one operation, like an API call, a database query, or a function execution, and together they give you the full picture of what happened.</p>
<p>Every trace gets a unique trace ID, and every span gets its own span ID. The trace ID is what links all the spans together. No matter how many services a request passes through, they all share the same trace ID, so you can follow the request from start to finish in your observability backend.</p>
<p>Here's a simple example. A user places an order, and the request flows through four services:</p>
<img src="https://cdn.hashnode.com/uploads/covers/62bc299d9c913efac56c91a4/f1672972-b0d7-4a60-964a-7583946affad.png" alt="Trace tree diagram showing four spans under trace ID abc123: API Gateway (0–5ms), Order Service (5–20ms), Payment Service (20–45ms), and Database (45–50ms)." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>Each span has a start time and an end time, so you can see how long each operation took. If something slowed down or failed, you can pinpoint exactly where it happened just by looking at the spans.</p>
<h3 id="heading-step-4-context-propagation-connects-work-across-services">Step 4: Context Propagation Connects Work Across Services</h3>
<p>In Step 3, you saw how a single trace is made up of spans from multiple services. But here's a question you need to ask: how does OpenTelemetry know that a span in your payment service belongs to the same trace as a span in your order service?</p>
<p>Without something connecting them, each service would record its own spans independently. Your API gateway would see one operation, your order service would see another, and your payment service would see a third. They'd look like completely separate requests with no relationship to each other, which makes debugging across services nearly impossible.</p>
<p>That's where context propagation comes in. As a request moves from one service to another, OpenTelemetry attaches the trace context to it, typically as HTTP headers. That context carries the trace ID and the parent span ID, so every service that handles the request knows which trace it belongs to and where it sits in the chain.</p>
<p>Here's what that looks like in practice:</p>
<img src="https://cdn.hashnode.com/uploads/covers/62bc299d9c913efac56c91a4/526efe4d-2d79-4cdb-b42d-80d8b304312a.png" alt="Context propagation diagram showing trace ID abc123 traveling across API Gateway (span-id: 001), Order Service (span-id: 002), and Payment Service (span-id: 003) via HTTP headers." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>All three services share the same trace ID. That's what lets your observability backend connect the spans together into one complete trace.</p>
<p>OpenTelemetry doesn't invent its own rules for this. It follows the <a href="https://opentelemetry.io/docs/concepts/context-propagation/#propagation">W3C Trace Context</a> standard, a widely adopted specification that defines how trace context should be formatted and passed between services, so it works consistently across different languages, frameworks, and vendors.</p>
<p>The good news is that if you're using automatic instrumentation, context propagation happens automatically. OpenTelemetry handles the headers for you, so you don't have to think about it unless you're working with a custom transport or a non-standard setup.</p>
<h3 id="heading-step-5-the-opentelemetry-sdk-processes-the-telemetry">Step 5: The OpenTelemetry SDK Processes the Telemetry</h3>
<p>At this point, OpenTelemetry is capturing telemetry and keeping traces connected across services. But between the moment a span is created and the moment it leaves your application, something has to process it. That's the SDK's job.</p>
<p>When your instrumented code creates a span, it does that through the OpenTelemetry API. The API is what you interact with as a developer, things like <code>trace.getTracer()</code> and <code>tracer.startActiveSpan()</code>. But the API alone doesn't process or send anything. It needs the SDK behind it to actually do the work.</p>
<img src="https://cdn.hashnode.com/uploads/covers/62bc299d9c913efac56c91a4/e35443fa-2eff-461d-983a-d6bf3bcd5163.png" alt="A flow diagram showing what happens inside OpenTelemetry before data leaves your application: instrumentation creates telemetry, the API receives it, the SDK processes it, a processor prepares it, and the exporter sends it out." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>Once the SDK receives the telemetry, it runs it through a processor. The processor is responsible for things like batching spans together before sending them, adding extra attributes, or filtering out data you don't need. The most common one you'll see is the <code>BatchSpanProcessor</code>, which groups spans and exports them in batches rather than one at a time, making it more efficient in production.</p>
<p>Before the processor even runs, the SDK also handles sampling. Sampling lets you control how much telemetry you actually collect. In high-traffic applications, recording every single span would generate an enormous amount of data. With sampling, you can tell the SDK to only capture a percentage of traces, which keeps your costs and data volume manageable without losing visibility.</p>
<p>Once the processor is done, it hands the data to the exporter, which is what actually sends it to its destination. You'll see how that works in the next step.</p>
<h3 id="heading-step-6-exporters-send-the-telemetry">Step 6: Exporters Send the Telemetry</h3>
<p>The exporter's job is simple: take the telemetry the SDK prepared and send it to whatever destination you've configured.</p>
<p>OpenTelemetry uses OTLP, the OpenTelemetry Protocol, to transport telemetry data. It's a standard wire protocol designed specifically for transmitting traces, metrics, and logs, and it runs over either HTTP or gRPC.</p>
<p>[!NOTE] OTLP and OpenTelemetry are not the same thing. OpenTelemetry is the full framework, covering instrumentation, the SDK, the Collector, and more. OTLP is just the protocol it uses to transport data.</p>
<p>Although OTLP is the default, not every exporter uses it. Some exporters send data directly to specific backends in their own format, like Jaeger or Prometheus. So depending on your setup, you might use an OTLP exporter to send data to a Collector or backend, or a vendor-specific exporter to send it directly."</p>
<h3 id="heading-step-7-the-opentelemetry-collector-receives-and-processes-the-data">Step 7: The OpenTelemetry Collector Receives and Processes the Data</h3>
<p>The OpenTelemetry Collector is a standalone service that sits between your application and your observability backend. It receives telemetry data, processes it, and forwards it to one or more destinations.</p>
<p>Using the Collector is common but not mandatory. You can configure your exporter to send data directly to your backend and skip the Collector entirely. But in most production setups, teams add a Collector because it gives them a central place to manage telemetry, without touching application code.</p>
<p>The Collector has three stages:</p>
<ul>
<li><p><strong>Receivers</strong> accept incoming telemetry from your applications, typically over OTLP.</p>
</li>
<li><p><strong>Processors</strong> transform the data at the Collector level, things like batching spans, filtering out noise, or adding attributes before forwarding.</p>
</li>
<li><p><strong>Exporters</strong> send the processed data to your backend, or multiple backends if needed.</p>
</li>
</ul>
<p>Here's a minimal Collector configuration:</p>
<pre><code class="language-yaml">receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  batch:

exporters:
  otlphttp:
    endpoint: https://your-backend.com

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlphttp]
</code></pre>
<p>This config accepts traces over gRPC, batches them for efficiency, and forwards them to an observability backend over HTTP.</p>
<p>The Collector can also receive data from multiple applications simultaneously and route it to one or more destinations. So instead of each application shipping telemetry directly to your backend, they all send it to the Collector, and the Collector handles the rest.</p>
<p>For the full configuration, see the <a href="https://opentelemetry.io/docs/collector/configuration/">OpenTelemetry Collector configuration docs</a>.</p>
<h3 id="heading-step-8-an-observability-backend-stores-and-analyzes-the-telemetry">Step 8: An Observability Backend Stores and Analyzes the Telemetry</h3>
<p>This is where OpenTelemetry's job ends. Once your telemetry leaves the Collector, it arrives at your observability backend.</p>
<p>The backend is what stores your data, lets you query it, and gives you the dashboards and alerts you actually interact with day to day. OpenTelemetry doesn't provide any of that. It gets the data there, and the backend does the rest.</p>
<p>A few popular backends that support OpenTelemetry natively:</p>
<ul>
<li><p><strong>Open-source:</strong> Jaeger, Prometheus, Grafana Tempo</p>
</li>
<li><p><strong>Commercial:</strong> Datadog, New Relic, Honeycomb, Dynatrace, Elastic, Lightstep, Grafana Cloud, Middleware.</p>
</li>
</ul>
<p>Once your data is in the backend, you can search through traces to debug a slow request, build dashboards to monitor your application's health, and set up alerts when something goes wrong.</p>
<h2 id="heading-putting-the-opentelemetry-flow-together">Putting the OpenTelemetry Flow Together</h2>
<p>Let’s assume a user initiates a bank transfer on a mobile banking app. The request hits your API gateway, and because your application is instrumented, OpenTelemetry immediately starts capturing what’s happening. It creates a span for the incoming request and assigns it a trace ID.</p>
<p>As the request moves to the authentication service, context propagation carries that trace ID along in the request headers. The authentication service creates its own span and attaches it to the same trace. The same thing happens when the authentication service calls the transaction service, and when the transaction service hits the database to process the transfer. Four services and four spans, with one trace ID connecting them all.</p>
<p>Meanwhile, the SDK processes the telemetry in the background, runs the spans through the batch processor, and hands them to the exporter. The exporter packages everything into OTLP and sends it to the Collector, which applies your processing rules and forwards it to your observability backend.</p>
<p>Here’s a summary of every component involved in that flow:</p>
<table>
<thead>
<tr>
<th><strong>Component</strong></th>
<th>Role</th>
</tr>
</thead>
<tbody><tr>
<td>Instrumentation</td>
<td>Captures what’s happening inside your application</td>
</tr>
<tr>
<td>API</td>
<td>Exposes the methods your code calls to create spans, metrics, and logs</td>
</tr>
<tr>
<td>SDK</td>
<td>Processes and prepares telemetry for export</td>
</tr>
<tr>
<td>Exporter</td>
<td>Packages and sends telemetry via OTLP</td>
</tr>
<tr>
<td>Collector</td>
<td>Receives, processes, and routes telemetry to your backend</td>
</tr>
<tr>
<td>Observability backend</td>
<td>Stores, queries, and visualizes your telemetry</td>
</tr>
</tbody></table>
<p>From that single transfer request, you now have a complete trace in your backend. When you open your dashboard and search the trace ID, you'll see every service, every span, and every millisecond of that transaction laid out in front of you.</p>
<h2 id="heading-do-you-need-every-opentelemetry-component">Do You Need Every OpenTelemetry Component?</h2>
<p>To be honest, you don’t need every component to get started with OpenTelemetry. The pipeline you’ve seen throughout this article is the full setup, but not every team uses all of that.</p>
<ol>
<li><p><strong>Without the Collector:</strong> Your exporter sends telemetry directly to your backend with nothing in between. Works well for smaller projects or when you're just getting started.</p>
</li>
<li><p><strong>With the Collector:</strong> The more common production setup. Teams add the Collector when they need more control, like routing telemetry to multiple backends, filtering sensitive data, or managing telemetry from dozens of services in one place.</p>
</li>
</ol>
<p>The Collector is powerful, but it’s not mandatory. Start without it if your setup is simple, and add it when you actually need it.</p>
<h2 id="heading-why-use-opentelemetry">Why Use OpenTelemetry?</h2>
<p>You've now seen how every piece of OpenTelemetry fits together. Here's why it's worth using:</p>
<ul>
<li><p><strong>Vendor-neutral instrumentation:</strong> Before OpenTelemetry, switching observability tools meant you had to re-instrument your entire application from scratch. With OpenTelemetry, you instrument once and switch backends freely.</p>
</li>
<li><p><strong>Consistent telemetry across services and languages:</strong> Your backend might be in Go, your microservices in Python, and your data pipeline in Java. OpenTelemetry has SDKs for all of them, and they all produce telemetry in the same format, so your whole stack speaks the same language.</p>
</li>
<li><p><strong>Correlated observability signals:</strong> Traces, metrics, and logs all flow through the same pipeline. So when something goes wrong, a spike in your metrics leads you to a trace, and that trace points you to the exact log entry where things broke down.</p>
</li>
<li><p><strong>It's becoming the industry standard:</strong> Most observability backends already support OpenTelemetry natively. Instead of learning a vendor-specific instrumentation approach every time you adopt a new tool, you learn OpenTelemetry once and it works everywhere.</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>OpenTelemetry gives you a standard way to instrument your application, collect telemetry, and ship it to any backend you choose, without locking you into a specific vendor or tool.</p>
<p>If you remember nothing else from this article, remember this: you instrument your application, generate telemetry, process it, export it, and analyze it. Every step has a component behind it, and now you know what each one does.</p>
<p>If you're just getting started, you don't need to set everything up at once. Start with instrumentation, get your telemetry flowing to a backend, and add the Collector when you need it. You can always add more as your needs grow.</p>
<p>If you found this helpful, I'd love to connect. You can find me on <a href="https://www.linkedin.com/in/chosenvincent1">LinkedIn</a> or <a href="https://x.com/ChosenVincent1">X</a>. Feel free to reach out if you have questions or just want to talk about observability and developer tooling.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use Skills in Agentic Flutter Development: A Handbook for Devs ]]>
                </title>
                <description>
                    <![CDATA[ One of the biggest misconceptions about AI-assisted development is that using AI means giving up the engineering experience you've built over the years. It doesn't. You can take the architecture patte ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-skills-in-agentic-flutter-development-a-handbook-for-devs/</link>
                <guid isPermaLink="false">6a9994ee30c9235bff67094c</guid>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Dart ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ flutter-aware ]]>
                    </category>
                
                    <category>
                        <![CDATA[ handbook ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ai-agent ]]>
                    </category>
                
                    <category>
                        <![CDATA[ skills ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Atuoha Anthony ]]>
                </dc:creator>
                <pubDate>Thu, 03 Sep 2026 15:40:30 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/aa0f5630-f617-4945-aa3a-5c962cb1609a.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>One of the biggest misconceptions about AI-assisted development is that using AI means giving up the engineering experience you've built over the years. It doesn't.</p>
<p>You can take the architecture patterns you've learned, the mistakes you've made, the conventions your team follows, and the standards you've developed as a Flutter engineer and teach them to your AI coding agent through agent skills. That means you don't have to choose between your experience and AI. You can bring both together.</p>
<p>But almost every Flutter developer feels a specific frustration the first time they use an AI coding agent on a real project.</p>
<p>You ask the agent to build a profile screen. It produces something that works. But instead of creating a clean, reusable <code>ProfileCard</code> widget in your <code>widgets/</code> folder, it writes a <code>_buildProfileCard()</code> private method buried inside the screen file.</p>
<p>Instead of separating concerns and placing the <code>StatefulWidget</code> and its state where your carefully designed file structure expects them, it appends both to the bottom of a file that already has ten classes.</p>
<p>The data model uses <code>Map&lt;String, dynamic&gt;</code> instead of your <code>freezed</code>-annotated classes. The imports skip your barrel files and reach directly into internal package paths. The theming ignores your design tokens and uses hardcoded hex values. The error handling uses raw strings instead of your typed failure hierarchy. The state management is Provider when your team uses Bloc.</p>
<p>None of this is wrong in an absolute sense. The agent didn't make mistakes because it's bad at Dart. It made mistakes because it doesn't know how your team writes Flutter code.</p>
<p>This is the problem that agent skills were built to solve.</p>
<p>Agent skills are structured Markdown files that teach an AI agent the "how" of a specific task, not just the "what." When an agent picks up a skill before generating code, it's equipped with your team's conventions, your architectural patterns, your file organization rules, your naming standards, and your quality expectations. The result is code that belongs in your project.</p>
<p>The Flutter team maintains an official repository of skills at <code>github.com/flutter/agent-plugins</code>, and the Dart team maintains a complementary set at <code>github.com/dart-lang/skills</code>. Together they cover responsive layouts, declarative routing, JSON serialization, unit testing, static analysis, package dependency resolution, pattern matching, and more.</p>
<p>But the most powerful skills are the ones you write yourself, the ones that encode your specific experiences as an engineer, your team's specific mistakes, and your project's specific patterns. A skill you write from your own production experience is worth ten generic ones, because it prevents the exact mistakes your team has actually made in the exact codebase your team maintains.</p>
<p>Skills work across every major AI coding agent. Whether your team uses Claude Code, Antigravity, OpenAI Codex, Cursor, GitHub Copilot CLI, or any other compatible agent, skills follow a universal standard. Write the skill once, and it works everywhere.</p>
<p>This handbook covers everything: what skills are, how they work internally, how to install the official Flutter and Dart skills, how to configure skills for each major agent, how to read and understand an existing skill deeply, and most importantly, how to write your own skills that genuinely improve AI output on your specific codebase.</p>
<p>It also covers the essential skills every Flutter team should have, the Dart skills every developer benefits from, and the advanced patterns that make skills compounding over time.</p>
<p>By the end, you won't just know how to use skills. You'll write them with the same intentionality you bring to writing clean Flutter code, and you'll understand why doing so is one of the highest-leverage investments you can make in your team's engineering quality.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-are-agent-skills">What Are Agent Skills?</a></p>
</li>
<li><p><a href="#heading-the-problem-why-ai-agents-get-flutter-wrong">The Problem: Why AI Agents Get Flutter Wrong</a></p>
</li>
<li><p><a href="#heading-how-skills-work-progressive-disclosure">How Skills Work: Progressive Disclosure</a></p>
</li>
<li><p><a href="#heading-the-anatomy-of-a-skill-file">The Anatomy of a Skill File</a></p>
</li>
<li><p><a href="#heading-installing-official-flutter-and-dart-skills">Installing Official Flutter and Dart Skills</a></p>
</li>
<li><p><a href="#heading-using-skills-with-claude-code">Using Skills with Claude Code</a></p>
</li>
<li><p><a href="#heading-using-skills-with-antigravity">Using Skills with Antigravity</a></p>
</li>
<li><p><a href="#heading-using-skills-with-openai-codex">Using Skills with OpenAI Codex</a></p>
</li>
<li><p><a href="#heading-using-skills-with-cursor">Using Skills with Cursor</a></p>
</li>
<li><p><a href="#heading-using-skills-with-other-agents">Using Skills with Other Agents</a></p>
</li>
<li><p><a href="#heading-the-official-flutter-skills-a-deep-dive">The Official Flutter Skills: A Deep Dive</a></p>
</li>
<li><p><a href="#heading-the-official-dart-skills-a-deep-dive">The Official Dart Skills: A Deep Dive</a></p>
</li>
<li><p><a href="#heading-the-flutter-file-organization-skill-a-complete-walkthrough">The flutter-file-organization Skill: A Complete Walkthrough</a></p>
</li>
<li><p><a href="#heading-writing-your-own-skills-the-complete-guide">Writing Your Own Skills: The Complete Guide</a></p>
</li>
<li><p><a href="#heading-essential-flutter-skills-every-team-should-have">Essential Flutter Skills Every Team Should Have</a></p>
</li>
<li><p><a href="#heading-essential-dart-skills-every-developer-should-write">Essential Dart Skills Every Developer Should Write</a></p>
</li>
<li><p><a href="#heading-skills-for-architecture-and-large-codebases">Skills for Architecture and Large Codebases</a></p>
</li>
<li><p><a href="#heading-advanced-skill-patterns">Advanced Skill Patterns</a></p>
</li>
<li><p><a href="#heading-package-level-skills-teaching-the-agent-your-libraries">Package-Level Skills: Teaching the Agent Your Libraries</a></p>
</li>
<li><p><a href="#heading-skills-vs-rules-vs-mcp-knowing-the-difference">Skills vs Rules vs MCP: Knowing the Difference</a></p>
</li>
<li><p><a href="#heading-organizing-skills-in-a-team">Organizing Skills in a Team</a></p>
</li>
<li><p><a href="#heading-best-practices-for-writing-skills">Best Practices for Writing Skills</a></p>
</li>
<li><p><a href="#heading-common-mistakes-when-writing-skills">Common Mistakes When Writing Skills</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-references">References</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before working through this guide, you should have the following in place.</p>
<h3 id="heading-1-flutter-and-dart-proficiency">1. Flutter and Dart proficiency</h3>
<p>You should be comfortable building multi-screen Flutter apps, working with state management patterns, and following basic clean architecture principles. You don't need to be a senior engineer, but the skill examples in this guide assume you know what a <code>StatefulWidget</code> is, what a repository pattern looks like, why sealed classes matter, and what <code>json_serializable</code> generates.</p>
<h3 id="heading-2-a-working-ai-coding-agent">2. A working AI coding agent</h3>
<p>Skills work with agents including Claude Code, Antigravity, OpenAI Codex, GitHub Copilot CLI, Cursor, and others. You need at least one of these installed and working. This guide covers agent-specific setup for all of them.</p>
<h3 id="heading-3-nodejs-installed">3. Node.js installed</h3>
<p>The <code>skills</code> CLI tool (used to install official skills) is distributed through npm. Run <code>node -v</code> to check. If Node.js isn't installed, download it from <a href="https://nodejs.org">nodejs.org</a>.</p>
<h3 id="heading-4-a-flutter-or-dart-project-to-work-with">4. A Flutter or Dart project to work with</h3>
<p>The examples and skill exercises in this guide work best when applied to a real project rather than followed abstractly.</p>
<h3 id="heading-5-basic-markdown-familiarity">5. Basic Markdown familiarity</h3>
<p>Skills are written in Markdown. You should know what a heading is (<code>##</code>), what a code block looks like (triple backticks), and what a YAML frontmatter block looks like (the <code>---</code> enclosed block at the top of a file).</p>
<p>You don't need any special tools beyond these. Skills are plain text files that live in a folder in your project. There's nothing to build, compile, or install beyond the initial CLI command.</p>
<h2 id="heading-what-are-agent-skills">What Are Agent Skills?</h2>
<p>Think about the difference between hiring a developer who knows Dart and hiring a developer who has worked on Flutter projects similar to yours for two years.</p>
<p>Both can write working Flutter code. But the experienced one knows things that aren't in any documentation: that your team always extracts widget sections into their own files rather than using private build methods, that you use a specific pattern for handling loading states, that your Bloc events are named as past-tense verbs, that you never use <code>BuildContext</code> inside async gaps without checking <code>mounted</code>, and that your team uses <code>fpdart</code> for <code>Either</code> types instead of throwing exceptions across layer boundaries.</p>
<p>A skill is how you give that experienced-developer knowledge to an AI agent. It's a document that describes not just what to do but how to do it, what to avoid, and why the rules exist.</p>
<p>Formally, agent skills provide a standardized way to give your AI agent a set of task-oriented blueprints to follow. By giving the agent actual domain expertise and repeatable workflows, you drastically reduce mistakes and can enforce consistent patterns.</p>
<p>The key word is task-oriented. A skill isn't a style guide. It's a set of instructions tied to a specific category of work.</p>
<h3 id="heading-the-universal-standard">The Universal Standard</h3>
<p>Skills follow a specification maintained at <a href="https://agentskills.io">agentskills.io</a>. This specification defines the file format (Markdown with YAML frontmatter), the directory location (<code>.agents/skills/</code>), and the naming conventions.</p>
<p>Because the specification is universal, the same skill files work across Claude Code, Cursor, Antigravity, Codex, and any other agent that follows the standard.</p>
<p>This portability matters for teams. You don't need to write separate skills for each agent. You write one skill, commit it to your repository, and every agent your team uses benefits from it immediately.</p>
<h3 id="heading-where-skills-live">Where Skills Live</h3>
<p>Skills live in the <code>.agents/skills/</code> directory of your project workspace. This is the standard location that all compatible agents discover automatically when they start working on a task.</p>
<pre><code class="language-plaintext">your_flutter_project/
  .agents/
    skills/
      flutter-file-organization.md
      flutter-state-management-bloc.md
      flutter-testing.md
      flutter-theming.md
      flutter-error-handling.md
      flutter-navigation.md
      flutter-feature-architecture.md
      dart-unit-testing.md
      dart-static-analysis.md
      dart-pattern-matching.md
  lib/
  android/
  ios/
  pubspec.yaml
</code></pre>
<p><code>.agents/skills/</code> is the convention established by the agent skills specification. When an agent starts a session on your project, it discovers this directory, indexes the skill files, reads their metadata to understand what capabilities are available, and loads full skill content only when a task matches a skill's description.</p>
<h3 id="heading-what-makes-skills-different-from-system-prompts-or-rules">What Makes Skills Different from System Prompts or Rules</h3>
<p>A one-time prompt tells the agent what you want right now, in this session. An AI rules file (like <code>.cursorrules</code> or <code>CLAUDE.md</code>) tells the agent project-wide facts that apply to every task. A skill teaches the agent how to perform a specific category of work correctly across all future requests, loaded only when relevant.</p>
<p>When you write a skill for Flutter file organization, you don't need to explain your conventions in the chat every session. Every time you or a teammate asks the agent to create, split, or refactor a Flutter file, the skill loads automatically and provides the same quality guidance. When a new developer joins the team and starts using an AI agent, they get the benefit of every skill the team has written from day one, without needing to be taught the team's standards manually.</p>
<h2 id="heading-the-problem-why-ai-agents-get-flutter-wrong">The Problem: Why AI Agents Get Flutter Wrong</h2>
<p>To understand why skills are necessary, you need to understand the specific and predictable ways AI agents fail at Flutter and Dart without them. These failures aren't random. They trace to a handful of root causes that skills are designed to address.</p>
<h3 id="heading-the-training-data-problem">The Training Data Problem</h3>
<p>An AI agent has knowledge of Dart and Flutter from its training data. That training data includes millions of lines of Flutter code from public repositories, documentation, tutorials, and forum answers. It includes old patterns (pre-null-safety Dart), bad patterns (God-class widgets), and patterns that are correct in isolation but wrong for a specific team's standards.</p>
<p>When an agent generates code without a skill, it draws on all of that mixed training data. It might generate code in the style of a 2021 tutorial that uses <code>setState</code> everywhere, or in the style of a repository that uses <code>ChangeNotifier</code> when your team uses Bloc, or it might use <code>Navigator.push</code> when your team carefully uses GoRouter for deep-linking support.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63a47b24490dd1c9cd9c32ff/4caaf86d-01e3-465d-b3f8-c372ad31b80c.png" alt="A two-part diagram comparing an AI agent without and with team skills. The top section shows broad training data flowing into patterns that may not fit the team. The bottom section shows the same training data combined with focused team rules for file organization, BLoC state management, error handling, theming, and testing, resulting in output that fits the existing codebase." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>Skills don't replace the agent's existing knowledge. They give it a clear engineering context. Without skills, the agent chooses from a broad mix of patterns with varying quality. With team-defined skills, those patterns are constrained by the project's architecture, conventions, and standards, making the resulting code more consistent with the existing codebase.</p>
<h3 id="heading-the-most-common-flutter-specific-failures">The Most Common Flutter-Specific Failures</h3>
<h4 id="heading-1-private-build-methods-instead-of-extracted-widgets">1. Private build methods instead of extracted widgets.</h4>
<p>An agent asked to build a complex screen nests private methods like <code>_buildHeader()</code>, <code>_buildStatsList()</code>, and <code>_buildActionBar()</code> inside the screen class. This is valid Dart but architecturally harmful: these sections should be separate, testable, reusable widget classes in a <code>widgets/</code> folder.</p>
<h4 id="heading-2-separating-statefulwidget-from-state">2. Separating StatefulWidget from State.</h4>
<p>When splitting a large file, an agent may move the <code>StatefulWidget</code> class to one file and the <code>State&lt;T&gt;</code> class to another. This breaks a fundamental Flutter compilation constraint. The two must always live in the same file.</p>
<h4 id="heading-3-ignoring-your-state-management-choice">3. Ignoring your state management choice.</h4>
<p>Without knowing your state management preference, the agent picks whatever pattern it finds most frequently in its training data. One session it generates Bloc. The next it generates Provider. The next it uses <code>setState</code>. All in the same codebase.</p>
<h4 id="heading-4-using-map-instead-of-typed-models">4. Using Map instead of typed models.</h4>
<p>Without knowing your serialization conventions, an agent defaults to <code>Map&lt;String, dynamic&gt;</code>. If your team uses <code>freezed</code> and <code>json_serializable</code>, every generated model needs to be completely rewritten.</p>
<h4 id="heading-5-hardcoded-visual-values">5. Hardcoded visual values.</h4>
<p>Agents default to literal values: <code>Color(0xFF6750A4)</code>, <code>EdgeInsets.all(16)</code>, and <code>BorderRadius.circular(8)</code>. If your project has a design system with theme extensions and spacing constants, the agent ignores it entirely.</p>
<h4 id="heading-6-inline-comments-everywhere">6. Inline comments everywhere.</h4>
<p>Many teams specifically avoid code comments in favor of self-documenting code with descriptive names. Agents default to adding explanatory comments because most training data includes them, requiring cleanup on every review.</p>
<h4 id="heading-7-wrong-import-paths">7. Wrong import paths.</h4>
<p>An agent may import from internal package paths (<code>package:myapp/src/internal/models/user.dart</code>) instead of going through your barrel files (<code>package:myapp/features/profile/profile.dart</code>), creating invisible coupling to internal APIs that should be hidden.</p>
<h4 id="heading-8-raw-exception-handling">8. Raw exception handling.</h4>
<p>Without knowing your error architecture, agents use <code>try-catch</code> with raw <code>Exception</code> objects everywhere, ignoring your team's typed failure hierarchy and making error handling inconsistent across the codebase.</p>
<h2 id="heading-how-skills-work-progressive-disclosure">How Skills Work: Progressive Disclosure</h2>
<p>The mechanism behind skills is elegant and efficient. Instead of loading every instruction into the context window upfront, the agent only reads the metadata first. It pulls in the heavy, detailed instructions only when it actually needs them for the task at hand.</p>
<p>The Flutter documentation describes this as "progressive disclosure," analogous to deferred loading in Flutter itself.</p>
<p>This design solves a real problem. An AI agent's context window isn't infinite. If every skill loaded its full content for every task, the agent would be burning context budget on irrelevant information. A navigation skill doesn't need to be in context when you're asking the agent to write unit tests. A testing skill doesn't need to be in context when you're setting up routing.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63a47b24490dd1c9cd9c32ff/a65999e8-d2e8-4468-9bcc-d82dce2da392.png" alt="A two-phase diagram explaining progressive disclosure for AI agent skills. Phase 1 shows the agent reading only the frontmatter from every skill file, keeping context usage lightweight. Phase 2 shows the agent matching a user request to relevant skills, loading their full content while excluding unrelated skills. The result is relevant expertise without unnecessary context usage." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>Progressive disclosure keeps the agent's context focused. First, the agent indexes the lightweight metadata of all available skills. When a task arrives, it uses those descriptions to identify which skills are relevant and loads only their full instructions. Unrelated skills remain unloaded, reducing context usage while giving the agent the detailed guidance needed for the task.</p>
<p>This progressive disclosure model means you can have many skills in your project without worrying about context overflow. Having twenty skills isn't twenty times more expensive than having one skill. Only the relevant subset is ever loaded for any given task.</p>
<h2 id="heading-the-anatomy-of-a-skill-file">The Anatomy of a Skill File</h2>
<p>Every skill follows a specific structure. Understanding this structure deeply is the prerequisite for writing effective skills.</p>
<pre><code class="language-markdown">---
name: skill-name-in-kebab-case
description: A clear, specific description that answers: what does this skill cover,
when should it be applied, and what trigger words indicate this task needs this skill?
This is the ONLY part the agent reads when deciding whether this skill is relevant.
aliases: [alternative-name, another-name]
sources: [chat, code]
---

# Skill Title

Brief introduction of what this skill covers and why it exists.

## First Major Section

Content with specific, actionable rules.

## Second Major Section

More rules, examples, counterexamples.

## Code Examples

Concrete code demonstrating the patterns.
</code></pre>
<h3 id="heading-the-frontmatter-block-in-detail">The Frontmatter Block in Detail</h3>
<pre><code class="language-markdown">---
name: flutter-file-organization
description: Organize and split Flutter/Dart files while preserving StatefulWidget
and State relationships. Use when creating, refactoring, splitting, or reorganizing
Dart files and classes. Applies whenever a new screen, widget, model, or Bloc file
is being created or an existing file is being restructured.
aliases: [flutter-files, dart-organization]
sources: [chat, code]
---
</code></pre>
<p><code>name</code> is the unique identifier for this skill across your project. It follows kebab-case convention (lowercase words separated by hyphens) and conventionally starts with the platform or domain (<code>flutter-</code>, <code>dart-</code>, <code>react-</code>, and so on). The name is used by the agent when referencing the skill in its reasoning and by the CLI when managing skills.</p>
<p><code>description</code> is the most critical field in the entire file. It's the only field the agent reads during the lightweight Phase 1 indexing. A poorly written description means a perfectly written skill body never gets loaded. The description should answer three questions: what does this skill cover, when should it be triggered, and what are the specific trigger words or phrases that indicate this skill is relevant? Notice in the example how the description includes "Use when creating, refactoring, splitting, or reorganizing" along with a comprehensive list of file types. Each of those phrases is a potential trigger that helps the agent match task descriptions to this skill.</p>
<p><code>aliases</code> provides alternative names for the skill that the agent can use to reference it. These are optional but useful when the skill might be called different things in different contexts.</p>
<p><code>sources</code> indicates where this skill comes from. For custom team skills, this is typically <code>[chat]</code>. For skills coming from package authors, this might include <code>[package]</code>.</p>
<h3 id="heading-the-skill-body-structure">The Skill Body Structure</h3>
<p>The skill body is pure Markdown with a specific structural discipline that makes it most effective for agent consumption:</p>
<pre><code class="language-markdown"># Title Section (h1)
Brief context-setting paragraph. What problem does this skill solve? Why does it exist?
Keep this under three sentences.

## Core Rules (h2 sections)
Numbered or bulleted lists of specific, verifiable rules.
Each rule should be independently actionable.

## Named Sub-Pattern (h2 sections)
More specific guidance for a particular sub-domain of the skill.
Lead with the rule, then show the wrong pattern, then show the right pattern.

## Code Example (h2 sections)
Complete, runnable code that demonstrates the most important patterns.
Always show both wrong and right versions for patterns that agents commonly get wrong.
</code></pre>
<p>Agents navigate heading structure to understand skill organization. Clear <code>##</code> headings that name the sub-topic they cover help the agent find the specific section relevant to its current sub-task within a larger request.</p>
<h2 id="heading-installing-official-flutter-and-dart-skills">Installing Official Flutter and Dart Skills</h2>
<p>The Flutter and Dart teams maintain official skill repositories that represent years of accumulated knowledge about best practices in the ecosystem. These are your starting point.</p>
<h3 id="heading-installing-flutter-skills">Installing Flutter Skills</h3>
<pre><code class="language-bash">npx skills add flutter/agent-plugins --skill '*' --agent universal --yes
</code></pre>
<p><code>npx skills add</code> runs the <code>skills</code> CLI tool via npm without requiring a permanent installation. <code>flutter/agent-plugins</code> is the GitHub repository path where the official Flutter skills are maintained by the Flutter team. <code>--skill '*'</code> is a wildcard that installs all available skills from the repository rather than selecting specific ones. <code>--agent universal</code> places the skills in the <code>.agents/skills/</code> directory, which is the universal location all compatible agents look in. <code>--yes</code> skips the interactive confirmation prompt, making this command safe to put in project setup scripts or Makefiles.</p>
<p>After running this command, your project gains skills for responsive layouts, declarative routing with GoRouter, JSON serialization with <code>json_serializable</code>, integration testing setup, widget preview setup, widget testing, architecture best practices with BLoC and Clean Architecture, Bloc state management, Bloc forms, and more.</p>
<h3 id="heading-installing-dart-skills">Installing Dart Skills</h3>
<pre><code class="language-bash">npx skills add dart-lang/skills --skill '*' --agent universal --yes
</code></pre>
<p>The Dart team maintains a complementary set of skills focused on the Dart language itself, independent of Flutter's widget system. These skills are valuable for both Flutter apps and pure Dart projects like CLI tools, backend services, and packages.</p>
<p>The official Dart skills cover unit test generation, static analysis configuration, package dependency management, pattern matching and sealed classes, CLI application building, test coverage collection and analysis, runtime error fixing with the LSP, mock generation with Mockito, FFI bindings with ffigen, native assets for C and C++ integration, Dart memory optimization, and migrating from old test assertion styles to modern <code>package:checks</code>.</p>
<h3 id="heading-installing-both-at-once">Installing Both at Once</h3>
<pre><code class="language-bash">npx skills add flutter/agent-plugins dart-lang/skills --skill '*' --agent universal --yes
</code></pre>
<p>Listing both repository names in a single command installs them together and runs dependency resolution once, which is slightly faster than two separate commands. This is the recommended approach for a new Flutter project setup.</p>
<h3 id="heading-installing-skills-from-your-pubspec-dependencies">Installing Skills from Your pubspec Dependencies</h3>
<p>One of the most powerful aspects of the skills ecosystem is that package authors can ship skills alongside their packages. The <code>skills</code> CLI (available as a Dart package) can discover and install skills from all packages in your dependency tree:</p>
<pre><code class="language-bash">dart pub global activate skills
skills get
</code></pre>
<p><code>dart pub global activate skills</code> installs the <code>skills</code> Dart CLI tool globally on your machine. <code>skills get</code> reads your <code>pubspec.yaml</code> and <code>pubspec.lock</code>, finds every package in your dependency tree that ships a <code>skills/</code> directory, and installs those skills into your project's <code>.agents/skills/</code> directory automatically.</p>
<p>When you add a package to your project and run <code>skills get</code>, your agent immediately knows how to use that package correctly according to the package author's own instructions. This is a fundamental shift: instead of the agent guessing how a package works, the package author directly equips the agent with the correct usage patterns.</p>
<pre><code class="language-bash"># Update skills whenever your dependencies change
flutter pub get
skills get
</code></pre>
<p>Running <code>flutter pub get</code> updates your dependencies. Running <code>skills get</code> immediately after updates the skills to match. Making this a two-step habit ensures your agent always has current skills for your current dependencies.</p>
<h3 id="heading-verifying-installed-skills">Verifying Installed Skills</h3>
<pre><code class="language-bash">ls -la .agents/skills/
</code></pre>
<p><code>ls -la .agents/skills/</code> lists all installed skill files with details. You should see <code>.md</code> files named after each installed skill. The <code>-la</code> flags show hidden files and detailed information including file sizes and modification dates.</p>
<p>Once installed, test your agent's awareness of the skills:</p>
<pre><code class="language-plaintext">Which of my installed skills can help me with creating a new feature screen?
</code></pre>
<p>The agent responds with the skills it found that are relevant to that task, confirming they're loaded and indexed correctly. This is a good first test whenever you add skills to a project.</p>
<h2 id="heading-using-skills-with-claude-code">Using Skills with Claude Code</h2>
<p>Claude Code is Anthropic's agentic coding assistant that runs in your terminal. It's one of the most powerful agents for complex, multi-step Flutter development tasks and has excellent support for the skills standard.</p>
<h3 id="heading-installing-the-flutter-plugin-for-claude-code">Installing the Flutter Plugin for Claude Code</h3>
<p>The recommended approach for Claude Code is installing the full Flutter plugin, which bundles skills with MCP server configuration:</p>
<pre><code class="language-bash">claude mcp add flutter-mcp -- dart pub global run dart_mcp_server
npx skills add flutter/agent-plugins --skill '*' --agent claude-code --yes
npx skills add dart-lang/skills --skill '*' --agent claude-code --yes
</code></pre>
<p><code>claude mcp add flutter-mcp</code> registers the Dart MCP server with Claude Code. The MCP server gives Claude Code access to Flutter documentation, pub.dev package information, and Dart tooling directly without making web searches. <code>--agent claude-code</code> in the <code>skills add</code> command places skills in the Claude Code specific location if it differs from the universal <code>.agents/skills/</code> directory, though Claude Code also reads from the universal location.</p>
<h3 id="heading-claude-code-skills-directory">Claude Code Skills Directory</h3>
<p>Claude Code reads skills from <code>.agents/skills/</code> (the universal location) automatically. It also reads from <code>.claude/skills/</code> if you prefer to keep Claude-specific skills separate from universal skills.</p>
<pre><code class="language-plaintext">your_project/
  .agents/
    skills/
      flutter-file-organization.md    &lt;- universal, works everywhere
      flutter-bloc-state-management.md
  .claude/
    skills/
      claude-specific-workflow.md     &lt;- Claude Code only
    CLAUDE.md                         &lt;- Claude Code rules file
</code></pre>
<h3 id="heading-claude-code-rules-vs-skills">Claude Code Rules vs Skills</h3>
<p>Claude Code uses a <code>CLAUDE.md</code> file at the project root (or in <code>.claude/</code>) as a rules file: project-wide instructions that are always in context regardless of task.</p>
<p>Skills are loaded progressively. Use <code>CLAUDE.md</code> for project facts (what package this is, what SDK version, or what state management library is installed). Use skills for task-specific expertise (how to implement Bloc, how to organize files, or how to write tests).</p>
<pre><code class="language-markdown"># CLAUDE.md example

This is a Flutter app called Kopa, a personal budgeting tool.

## Technical Stack
- Flutter 3.47 with Dart 3.10
- State management: flutter_bloc ^9.0.0
- Navigation: go_router ^14.0.0
- Data layer: firebase_ai ^2.0.0 for AI features
- Serialization: freezed + json_serializable
- Testing: bloc_test, mocktail

## Package Name
com.example.kopa

## Minimum SDK
Android API 24, iOS 15

## Project Structure
Feature-first with clean architecture layers.
See the flutter-feature-architecture skill for full structure details.
</code></pre>
<p><code>CLAUDE.md</code> contains facts about the project that never change between tasks: the app name, the packages in use, the SDK versions, and the minimum platform targets. Skills contain the expertise for how to work with those packages and structure that code correctly.</p>
<h3 id="heading-using-skills-in-a-claude-code-session">Using Skills in a Claude Code Session</h3>
<p>Once skills are installed, Claude Code uses them automatically. You don't need to invoke them manually. When you ask:</p>
<pre><code class="language-plaintext">Create a UserProfile feature with Bloc state management, 
a repository that fetches from Firestore, and a screen 
that shows loading, data, and error states.
</code></pre>
<p>Claude Code detects that this request involves multiple skill domains (feature architecture, Bloc state management, file organization, and potentially theming and error handling), loads the relevant skill files, and generates code that follows all of your team's conventions simultaneously.</p>
<p>You can also be explicit:</p>
<pre><code class="language-plaintext">Using the flutter-bloc-state-management skill, implement 
the CartBloc for the shopping cart feature.
</code></pre>
<p>Naming the skill explicitly tells Claude Code to load that specific skill regardless of whether it would have detected the need automatically.</p>
<h2 id="heading-using-skills-with-antigravity">Using Skills with Antigravity</h2>
<p>Antigravity is Google's AI coding assistant, deeply integrated into the Flutter ecosystem and developed alongside the Flutter team. It has first-class support for agent skills and is one of the agents most thoroughly tested with the official Flutter skills.</p>
<h3 id="heading-installing-the-flutter-plugin-for-antigravity">Installing the Flutter Plugin for Antigravity</h3>
<pre><code class="language-plaintext">Open Settings in Antigravity by pressing Cmd+, (Mac) or Ctrl+, (Windows/Linux)
Click the Customizations tab
In the Build with Google Plugins section, click Customize
Click Download next to the Dart and Flutter integration
</code></pre>
<p>This installs the official Flutter plugin for Antigravity, which bundles skills, MCP server configuration, and rules in a single step. It's the recommended installation path because it ensures all three components (skills, MCP, and rules) are correctly configured together.</p>
<h3 id="heading-manual-skills-installation-for-antigravity">Manual Skills Installation for Antigravity</h3>
<p>If you prefer manual installation or need to add custom team skills:</p>
<pre><code class="language-bash">npx skills add flutter/agent-plugins --skill '*' --agent antigravity --yes
npx skills add dart-lang/skills --skill '*' --agent antigravity --yes
</code></pre>
<p><code>--agent antigravity</code> targets the Antigravity-specific skills directory, though Antigravity also reads from the universal <code>.agents/skills/</code> location.</p>
<h3 id="heading-antigravity-workflows-with-skills">Antigravity Workflows with Skills</h3>
<p>Antigravity supports "workflows," which are pre-defined task sequences that can reference skills. You can create a workflow for common team tasks:</p>
<pre><code class="language-markdown"># .antigravity/workflows/new-feature.md

## Create New Feature Workflow

Apply skills: flutter-feature-architecture, flutter-bloc-state-management, 
flutter-testing, flutter-file-organization

Steps:
1. Create the feature folder structure.
2. Create the domain model using Freezed.
3. Create the repository interface and implementation.
4. Create the Bloc with its events and states.
5. Create the screen widget.
6. Extract reusable component widgets.
7. Create unit tests for the repository.
8. Create `bloc_test` tests for the Bloc.
9. Create widget tests for the screen.
</code></pre>
<p>Workflows that reference skills ensure the agent applies the correct conventions for every step of a multi-step task. Without this explicit referencing, the agent might apply the file organization skill for step 1 but forget to apply the testing skill for steps 7 through 9.</p>
<h2 id="heading-using-skills-with-openai-codex">Using Skills with OpenAI Codex</h2>
<p>OpenAI Codex is a terminal-based agentic coding assistant similar in spirit to Claude Code. It runs in your terminal and executes multi-step tasks against your codebase.</p>
<h3 id="heading-installing-skills-for-codex">Installing Skills for Codex</h3>
<pre><code class="language-bash">npx skills add flutter/agent-plugins --skill '*' --agent codex --yes
npx skills add dart-lang/skills --skill '*' --agent codex --yes
</code></pre>
<p><code>--agent codex</code> targets the Codex-specific skills directory. Codex also reads from the universal <code>.agents/skills/</code> directory, so the <code>--agent universal</code> flag works equally well.</p>
<h3 id="heading-codex-rules-file">Codex Rules File</h3>
<p>Similar to Claude Code's <code>CLAUDE.md</code>, Codex reads from an <code>AGENTS.md</code> file at the project root. Configure this alongside your skills:</p>
<pre><code class="language-markdown"># AGENTS.md

Flutter project: Kopa budgeting app
Stack: flutter_bloc, go_router, firebase_ai, freezed
Architecture: Feature-first with clean architecture
Test framework: bloc_test + mocktail
</code></pre>
<p><code>AGENTS.md</code> is the project-wide context file that Codex reads on every task. Keep it brief: five to fifteen lines covering the most important project facts. Detailed conventions belong in skills, not in <code>AGENTS.md</code>, because skills load progressively while <code>AGENTS.md</code> always loads.</p>
<h3 id="heading-plugin-installation-note-for-codex">Plugin Installation Note for Codex</h3>
<p>Codex plugins currently can't bundle rules files automatically. This means installing the Flutter plugin from <code>flutter/agent-plugins</code> installs the skills but doesn't automatically create the <code>AGENTS.md</code> file.</p>
<p>Create this file manually after running the plugin installation. The official Flutter documentation provides a template for the recommended <code>AGENTS.md</code> content for Flutter projects.</p>
<h2 id="heading-using-skills-with-cursor">Using Skills with Cursor</h2>
<p>Cursor is an AI-first code editor built on VS Code. It integrates agent capabilities directly into the editing experience and supports skills through a combination of its rules system and the universal <code>.agents/skills/</code> directory.</p>
<h3 id="heading-installing-skills-for-cursor">Installing Skills for Cursor</h3>
<pre><code class="language-bash">npx skills add flutter/agent-plugins --skill '*' --agent cursor --yes
npx skills add dart-lang/skills --skill '*' --agent cursor --yes
</code></pre>
<p>Cursor reads skills from <code>.agents/skills/</code> as part of its agent context. The <code>--agent cursor</code> flag ensures skills are placed correctly for Cursor's discovery mechanism.</p>
<h3 id="heading-cursor-rules-integration">Cursor Rules Integration</h3>
<p>Cursor uses <code>.cursorrules</code> (or the newer <code>.cursor/rules/</code> directory in recent versions) for project-wide instructions, analogous to Claude Code's <code>CLAUDE.md</code>:</p>
<pre><code class="language-markdown"># .cursor/rules/flutter.mdc

---
description: Flutter project rules applied to all Dart files
globs: ["**/*.dart", "pubspec.yaml"]
alwaysApply: true
---

This is a Flutter project using flutter_bloc, go_router, and freezed.
All state management uses the Bloc pattern.
Feature-first folder structure with clean architecture.
See installed skills in .agents/skills/ for detailed conventions.
</code></pre>
<p><code>globs: ["**/*.dart"]</code> applies this rule only when Dart files are being edited, which prevents the Flutter rules from loading during Markdown editing or YAML configuration. <code>alwaysApply: true</code> ensures the rule is always in context when matching files are open.</p>
<p>The reference to the skills directory at the bottom is intentional: it tells the agent to look at the skills for implementation details rather than making the rules file exhaustively long.</p>
<h3 id="heading-using-composer-and-chat-in-cursor-with-skills">Using Composer and Chat in Cursor with Skills</h3>
<p>In Cursor's Composer (the multi-file editing agent), skills load automatically when you describe a task. In Cursor Chat (the inline assistant), you may need to be more explicit:</p>
<pre><code class="language-plaintext">@flutter-file-organization Create a new PostCard widget 
extracted from the post list screen
</code></pre>
<p>The <code>@</code> prefix in Cursor chat can reference installed skills by name in some configurations. In others, simply describing the task in enough detail is sufficient for the agent to load the relevant skill automatically.</p>
<h2 id="heading-using-skills-with-other-agents">Using Skills with Other Agents</h2>
<h3 id="heading-github-copilot-cli">GitHub Copilot CLI</h3>
<p>GitHub Copilot CLI supports the universal <code>.agents/skills/</code> directory when run in agent mode (<code>gh copilot explain</code> and <code>gh copilot suggest</code>):</p>
<pre><code class="language-bash">npx skills add flutter/agent-plugins --skill '*' --agent copilot --yes
</code></pre>
<p>Note from the <code>skills</code> CLI documentation that GitHub Copilot isn't auto-detected when using <code>skills get</code> because the <code>.github/</code> directory is commonly used for other purposes. Always use the explicit <code>--agent copilot</code> flag when installing skills for Copilot.</p>
<h3 id="heading-gemini-cli">Gemini CLI</h3>
<p>Google's Gemini CLI supports the universal skills directory:</p>
<pre><code class="language-bash">npx skills add flutter/agent-plugins --skill '*' --agent gemini --yes
</code></pre>
<h3 id="heading-universal-installation">Universal Installation</h3>
<p>If you want a single installation that works for all agents simultaneously:</p>
<pre><code class="language-bash">npx skills add flutter/agent-plugins --skill '*' --agent universal --yes
npx skills add dart-lang/skills --skill '*' --agent universal --yes
</code></pre>
<p>The <code>universal</code> agent target places skills in <code>.agents/skills/</code>, which all compliant agents discover automatically. This is the recommended default for teams that use multiple agents or want to be agent-agnostic.</p>
<h3 id="heading-verifying-agent-discovery">Verifying Agent Discovery</h3>
<p>Regardless of which agent you use, you can verify skill discovery with a natural language question to the agent:</p>
<pre><code class="language-plaintext">Summarize the capabilities of the skills you have available for this project.
</code></pre>
<p>A correctly configured agent responds with a list of installed skills and their descriptions, confirming that discovery is working. If the agent says it has no skills or can't find any, check that:</p>
<ol>
<li><p>The <code>.agents/skills/</code> directory exists at the project root</p>
</li>
<li><p>The directory contains <code>.md</code> files with valid YAML frontmatter</p>
</li>
<li><p>The agent supports the universal skills specification</p>
</li>
</ol>
<h2 id="heading-the-official-flutter-skills-a-deep-dive">The Official Flutter Skills: A Deep Dive</h2>
<p>The official Flutter skills repository (<code>flutter/agent-plugins</code>) contains a set of skills that represent the Flutter team's best thinking on common development patterns. Understanding what each skill covers helps you decide which to install, which to customize, and which to supplement with your own skills.</p>
<h3 id="heading-flutter-responsive-layout">flutter-responsive-layout</h3>
<p>This skill teaches the agent how to build layouts that adapt correctly across mobile, tablet, and desktop breakpoints. It covers <code>AdaptiveScaffold</code>, <code>LayoutBuilder</code>, <code>MediaQuery</code>, <code>Breakpoints</code>, and the patterns the Flutter Adaptive Framework recommends for handling different screen sizes.</p>
<p>Without this skill, agents build layouts that look fine on a single device size and break on others. With it, agents produce layouts that are responsive from the first line of code, using the correct Flutter-specific tools rather than hardcoded pixel thresholds.</p>
<h3 id="heading-flutter-declarative-routing">flutter-declarative-routing</h3>
<p>This skill teaches GoRouter setup, route definition patterns, nested navigation, redirect logic for authentication, deep linking configuration, and the correct way to pass typed parameters between routes.</p>
<p>Without this skill, agents often use <code>Navigator.push</code> even in codebases that carefully use GoRouter everywhere. They also commonly get deep linking wrong and struggle with the typed parameter extraction pattern GoRouter requires.</p>
<h3 id="heading-flutter-json-serialization">flutter-json-serialization</h3>
<p>This skill teaches the <code>json_serializable</code> and <code>freezed</code> workflow: adding annotations, running <code>build_runner</code>, creating <code>fromJson</code>/<code>toJson</code> methods, handling nullable fields, and using <code>@JsonKey</code> for field name mapping.</p>
<p>Without this skill, agents manually write serialization code or use <code>Map&lt;String, dynamic&gt;</code> throughout the data layer, producing fragile code that breaks silently when field names change.</p>
<h3 id="heading-flutter-add-widget-test">flutter-add-widget-test</h3>
<p>This skill teaches <code>testWidgets</code>, <code>WidgetTester</code>, pump strategies (<code>pump</code>, <code>pumpAndSettle</code>, <code>pumpWidget</code>), widget finders (<code>find.text</code>, <code>find.byType</code>, <code>find.byKey</code>), gesture simulation, and how to wrap widgets in minimal but sufficient test infrastructure.</p>
<h3 id="heading-flutter-add-integration-test">flutter-add-integration-test</h3>
<p>This skill teaches how to set up and run end-to-end integration tests on devices, web browsers, or Firebase Test Lab. It covers test setup, the <code>IntegrationTestWidgetsFlutterBinding</code>, app startup sequencing, and interacting with a fully running app in test.</p>
<h3 id="heading-flutter-bloc">flutter-bloc</h3>
<p>This skill teaches the complete Bloc workflow: defining events, states, and the Bloc class, providing the Bloc with <code>BlocProvider</code>, consuming it with <code>BlocBuilder</code>, <code>BlocListener</code>, and <code>BlocConsumer</code>, and testing with <code>bloc_test</code>.</p>
<h3 id="heading-flutter-apply-architecture-best-practices">flutter-apply-architecture-best-practices</h3>
<p>This skill enforces Clean Architecture (Data, Domain, Presentation) with the BLoC pattern as the official Flutter team recommends it. It defines layer boundaries, dependency rules, and the repository pattern.</p>
<h3 id="heading-flutter-add-widget-preview">flutter-add-widget-preview</h3>
<p>This skill teaches the Widget Previewer system introduced in Flutter 3.47, including the <code>@Preview</code> annotation, how to set up preview infrastructure, and how to write useful previews for complex widgets.</p>
<h2 id="heading-the-official-dart-skills-a-deep-dive">The Official Dart Skills: A Deep Dive</h2>
<p>The Dart team's official skills repository (<code>dart-lang/skills</code>) covers the Dart language itself rather than Flutter's widget system. These skills apply to any Dart code: Flutter app logic, Dart CLI tools, Dart backend services, and Dart packages.</p>
<h3 id="heading-dart-add-unit-test">dart-add-unit-test</h3>
<p>This is the most fundamental Dart skill and the one with the highest immediate impact. It teaches the agent how to write proper unit tests for any Dart class, including:</p>
<ul>
<li><p>Setting up the <code>test/</code> directory mirroring the <code>lib/</code> structure</p>
</li>
<li><p>Writing <code>group</code> and <code>test</code> blocks with descriptive names</p>
</li>
<li><p>Using <code>setUp</code> and <code>tearDown</code> for test lifecycle management</p>
</li>
<li><p>Using <code>expect</code> with the right matchers</p>
</li>
<li><p>Mocking dependencies with <code>mocktail</code></p>
</li>
<li><p>Testing async code with <code>expectLater</code> and stream matchers</p>
</li>
</ul>
<p>Without this skill, agents produce tests that test the wrong things, use incorrect assertion patterns, and structure test files in ways that don't mirror the source tree. With it, agents produce tests that follow the <code>package:test</code> conventions correctly from the first run.</p>
<pre><code class="language-markdown"># What dart-add-unit-test teaches the agent

## Test file placement
test/features/profile/data/profile_repository_test.dart
mirrors
lib/features/profile/data/profile_repository.dart

## Test naming
```
group('ProfileRepository', () {
  group('getProfile', () {
    test('returns ProfileLoaded when API call succeeds', () async {
      // ...
    });

    test('returns NetworkFailure when connection fails', () async {
      // ...
    });
  });
});
```

## Async testing
```
await expectLater(
  repository.getProfile('user123'),
  completion(isA&lt;Right&lt;AppFailure, UserProfile&gt;&gt;()),
);
```
</code></pre>
<h3 id="heading-dart-run-static-analysis">dart-run-static-analysis</h3>
<p>This skill teaches the agent how to work with Dart's static analysis infrastructure: configuring <code>analysis_options.yaml</code>, running <code>dart analyze</code>, applying <code>dart fix --apply</code>, understanding lint rules, suppressing false positives correctly, and enforcing strict type checks.</p>
<pre><code class="language-markdown"># What dart-run-static-analysis covers

## analysis_options.yaml configuration
include: package:flutter_lints/flutter.yaml

analyzer:
  language:
    strict-casts: true
    strict-inference: true
    strict-raw-types: true
  exclude:
    - '**/*.g.dart'
    - '**/*.freezed.dart'

linter:
  rules:
    avoid_print: true
    prefer_final_fields: true
    require_trailing_commas: true

## Correct suppression (when a lint is a false positive)
// ignore: avoid_print  &lt;- line-level, for one occurrence
// ignore_for_file: type=lint  &lt;- file-level, for generated files
</code></pre>
<p>Understanding how to configure <code>analysis_options.yaml</code> correctly is one of those tasks where agents frequently make mistakes without guidance: they enable the wrong rules, forget to exclude generated files, or suppress diagnostics too broadly. This skill makes those configurations correct from the start.</p>
<h3 id="heading-dart-tooling">dart-tooling</h3>
<p>This skill teaches how to resolve package version conflicts in <code>pubspec.yaml</code>, use dependency overrides correctly, understand the difference between direct and transitive dependencies, and read <code>pubspec.lock</code> to diagnose version resolution issues.</p>
<p>Package dependency management is an area where agents frequently hallucinate package versions or suggest <code>dependency_overrides</code> in ways that mask real conflicts. This skill corrects those behaviors.</p>
<h3 id="heading-dart-use-pattern-matching">dart-use-pattern-matching</h3>
<p>This skill is one of the highest-value Dart skills because Dart 3's sealed classes and pattern matching represent a genuinely new coding paradigm that agents trained before Dart 3's release don't use consistently. It teaches:</p>
<ul>
<li><p>Switch expressions on sealed classes with exhaustiveness</p>
</li>
<li><p>Destructuring patterns in switch cases</p>
</li>
<li><p>Guard clauses with <code>when</code></p>
</li>
<li><p>Record patterns</p>
</li>
<li><p>List and map patterns</p>
</li>
<li><p>The correct use of <code>_</code> (wildcard) in patterns</p>
</li>
</ul>
<pre><code class="language-dart">// What the agent learns to write with dart-use-pattern-matching

// Before: traditional switch on enum (old pattern)
switch (state) {
  case AppState.loading:
    return CircularProgressIndicator();
  case AppState.loaded:
    return ContentWidget(data: data);
  default:
    return ErrorWidget();
}

// After: switch expression with pattern matching (idiomatic Dart 3)
return switch (state) {
  AppStateLoading() =&gt; const CircularProgressIndicator(),
  AppStateLoaded(:final data) =&gt; ContentWidget(data: data),
  AppStateError(:final message) =&gt; ErrorWidget(message: message),
};
</code></pre>
<p>The destructuring pattern <code>AppStateLoaded(:final data)</code> is pure Dart 3 and extremely clean, but agents without this skill rarely produce it because it wasn't in the training data for older agent versions.</p>
<h3 id="heading-dart-collect-coverage">dart-collect-coverage</h3>
<p>This skill teaches test coverage collection, LCOV report generation, HTML report generation, and how to filter out generated code (<code>*.g.dart</code>, <code>*.freezed.dart</code>) from coverage reports so the numbers reflect real coverage rather than being inflated by generated code that can't be meaningfully tested.</p>
<h3 id="heading-dart-generate-test-mocks">dart-generate-test-mocks</h3>
<p>This skill teaches the <code>mockito</code> and <code>build_runner</code> workflow for generating type-safe mocks from interfaces and abstract classes. It covers adding the annotations, running <code>dart run build_runner build</code>, and using the generated mocks in tests.</p>
<h3 id="heading-dart-fix-runtime-errors">dart-fix-runtime-errors</h3>
<p>This is a procedural skill: it teaches the agent to use the LSP (Language Server Protocol) to fetch the current stack trace, locate the failing line, apply a fix, and verify resolution using hot reload. This is the correct workflow for fixing runtime errors in a live Flutter app rather than guessing at the cause.</p>
<h3 id="heading-dart-genkit">dart-genkit</h3>
<p>This skill teaches how to build AI-powered workflows and agents using the Genkit Dart SDK. It's specifically relevant for Flutter developers building AI features, covering flow definition, tool calling, model selection, and streaming.</p>
<h3 id="heading-dart-migrate-to-checks-package">dart-migrate-to-checks-package</h3>
<p>This skill teaches how to migrate from the older <code>package:matcher</code> assertion style to the newer <code>package:checks</code> style, which produces better error messages and is more composable.</p>
<pre><code class="language-dart">// Old style (package:matcher)
expect(result, isA&lt;Right&lt;AppFailure, UserProfile&gt;&gt;());
expect(result.getOrElse(() =&gt; null)?.name, equals('Ade'));

// New style (package:checks)
check(result).isA&lt;Right&lt;AppFailure, UserProfile&gt;&gt;();
check(result.getOrElse(() =&gt; null)?.name).equals('Ade');
</code></pre>
<h3 id="heading-dart-memory">dart-memory</h3>
<p>This skill teaches how to prevent memory leaks and reduce garbage collection pressure in Flutter and Dart apps, covering <code>StreamController</code> disposal, <code>AnimationController</code> disposal, closure capture patterns that prevent garbage collection, and how to use DevTools to identify memory issues.</p>
<h3 id="heading-dart-build-cli-app">dart-build-cli-app</h3>
<p>For Flutter developers who also write Dart CLI tools, backend scripts, or deployment automation in Dart, this skill covers entrypoint structure, argument parsing with <code>package:args</code>, exit codes, subprocess handling, and cross-platform script patterns.</p>
<h3 id="heading-dart-logic-patterns">dart-logic-patterns</h3>
<p>This skill covers algorithms, data structures, and Dart-specific patterns for organizing business logic: using <code>Iterable</code> methods correctly, choosing between <code>List</code>, <code>Set</code>, and <code>Map</code> for different use cases, implementing efficient search and sort, and using Dart's collection literals productively.</p>
<h2 id="heading-the-flutter-file-organization-skill-a-complete-walkthrough">The flutter-file-organization Skill: A Complete Walkthrough</h2>
<p>The file organization skill is the most universally applicable Flutter skill and an excellent teaching example for how skills should be structured. Reading it carefully reveals the principles behind every effective skill.</p>
<pre><code class="language-markdown">---
name: flutter-file-organization
description: Organize and split Flutter/Dart files while preserving StatefulWidget and State relationships. Use when creating, refactoring, splitting, or reorganizing Dart files and classes.
---

# Flutter File Organization

When creating, splitting, refactoring, or reorganizing Flutter/Dart files, follow these rules.

## Core Rules

1. Inspect the existing file before modifying it.
2. Identify all classes, enums, extensions, mixins, typedefs, and top-level declarations.
3. Identify relationships and dependencies between declarations before splitting them.
4. Keep each independent primary class in its own file.
5. Treat tightly coupled declarations as a single implementation unit and keep them together.
6. Never separate a `StatefulWidget` from its corresponding `State&lt;T&gt;` class.
7. Update all imports and references after moving declarations.
8. Do not introduce unnecessary private helper classes or methods.
9. Preserve existing application behavior. File organization must not change functionality.
10. Run `dart format` on modified Dart files.
11. Run the project's analyzer and relevant tests.
</code></pre>
<p>Rule 1 ("Inspect the existing file before modifying it") prevents one of the most common and costly agent mistakes: making assumptions about file contents without reading them.</p>
<p>An agent that skips inspection may duplicate declarations, break dependencies, or introduce naming conflicts with things that already exist. Making inspection an explicit first rule ensures the agent always starts from a complete picture of the current state.</p>
<p>Rules 2 and 3 ("Identify all classes" and "Identify relationships") are mandatory pre-flight checks. Before the agent touches a single byte of a file, it must map everything that exists and how the pieces depend on each other.</p>
<p>This is the equivalent of "measure twice, cut once" applied to code refactoring, and it prevents the most frustrating class of bug: refactors that break things that were working.</p>
<p>Rule 6 ("Never separate a StatefulWidget from its corresponding State class") encodes Flutter-specific compilation knowledge. A developer who knows Dart deeply but doesn't know Flutter could reasonably split a file by moving every class to its own file. They would hit a compile error because <code>_ProfilePageState</code> references the <code>ProfilePage</code> widget through <code>widget</code>, which has a type that <code>State&lt;T&gt;</code> establishes at the class level. The two classes form a single compilation unit that can't be separated. This rule prevents a compile error that no amount of general Dart knowledge would avoid.</p>
<p>Rules 10 and 11 ("Run dart format" and "Run the project's analyzer") close the task-completion loop. Without these rules, an agent declares success after generating files, leaving formatting inconsistencies and possible analyzer warnings for you to discover later. With them, the agent runs both tools before reporting completion, catching issues immediately.</p>
<pre><code class="language-markdown">## Widget Extraction

Do not create private `_build...()` methods as a way of extracting substantial widget UI.

For example, do not do this:

```
Widget _buildUserCard() {
  return Container(
    ...
  );
}
```

Instead separate this into a class that is public and place it inside the widgets folder or the components folder.
</code></pre>
<p>The Widget Extraction section does four things that every good skill rule should do: states the rule clearly, explains the prohibited pattern precisely (not just vaguely), shows a concrete code example of what not to do so there's no ambiguity, and tells the agent what to do instead.</p>
<p>The <code>_build...()</code> pattern is very common in training data (tutorials often use it for simplicity), which means saying "avoid it" without a concrete example risks not overriding the learned behavior.</p>
<p>Showing the exact code pattern to avoid and contrasting it with the alternative makes the instruction maximally clear.</p>
<pre><code class="language-markdown">## Component Extraction

Do not place large amounts of UI inside a single widget.

Extract logical sections into reusable components whenever appropriate.

Examples include:

- Header sections
- Statistics cards
- Filter bars
- Search bars
- Lists
- Table rows
- Buttons
- Empty states
- Loading views
- Form sections
- Dialog content

Favor small, reusable widgets over large build methods.
</code></pre>
<p>The example list in the Component Extraction section is drawn from real experience. These are the actual UI sections that accumulate inside screen widgets in production Flutter apps. An agent reading this list will recognize these patterns in the code it examines and know to extract them.</p>
<p>Without the list, "extract logical sections" is too vague for reliable behavior: the agent needs to know concretely what counts as a "logical section."</p>
<pre><code class="language-markdown">## Code Comments

Do not write code comments.

This rule applies everywhere and to every layer.
</code></pre>
<p>The code comments rule is brief because it's absolute. The phrase "applies everywhere and to every layer" is deliberate. Without this scope qualifier, an agent might interpret the rule as applying only to the current file organization task and revert to adding comments in other files it creates or modifies. The explicit scope removes ambiguity and makes the rule's intent clear across all contexts.</p>
<h2 id="heading-writing-your-own-skills-the-complete-guide">Writing Your Own Skills: The Complete Guide</h2>
<p>The official skills are your foundation. But your most valuable skills are often the ones you write yourself, encoding the specific patterns, mistakes, and standards of your own projects.</p>
<h3 id="heading-the-right-mindset-for-writing-skills">The Right Mindset for Writing Skills</h3>
<p>Writing a skill is not the same as writing documentation for humans. Documentation for humans relies on shared context, implicit understanding, and the ability to ask questions. Skills for agents must be explicit, precise, and assume no knowledge beyond what the skill file contains.</p>
<p>The best skills come from real experience with your codebase. Keep a running list of every time you manually fix AI-generated code. Every fix is a skill rule. When you explain a convention to a new team member, that explanation is skill content. When you catch the same mistake in code review three times in a row, that mistake needs a skill.</p>
<p>Ask yourself before writing any rule: "Would an agent that doesn't know my codebase know to do this?" If the answer is no, the rule belongs in a skill.</p>
<h3 id="heading-the-description-the-most-important-twenty-words">The Description: The Most Important Twenty Words</h3>
<p>The description field is the gatekeeper. Write it last, after the skill body is complete, so it accurately describes what the skill actually covers. A good description passes this test: if an agent reads only the description, it knows whether this skill is relevant for a given task.</p>
<pre><code class="language-yaml"># Poor: too vague, no trigger phrases
description: How to handle state in Flutter apps.

# Better: specific, multiple trigger phrases, clear scope
description: Implement state management using flutter_bloc in Flutter applications.
Use when adding state management to screens, creating new features that have loading
or error states, fetching data from APIs, handling user interactions that change
UI state, or implementing BlocProvider, BlocBuilder, BlocListener, or BlocConsumer.
Applies when you see references to bloc, cubit, state, event, or stream in a task.
</code></pre>
<p>The second description is better for several specific reasons. It lists specific trigger scenarios ("creating new features that have loading or error states") that are more likely to match actual task descriptions than the vague "handle state." It includes the API surface of the relevant package (<code>BlocProvider</code>, <code>BlocBuilder</code>) which are likely to appear in task descriptions. And it lists the conceptual keywords (<code>bloc</code>, <code>cubit</code>, <code>state</code>, and <code>event</code>) that serve as signals.</p>
<h3 id="heading-writing-rules-that-change-agent-behavior">Writing Rules That Change Agent Behavior</h3>
<p>Not all rules are equal. Rules that tell an agent to do something it was already doing provide no value. Rules that change what the agent does are the valuable ones. To write rules that change behavior, start from observation: what did the agent actually produce that was wrong, and what rule would have prevented that?</p>
<pre><code class="language-markdown">## Rules That Work vs Rules That Do Not

DO NOT WORK (agent was already trying to do these):
- Write clean, readable code.
- Follow Flutter best practices.
- Use appropriate state management.
- Keep the codebase maintainable.

WORK (these change specific agent behavior):
- Extract any widget build section exceeding 30 lines into a separate class in widgets/.
- Never call setState inside a widget that has a corresponding BlocBuilder.
- Name Bloc events as past-tense verbs: ProfileLoadRequested, not LoadProfile.
- Place all Bloc files (bloc, event, state) in a bloc/ subdirectory inside the feature.
- The state class uses sealed keyword: sealed class ProfileState {}.
- Provide super.key in every widget constructor: const MyWidget({super.key}).
- Check mounted before calling setState in any async method.
</code></pre>
<p>Notice that working rules contain specific numbers (30 lines), specific folder names (widgets/, bloc/), specific naming patterns with examples, and specific code patterns. Vague rules like "write clean code" describe something the agent already tries to do by default. Specific rules like "name Bloc events as past-tense verbs with concrete examples" change actual output.</p>
<h3 id="heading-the-counterexample-pattern">The Counterexample Pattern</h3>
<p>For rules that address patterns that are common in training data, showing the wrong pattern alongside the right one is significantly more effective than describing the rule in text alone. The agent has seen the wrong pattern thousands of times in training. A text rule may not be strong enough to override that learned behavior. A visual contrast makes the intention unmistakable.</p>
<pre><code class="language-markdown">## Error State Naming

Do not name error states with the word "Error" alone at the end.

Do not do this:

```
final class ProfileError extends ProfileState {
  const ProfileError();
}
</code></pre>
<p>Include the error context:</p>
<pre><code class="language-dart">final class ProfileLoadFailure extends ProfileState {
  const ProfileLoadFailure({required this.message});
  final String message;
}
</code></pre>
<p>Including the action name (<code>Load</code>) makes the error state specific to the operation that failed. This is important when a single Bloc handles multiple operations that can fail independently. <code>ProfileLoadFailure</code> and <code>ProfileUpdateFailure</code> can coexist meaningfully. <code>ProfileError</code> and <code>ProfileError2</code> can't.</p>
<p>The explanation after the counterexample ("Including the action name...") connects the rule to the reason, which helps the agent apply the rule correctly in edge cases rather than just following the letter of the rule.</p>
<h2 id="heading-essential-flutter-skills-every-team-should-have">Essential Flutter Skills Every Team Should Have</h2>
<p>Based on the most common areas where AI agents produce incorrect Flutter output, here are the essential skills every Flutter team should write and maintain. Each is presented in full, ready to be adapted to your specific conventions.</p>
<h3 id="heading-the-bloc-state-management-skill">The Bloc State Management Skill</h3>
<pre><code class="language-plaintext">---
name: flutter-bloc-state-management
description: Implement state management using flutter_bloc. Use when creating new features,
adding state to screens, fetching data from APIs, handling user interactions that produce
loading or error states, using BlocProvider, BlocBuilder, BlocListener, BlocConsumer,
adding a Cubit, or any task involving state transitions in Flutter.
---

# Flutter Bloc State Management

This project uses flutter_bloc for all state management. Do not use setState, ChangeNotifier,
Provider, or Riverpod unless explicitly instructed.

## File Structure

Every feature that requires state management has three Bloc files in a bloc/ subdirectory:
</code></pre>
<pre><code class="language-plaintext">lib/
  features/
    profile/
      bloc/
        profile_bloc.dart      &lt;- Bloc class and handler methods
        profile_event.dart     &lt;- All events as sealed class hierarchy
        profile_state.dart     &lt;- All states as sealed class hierarchy
      screens/
        profile_screen.dart
      widgets/
        profile_card.dart
      profile.dart              &lt;- barrel export
</code></pre>
<h4 id="heading-sealed-classes">Sealed Classes</h4>
<p>Events and states use Dart's sealed class system for exhaustive handling:</p>
<pre><code class="language-dart">// profile_event.dart
sealed class ProfileEvent {}

final class ProfileLoadRequested extends ProfileEvent {
  const ProfileLoadRequested({required this.userId});
  final String userId;
}

final class ProfileUsernameUpdated extends ProfileEvent {
  const ProfileUsernameUpdated({required this.newUsername});
  final String newUsername;
}
</code></pre>
<pre><code class="language-dart">// profile_state.dart
sealed class ProfileState {}

final class ProfileInitial extends ProfileState {}

final class ProfileLoading extends ProfileState {}

final class ProfileLoaded extends ProfileState {
  const ProfileLoaded({required this.profile});
  final UserProfile profile;
}

final class ProfileLoadFailure extends ProfileState {
  const ProfileLoadFailure({required this.message});
  final String message;
}
</code></pre>
<p><code>sealed class</code> makes the hierarchy exhaustive: Dart's compiler can verify that every possible state is handled in a switch statement. <code>final class</code> on concrete implementations prevents unintended subclassing. Every state and event is <code>final</code> and <code>sealed</code>.</p>
<h4 id="heading-naming-conventions">Naming Conventions</h4>
<p>The Bloc class should use the feature name followed by <code>Bloc</code>, such as <code>ProfileBloc</code>, <code>AuthBloc</code>, or <code>CartBloc</code>. Events should use a past-tense verb phrase followed by the feature name and the <code>Event</code> suffix, such as <code>ProfileLoadRequested</code> or <code>AuthLoginAttempted</code>. States should use the feature name followed by a descriptive noun or adjective, such as <code>ProfileInitial</code>, <code>ProfileLoading</code>, <code>ProfileLoaded</code>, or <code>ProfileLoadFailure</code>.</p>
<p>Don't name events as commands (not <code>LoadProfile</code>, but <code>ProfileLoadRequested</code>). Don't name error states simply as <code>ProfileError</code>. Include the operation: <code>ProfileLoadFailure</code>, <code>ProfileUpdateFailure</code>.</p>
<h4 id="heading-the-bloc-class">The Bloc Class</h4>
<pre><code class="language-dart">// profile_bloc.dart
class ProfileBloc extends Bloc&lt;ProfileEvent, ProfileState&gt; {
  final ProfileRepository _repository;

  ProfileBloc({required ProfileRepository repository})
      : _repository = repository,
        super(ProfileInitial()) {
    on&lt;ProfileLoadRequested&gt;(_onProfileLoadRequested);
    on&lt;ProfileUsernameUpdated&gt;(_onProfileUsernameUpdated);
  }

  Future&lt;void&gt; _onProfileLoadRequested(
    ProfileLoadRequested event,
    Emitter&lt;ProfileState&gt; emit,
  ) async {
    emit(ProfileLoading());

    final result = await _repository.getProfile(event.userId);

    result.fold(
      (failure) =&gt; emit(ProfileLoadFailure(message: _mapFailure(failure))),
      (profile) =&gt; emit(ProfileLoaded(profile: profile)),
    );
  }

  String _mapFailure(AppFailure failure) =&gt; switch (failure) {
    NetworkFailure(:final message) =&gt; message,
    ServerFailure(:final message) =&gt; message,
    NotFoundFailure() =&gt; 'Profile not found',
    UnauthorizedFailure() =&gt; 'Please sign in again',
    _ =&gt; 'An unexpected error occurred',
  };
}
</code></pre>
<p>Each event handler is a private method named <code>_on</code> + EventClassName. The pattern is consistent across all Blocs. Every handler emits a loading state before the async operation and emits either a success or failure state after. No handler returns data directly. All communication is through emitted states.</p>
<h4 id="heading-widget-integration">Widget Integration</h4>
<pre><code class="language-dart">class ProfileScreen extends StatelessWidget {
  const ProfileScreen({super.key, required this.userId});
  final String userId;

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) =&gt; ProfileBloc(
        repository: context.read&lt;ProfileRepository&gt;(),
      )..add(ProfileLoadRequested(userId: userId)),
      child: BlocConsumer&lt;ProfileBloc, ProfileState&gt;(
        listener: (context, state) {
          if (state is ProfileLoadFailure) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text(state.message)),
            );
          }
        },
        builder: (context, state) =&gt; switch (state) {
          ProfileInitial() =&gt; const SizedBox.shrink(),
          ProfileLoading() =&gt; const Center(child: CircularProgressIndicator()),
          ProfileLoaded(:final profile) =&gt; ProfileContent(profile: profile),
          ProfileLoadFailure(:final message) =&gt; ProfileErrorView(message: message),
        },
      ),
    );
  }
}
</code></pre>
<p><code>BlocConsumer</code> combines listener (side effects) and builder (UI). The switch expression on sealed states is exhaustive: the compiler enforces that every state has a corresponding UI.</p>
<h4 id="heading-prohibited-patterns">Prohibited Patterns</h4>
<p>Don't use <code>setState</code> in any widget that has a corresponding Bloc. Don't call <code>context.read&lt;SomeBloc&gt;().add(event)</code> from inside <code>initState</code> without deferring with <code>addPostFrameCallback</code>. Don't access <code>BuildContext</code> after an <code>await</code> without checking <code>mounted</code>. Don't create a Bloc inside a <code>StatelessWidget.build</code> method (it is recreated on every rebuild).</p>
<h3 id="heading-the-feature-architecture-skill">The Feature Architecture Skill</h3>
<pre><code class="language-plaintext">---
name: flutter-feature-architecture
description: Structure Flutter features using clean architecture with repository, service,
and presentation layers. Use when creating new features, adding screens, implementing
data fetching, organizing existing code, deciding where a new file belongs, or any task
that involves folder structure, layer boundaries, or the project's directory organization.
---

# Flutter Feature Architecture

This project uses feature-first folder structure with clean architecture layers.
</code></pre>
<h4 id="heading-top-level-structure">Top-Level Structure</h4>
<pre><code class="language-plaintext">lib/
  core/
    constants/     &lt;- app-wide constants, not feature-specific
    errors/         &lt;- AppFailure sealed class hierarchy
    extensions/     &lt;- Dart extension methods
    theme/          &lt;- theme extensions, color tokens, typography
    utils/          &lt;- pure utility functions
  features/
    auth/
    profile/
    home/
    settings/
  shared/
    widgets/        &lt;- widgets used in 3+ features
    models/         &lt;- models shared between features
    services/       &lt;- services used by multiple features
  app.dart          &lt;- MaterialApp setup
  main.dart         &lt;- entry point
</code></pre>
<h4 id="heading-feature-folder-structure">Feature Folder Structure</h4>
<p>Every feature follows this internal structure:</p>
<pre><code class="language-plaintext">features/
  profile/
    bloc/
      profile_bloc.dart
      profile_event.dart
      profile_state.dart
    data/
      profile_repository.dart          &lt;- interface
      profile_repository_impl.dart     &lt;- implementation
      profile_remote_data_source.dart
      profile_local_data_source.dart
    domain/
      profile_model.dart                &lt;- freezed domain model
    screens/
      profile_screen.dart
      edit_profile_screen.dart
    widgets/
      profile_card.dart
      profile_header.dart
      profile_stats_row.dart
    profile.dart                         &lt;- barrel export
</code></pre>
<h4 id="heading-layer-dependency-rules">Layer Dependency Rules</h4>
<p>The presentation layer (screens and widgets) depends only on Bloc and domain models. The Bloc depends only on the repository interface (not the implementation). The repository implementation depends on data sources. Data sources depend on external packages (Firebase, HTTP, SharedPreferences).</p>
<p>Never import across layers in the wrong direction. The data layer never imports from the presentation layer. The domain layer imports from nothing in the project.</p>
<h4 id="heading-the-barrel-export-file">The Barrel Export File</h4>
<p>Every feature has a barrel file that exports only the public API of the feature:</p>
<pre><code class="language-dart">// features/profile/profile.dart
export 'domain/profile_model.dart';
export 'screens/profile_screen.dart';
export 'screens/edit_profile_screen.dart';
export 'bloc/profile_bloc.dart';
export 'bloc/profile_event.dart';
export 'bloc/profile_state.dart';
</code></pre>
<p>Internal implementation files (data sources, repository implementation) aren't exported. Consuming code imports <code>package:myapp/features/profile/profile.dart</code>, never deep paths.</p>
<h4 id="heading-the-core-folder-rule">The Core Folder Rule</h4>
<p>A file belongs in core/ only if it's used by three or more features. If used by only one or two features, it belongs inside those features' folders. Don't preemptively move things to core/ based on where they might be used in the future.</p>
<h3 id="heading-the-error-handling-skill">The Error Handling Skill</h3>
<pre><code class="language-markdown">---
name: flutter-error-handling
description: Implement error handling using typed AppFailure classes and Either return types.
Use when handling errors from API calls, repository methods, Bloc error states, catching
exceptions in data sources, showing error UI, implementing try-catch, or any task that
involves failure, exception, error state, or error message handling.
---

# Flutter Error Handling

This project uses a typed failure system. Raw exceptions do not cross layer boundaries.
</code></pre>
<h4 id="heading-the-appfailure-hierarchy">The AppFailure Hierarchy</h4>
<pre><code class="language-dart">// core/errors/app_failure.dart
sealed class AppFailure {
  const AppFailure();
}

final class NetworkFailure extends AppFailure {
  const NetworkFailure({required this.message});
  final String message;
}

final class ServerFailure extends AppFailure {
  const ServerFailure({required this.statusCode, required this.message});
  final int statusCode;
  final String message;
}

final class CacheFailure extends AppFailure {
  const CacheFailure({required this.message});
  final String message;
}

final class NotFoundFailure extends AppFailure {
  const NotFoundFailure();
}

final class UnauthorizedFailure extends AppFailure {
  const UnauthorizedFailure();
}

final class ValidationFailure extends AppFailure {
  const ValidationFailure({required this.field, required this.message});
  final String field;
  final String message;
}
</code></pre>
<p><code>sealed class AppFailure</code> makes the hierarchy exhaustive. New failure types are added as <code>final class</code> subclasses. The compiler enforces that switch statements on <code>AppFailure</code> handle every possible subtype.</p>
<h4 id="heading-repository-return-types">Repository Return Types</h4>
<p>Repository methods return <code>Either&lt;AppFailure, T&gt;</code> from the <code>fpdart</code> package:</p>
<pre><code class="language-dart">abstract class ProfileRepository {
  Future&lt;Either&lt;AppFailure, UserProfile&gt;&gt; getProfile(String userId);
  Future&lt;Either&lt;AppFailure, Unit&gt;&gt; updateUsername(String userId, String username);
}
</code></pre>
<p>Returning <code>Either</code> makes failure possible-but-explicit at the type level. Consumers of the repository can't accidentally ignore the possibility of failure because the return type forces them to handle both branches.</p>
<h4 id="heading-data-source-exception-handling">Data Source Exception Handling</h4>
<p>Data sources are the only layer that uses try-catch. They catch raw exceptions and convert them to AppFailure objects:</p>
<pre><code class="language-dart">class ProfileRemoteDataSource {
  Future&lt;Either&lt;AppFailure, UserProfileDto&gt;&gt; getProfile(String userId) async {
    try {
      final doc = await _firestore.collection('users').doc(userId).get();

      if (!doc.exists) return left(const NotFoundFailure());

      return right(UserProfileDto.fromJson(doc.data()!));
    } on FirebaseException catch (e) {
      return switch (e.code) {
        'permission-denied' =&gt; left(const UnauthorizedFailure()),
        'unavailable' =&gt; left(NetworkFailure(message: e.message ?? 'Network error')),
        _ =&gt; left(ServerFailure(statusCode: 0, message: e.message ?? 'Server error')),
      };
    } catch (e) {
      return left(NetworkFailure(message: e.toString()));
    }
  }
}
</code></pre>
<h4 id="heading-prohibited-patterns">Prohibited Patterns</h4>
<p>Don't use <code>try-catch</code> in Blocs, repositories, or presentation layer code. Don't throw exceptions from repository methods. Don't use <code>String</code> as an error message type in state classes. Use the typed failure. Don't pass raw exception messages to the UI. Map failures to user-friendly messages in the Bloc.</p>
<h3 id="heading-the-theming-skill">The Theming Skill</h3>
<pre><code class="language-markdown">---
name: flutter-theming
description: Apply colors, typography, spacing, and visual styling using the project's
theme extension system. Use whenever writing code that involves colors, text styles,
padding, margin, border radius, shadows, or any visual appearance of UI components.
Apply when you see requests involving styling, colors, fonts, spacing, or visual design.
---

# Flutter Theming

This project uses theme extensions for all visual styling. Hardcoded visual values are not permitted anywhere in the codebase.
</code></pre>
<h4 id="heading-color-access">Color Access</h4>
<pre><code class="language-dart">// Do not do this
color: const Color(0xFF6750A4)
color: Colors.deepPurple
backgroundColor: Theme.of(context).colorScheme.primary

// Do this
color: context.appColors.primary
backgroundColor: context.appColors.surface
</code></pre>
<p><code>context.appColors</code> is an extension on <code>BuildContext</code> defined in <code>core/theme/app_colors_extension.dart</code>. It provides typed access to the full color palette with names that communicate intent.</p>
<p>Available colors: use the semantic colors provided through <code>context.appColors</code>.</p>
<p>For <strong>branding and surfaces</strong>, use <code>context.appColors.primary</code> for the main brand color, <code>context.appColors.secondary</code> for secondary accents, <code>context.appColors.surface</code> for card and container backgrounds, and <code>context.appColors.background</code> for screen backgrounds.</p>
<p>For <strong>states</strong>, use <code>context.appColors.error</code> for error states and <code>context.appColors.success</code> for success states.</p>
<p>For <strong>text</strong>, use <code>context.appColors.textPrimary</code> for primary readable text, <code>context.appColors.textSecondary</code> for captions, labels, and secondary information, and <code>context.appColors.textDisabled</code> for disabled controls and text.</p>
<h4 id="heading-spacing">Spacing</h4>
<pre><code class="language-dart">// Do not do this
padding: const EdgeInsets.all(16)
margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 8)

// Do this
padding: const EdgeInsets.all(AppSpacing.md)
margin: const EdgeInsets.symmetric(
  horizontal: AppSpacing.lg,
  vertical: AppSpacing.sm,
)
</code></pre>
<p><code>AppSpacing</code> is defined in <code>core/constants/app_spacing.dart</code> and provides the following spacing values:</p>
<p><strong>xs:</strong> 4 · <strong>sm:</strong> 8 · <strong>md:</strong> 16 · <strong>lg:</strong> 24 · <strong>xl:</strong> 32 · <strong>xxl:</strong> 48</p>
<h4 id="heading-typography">Typography</h4>
<pre><code class="language-dart">// Do not do this
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)

// Do this
style: context.appTypography.bodyMedium
style: context.appTypography.headlineLarge.copyWith(
  color: context.appColors.textPrimary,
)
</code></pre>
<p><code>context.appTypography</code> is an extension on <code>BuildContext</code> providing the full type scale.</p>
<h4 id="heading-border-radius">Border Radius</h4>
<pre><code class="language-dart">// Do not do this
borderRadius: BorderRadius.circular(8)

// Do this
borderRadius: BorderRadius.circular(AppRadius.sm)
</code></pre>
<p><code>AppRadius</code> constants: <code>xs</code> (4), <code>sm</code> (8), <code>md</code> (12), <code>lg</code> (16), <code>xl</code> (24), <code>round</code> (999).</p>
<h3 id="heading-the-navigation-skill">The Navigation Skill</h3>
<pre><code class="language-markdown">---
name: flutter-navigation
description: Implement navigation using GoRouter. Use when adding routes, navigating
between screens, implementing deep links, setting up route guards or redirects,
handling authentication-gated routes, working with nested navigation or shell routes,
or any task involving navigation, routing, back button, browser URL, or deep link.
---

# Flutter Navigation

This project uses GoRouter for all navigation. Do not use Navigator.push, Navigator.pushNamed, Navigator.pop (only via GoRouter), or any Navigator API that bypasses GoRouter.
</code></pre>
<h4 id="heading-route-constants">Route Constants</h4>
<p>All route paths are constants in <code>core/router/routes.dart</code>:</p>
<pre><code class="language-dart">abstract class Routes {
  static const splash = '/';
  static const login = '/auth/login';
  static const register = '/auth/register';
  static const home = '/home';
  static const profile = '/home/profile/:userId';
  static const editProfile = '/home/profile/:userId/edit';
  static const settings = '/settings';
}
</code></pre>
<p>Never use string literals for navigation. Always use <code>Routes.home</code>, not <code>'/home'</code>.</p>
<h4 id="heading-navigation-methods">Navigation Methods</h4>
<pre><code class="language-dart">// Replace the current location (no back button to previous)
context.go(Routes.home);

// Push on top (back button returns to previous location)
context.push(Routes.profile.replaceAll(':userId', userId));

// Pop (go back)
context.pop();

// Pop with a result
context.pop(result);
</code></pre>
<p>Never use <code>Navigator.of(context).push(...)</code>. It bypasses GoRouter and breaks deep links.</p>
<h4 id="heading-router-definition">Router Definition</h4>
<p>All routes are defined in <code>core/router/app_router.dart</code>:</p>
<pre><code class="language-dart">final router = GoRouter(
  initialLocation: Routes.splash,
  redirect: _redirectLogic,
  routes: [
    GoRoute(
      path: Routes.home,
      pageBuilder: (context, state) =&gt; NoTransitionPage(
        child: const HomeScreen(),
      ),
    ),
    GoRoute(
      path: Routes.profile,
      builder: (context, state) {
        final userId = state.pathParameters['userId']!;
        return ProfileScreen(userId: userId);
      },
    ),
  ],
);
</code></pre>
<h4 id="heading-typed-parameters">Typed Parameters</h4>
<p>Extract path parameters from <code>state.pathParameters</code>, and query parameters from <code>state.uri.queryParameters</code>. Never parse the path string manually.</p>
<h2 id="heading-essential-dart-skills-every-developer-should-write">Essential Dart Skills Every Developer Should Write</h2>
<p>Beyond Flutter-specific skills, pure Dart development benefits enormously from team-level skills. These apply to any Dart code: business logic, data processing, testing, or CLI tools.</p>
<h3 id="heading-the-dart-model-and-freezed-skill">The Dart Model and Freezed Skill</h3>
<pre><code class="language-markdown">---
name: dart-models-freezed
description: Create immutable data models using the freezed package with json_serializable
for serialization. Use when creating new data models, DTOs, request or response objects,
value objects, or any Dart class that represents structured data. Applies when working
with JSON parsing, API response mapping, or defining data structures.
---

# Dart Models with Freezed

All data models use the freezed package for immutability and code generation.
</code></pre>
<h4 id="heading-model-definition">Model Definition</h4>
<pre><code class="language-dart">import 'package:freezed_annotation/freezed_annotation.dart';

part 'user_profile.freezed.dart';
part 'user_profile.g.dart';

@freezed
class UserProfile with _$UserProfile {
  const factory UserProfile({
    required String id,
    required String name,
    required String email,
    String? avatarUrl,
    @Default(false) bool isVerified,
    required DateTime createdAt,
  }) = _UserProfile;

  factory UserProfile.fromJson(Map&lt;String, dynamic&gt; json) =&gt;
      _$UserProfileFromJson(json);
}
</code></pre>
<p><code>@freezed</code> triggers code generation that produces an immutable class with a named constructor, <code>copyWith</code> for creating modified copies, <code>==</code> and <code>hashCode</code> based on all fields, <code>toString</code> for debugging, and <code>fromJson</code>/<code>toJson</code> via <code>json_serializable</code>.</p>
<p>The <code>part</code> directives are mandatory and must match the filename. <code>user_profile.dart</code> generates <code>user_profile.freezed.dart</code> and <code>user_profile.g.dart</code>.</p>
<h4 id="heading-field-rules">Field Rules</h4>
<p>Use <code>required</code> for fields that must always be present. Use <code>String?</code> (nullable) for optional fields. Use <code>@Default(value)</code> for fields with a sensible default that avoids nullability. And use <code>@JsonKey(name: 'field_name')</code> when the JSON field name differs from the Dart field name.</p>
<h4 id="heading-after-adding-or-modifying-a-model">After Adding or Modifying a Model</h4>
<p>Always run:</p>
<pre><code class="language-bash">dart run build_runner build --delete-conflicting-outputs
</code></pre>
<p>Never manually edit <code>.freezed.dart</code> or <code>.g.dart</code> files. They're generated and will be overwritten on the next build.</p>
<h4 id="heading-dtos-vs-domain-models">DTOs vs Domain Models</h4>
<p>Data Transfer Objects (DTOs) live in <code>data/</code> and map directly to API shapes. Domain models live in <code>domain/</code> and represent the app's internal data model.</p>
<p>A DTO may have fields like <code>created_at</code> (snake_case from API). The domain model has <code>createdAt</code> (camelCase). The repository maps from DTO to domain model.</p>
<h3 id="heading-the-dart-pattern-matching-skill">The Dart Pattern Matching Skill</h3>
<pre><code class="language-markdown">---
name: dart-pattern-matching-idiomatic
description: Use Dart 3 pattern matching, switch expressions, and sealed class hierarchies
for exhaustive control flow. Use when working with sealed classes, enums, discriminated
unions, conditional logic on types, or any switch statement that could be a switch
expression. Applies when refactoring if-else chains, handling multiple subtypes, or
implementing business logic that branches on type.
---

# Dart Pattern Matching

Use Dart 3 pattern matching for all control flow that involves type discrimination, sealed class hierarchies, or structural decomposition of data.
</code></pre>
<h4 id="heading-switch-expressions-over-switch-statements">Switch Expressions Over Switch Statements</h4>
<pre><code class="language-dart">// Do not do this (switch statement is an imperative flow)
switch (state) {
  case ProfileLoading():
    return const CircularProgressIndicator();
  case ProfileLoaded():
    return ProfileContent(profile: state.profile);
  case ProfileLoadFailure():
    return ErrorView(message: state.message);
  default:
    return const SizedBox.shrink();
}

// Do this (switch expression is a value, works in build methods)
return switch (state) {
  ProfileInitial() =&gt; const SizedBox.shrink(),
  ProfileLoading() =&gt; const CircularProgressIndicator(),
  ProfileLoaded(:final profile) =&gt; ProfileContent(profile: profile),
  ProfileLoadFailure(:final message) =&gt; ErrorView(message: message),
};
</code></pre>
<p>Switch expressions are values, not statements. They work naturally as the argument to <code>return</code> or as the value of a variable. Sealed class hierarchies make them exhaustive: if you add a new state, the compiler tells you every switch expression that needs to handle it.</p>
<h4 id="heading-destructuring-in-patterns">Destructuring in Patterns</h4>
<pre><code class="language-dart">// Access fields directly in the pattern
case ProfileLoaded(:final profile) =&gt; ProfileContent(profile: profile),
// Equivalent to:
case ProfileLoaded() =&gt; ProfileContent(profile: state.profile),
</code></pre>
<p>The <code>:final field</code> syntax inside a pattern binds the field's value directly in the case branch. This eliminates the need to access <code>state.profile</code> separately and makes the code more concise.</p>
<h4 id="heading-guard-clauses">Guard Clauses</h4>
<pre><code class="language-dart">return switch (state) {
  ProfileLoaded(:final profile) when profile.isVerified =&gt; VerifiedProfileView(profile: profile),
  ProfileLoaded(:final profile) =&gt; UnverifiedProfileView(profile: profile),
  _ =&gt; const LoadingView(),
};
</code></pre>
<p><code>when</code> adds a guard clause to a pattern. The case only matches when both the pattern matches and the guard condition is true. Guards allow fine-grained branching within a single type.</p>
<h4 id="heading-record-patterns">Record Patterns</h4>
<pre><code class="language-dart">// Matching on records
final (name, age) = getUserInfo();

// In switch expressions
final description = switch ((user.name, user.isAdmin)) {
  (final name, true) =&gt; '$name (Admin)',
  (final name, false) =&gt; name,
};
</code></pre>
<p>Records are structural tuples. Pattern matching on records extracts the components directly without named accessors.</p>
<h4 id="heading-converting-if-else-chains">Converting If-Else Chains</h4>
<p>When you see an if-else chain that branches on type or value, convert it to a switch expression:</p>
<pre><code class="language-dart">// Do not do this
String label;
if (priority == Priority.high) {
  label = 'Urgent';
} else if (priority == Priority.medium) {
  label = 'Normal';
} else {
  label = 'Low';
}

// Do this
final label = switch (priority) {
  Priority.high =&gt; 'Urgent',
  Priority.medium =&gt; 'Normal',
  Priority.low =&gt; 'Low',
};
</code></pre>
<h3 id="heading-the-dart-testing-conventions-skill">The Dart Testing Conventions Skill</h3>
<pre><code class="language-markdown">---
name: dart-testing-conventions
description: Write Dart unit tests following package:test conventions with mocktail mocks,
descriptive group/test naming, and correct async testing patterns. Use when writing any test
file, adding tests to existing files, mocking dependencies, testing async functions,
or verifying error handling behavior.
---

# Dart Testing Conventions
</code></pre>
<h4 id="heading-test-file-structure">Test File Structure</h4>
<pre><code class="language-dart">import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:myapp/features/profile/data/profile_repository_impl.dart';
import 'package:myapp/core/errors/app_failure.dart';

class MockProfileRemoteDataSource extends Mock
    implements ProfileRemoteDataSource {}

class MockProfileLocalDataSource extends Mock
    implements ProfileLocalDataSource {}

void main() {
  late MockProfileRemoteDataSource mockRemote;
  late MockProfileLocalDataSource mockLocal;
  late ProfileRepositoryImpl repository;

  setUp(() {
    mockRemote = MockProfileRemoteDataSource();
    mockLocal = MockProfileLocalDataSource();
    repository = ProfileRepositoryImpl(
      remote: mockRemote,
      local: mockLocal,
    );
  });

  group('ProfileRepositoryImpl', () {
    group('getProfile', () {
      test(
        'returns Right(profile) when remote data source succeeds',
        () async {
          when(() =&gt; mockRemote.getProfile(any()))
              .thenAnswer((_) async =&gt; right(fakeProfileDto));

          final result = await repository.getProfile('user123');

          expect(result.isRight(), isTrue);
          expect(result.getOrElse(() =&gt; null)?.id, equals('user123'));
        },
      );

      test(
        'returns Left(NetworkFailure) when remote throws network error',
        () async {
          when(() =&gt; mockRemote.getProfile(any()))
              .thenAnswer((_) async =&gt; left(NetworkFailure(message: 'No internet')));

          final result = await repository.getProfile('user123');

          expect(result.isLeft(), isTrue);
          expect(result.fold((f) =&gt; f, (_) =&gt; null), isA&lt;NetworkFailure&gt;());
        },
      );
    });
  });
}
</code></pre>
<h4 id="heading-test-naming">Test Naming</h4>
<p>Use descriptive test names that follow the pattern <strong>"does X when Y"</strong> or <strong>"returns X when Y"</strong>.</p>
<p><strong>Examples:</strong> <code>returns Right(profile) when remote data source succeeds</code>, <code>returns Left(NetworkFailure) when connection fails</code>, and <code>calls local data source when remote fails</code>.</p>
<p>Avoid using <strong>"test"</strong> or <strong>"should"</strong> in test names. For example, use <code>returns profile when repository call succeeds</code> instead of <code>test that profile is returned correctly</code> or <code>should return profile when called</code>.</p>
<h4 id="heading-mock-setup">Mock Setup</h4>
<p>Create fresh mocks in <code>setUp</code>, not at the top level of <code>main</code>. This ensures state from one test can't leak into another.</p>
<p>Use <code>registerFallbackValue</code> in <code>setUpAll</code> for any custom types passed to <code>any()</code>:</p>
<pre><code class="language-dart">setUpAll(() {
  registerFallbackValue(const ProfileLoadRequested(userId: ''));
  registerFallbackValue(left&lt;AppFailure, UserProfile&gt;(const NotFoundFailure()));
});
</code></pre>
<h4 id="heading-async-testing">Async Testing</h4>
<pre><code class="language-dart">// For Future results
final result = await repository.getProfile('user123');
expect(result.isRight(), isTrue);

// For Stream results
expectLater(
  bloc.stream,
  emitsInOrder([ProfileLoading(), ProfileLoaded(profile: fakeProfile)]),
);
</code></pre>
<p>Always use <code>await</code> for Futures. Use <code>expectLater</code> with <code>emitsInOrder</code> for Streams. Don't use <code>await Future.delayed(...)</code> in tests. Use <code>pump()</code> for widget tests or mock the async behavior with <code>thenAnswer</code>.</p>
<h2 id="heading-skills-for-architecture-and-large-codebases">Skills for Architecture and Large Codebases</h2>
<p>As your Flutter project grows, the complexity of architectural decisions increases. These skills are designed for larger codebases where consistent architecture is especially important.</p>
<h3 id="heading-the-performance-skill">The Performance Skill</h3>
<pre><code class="language-markdown">---
name: flutter-performance
description: Apply Flutter performance best practices including const widgets, selective
rebuilds, lazy loading, and proper use of keys. Use when optimizing screens, implementing
lists, adding animations, working with images, or any task where rendering performance,
jank, frame rate, or memory usage is relevant.
---

# Flutter Performance
</code></pre>
<h4 id="heading-const-widgets">Const Widgets</h4>
<p>Every widget that can be const must be const. Every constructor that can be const must have a const constructor:</p>
<pre><code class="language-dart">// Do not do this
class UserAvatar extends StatelessWidget {
  UserAvatar({super.key, required this.url}); // Missing const
  final String url;

  @override
  Widget build(BuildContext context) {
    return CircleAvatar(  // Missing const where possible
      backgroundImage: NetworkImage(url),
    );
  }
}

// Do this
class UserAvatar extends StatelessWidget {
  const UserAvatar({super.key, required this.url});
  final String url;

  @override
  Widget build(BuildContext context) {
    return CircleAvatar(
      backgroundImage: NetworkImage(url),
    );
  }
}
</code></pre>
<h4 id="heading-list-performance">List Performance</h4>
<p>Use <code>ListView.builder</code> for lists with unknown or large item counts. Never use <code>ListView</code> with <code>children</code> for lists that could grow beyond 20 items.</p>
<pre><code class="language-dart">// Do not do this for variable-length lists
ListView(
  children: items.map((item) =&gt; ItemCard(item: item)).toList(),
)

// Do this
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) =&gt; ItemCard(item: items[index]),
)
</code></pre>
<h4 id="heading-selective-rebuilds-with-blocselector">Selective Rebuilds with BlocSelector</h4>
<p>When only part of a widget tree depends on part of a state, use BlocSelector to rebuild only the dependent widget:</p>
<pre><code class="language-dart">// Do not do this (entire subtree rebuilds on any state change)
BlocBuilder&lt;CartBloc, CartState&gt;(
  builder: (context, state) =&gt; CartBadge(count: state is CartLoaded ? state.itemCount : 0),
)

// Do this (rebuilds only when item count changes)
BlocSelector&lt;CartBloc, CartState, int&gt;(
  selector: (state) =&gt; state is CartLoaded ? state.itemCount : 0,
  builder: (context, count) =&gt; CartBadge(count: count),
)
</code></pre>
<h4 id="heading-image-optimization">Image Optimization</h4>
<p>Use <code>cached_network_image</code> for network images. Never use <code>Image.network</code> directly. Use <code>cacheWidth</code> and <code>cacheHeight</code> to resize images at decode time for list items. Use WebP format on Android and HEIC/WebP on iOS for significantly smaller file sizes.</p>
<h3 id="heading-the-accessibility-skill">The Accessibility Skill</h3>
<pre><code class="language-markdown">---
name: flutter-accessibility
description: Implement accessibility features including semantic labels, focus management,
contrast requirements, and screen reader support. Use when creating interactive widgets,
images, icons, form fields, or any element that needs to be usable by people with
disabilities. Apply when working with Semantics, ExcludeSemantics, Focus, or FocusNode.
---

# Flutter Accessibility
</code></pre>
<h4 id="heading-semantic-labels-on-interactive-elements">Semantic Labels on Interactive Elements</h4>
<p>Every <code>IconButton</code>, <code>FloatingActionButton</code>, and <code>GestureDetector</code> that performs a meaningful action must have a semantic label:</p>
<pre><code class="language-dart">// Do not do this
IconButton(
  onPressed: _onShare,
  icon: const Icon(Icons.share),
)

// Do this
IconButton(
  onPressed: _onShare,
  icon: const Icon(Icons.share),
  tooltip: 'Share post', // Used as semantic label on mobile
)
</code></pre>
<h4 id="heading-images-and-decorative-icons">Images and Decorative Icons</h4>
<p>Purely decorative icons and images must be marked as such so screen readers skip them:</p>
<pre><code class="language-dart">// Decorative icon (no semantic value)
Icon(
  Icons.star,
  semanticLabel: '', // Empty label marks it as decorative
)

// Informative icon (has semantic value)
Icon(
  Icons.warning,
  semanticLabel: 'Warning: action cannot be undone',
)
</code></pre>
<h4 id="heading-form-accessibility">Form Accessibility</h4>
<p>All form fields must have labels that screen readers announce. Never rely solely on placeholder text for field identification:</p>
<pre><code class="language-dart">TextFormField(
  decoration: const InputDecoration(
    labelText: 'Email address',    // Screen readers announce this
    hintText: 'name@example.com', // Only visible when empty
  ),
)
</code></pre>
<h4 id="heading-minimum-touch-target-size">Minimum Touch Target Size</h4>
<p>All interactive elements must be at least 48x48 dp. If the visual size is smaller, use <code>SizedBox</code> or <code>Padding</code> to expand the hit area:</p>
<pre><code class="language-dart">SizedBox(
  width: 48,
  height: 48,
  child: IconButton(
    iconSize: 20,
    onPressed: _onClose,
    icon: const Icon(Icons.close),
  ),
)
</code></pre>
<h2 id="heading-advanced-skill-patterns">Advanced Skill Patterns</h2>
<h3 id="heading-teaching-tool-usage-as-part-of-task-completion">Teaching Tool Usage as Part of Task Completion</h3>
<p>Skills can make specific commands part of the definition of "task complete." This is one of the most powerful patterns because it closes the quality loop automatically:</p>
<pre><code class="language-markdown">## Required Verification Steps

After any code generation or modification task, always:

1. Run `dart format .` to format all Dart files
2. Run `flutter analyze` to check for analyzer errors and warnings
3. Run `flutter test` to verify no tests are broken by the changes
4. If any of the above produce errors, fix them before reporting the task as complete

Do not report a task complete if any of these commands fail.
</code></pre>
<p>This pattern transforms the skill from a code generation guide into a full quality assurance workflow. The agent doesn't just write code: it validates the code against your quality bar before saying it's finished.</p>
<h3 id="heading-conditional-rules-based-on-context">Conditional Rules Based on Context</h3>
<p>Some rules apply only in certain circumstances. Express these with conditional phrasing that helps the agent apply them correctly:</p>
<pre><code class="language-markdown">## Context-Dependent Rules

When a widget initiates a network request:
- Disable all interactive elements while the request is in flight
- Show a loading indicator appropriate to the UI scope
- Handle errors with a user-readable message
- Re-enable interactive elements when the request completes (success or failure)

When a Bloc handles multiple independent operations:
- Create separate error states for each operation (not a single generic Error state)
- Name each error state after the operation: ProfileLoadFailure, ProfileUpdateFailure

When creating a widget that appears in a ListView:
- Always provide a key
- Use const constructors wherever possible
- Consider using ListView.builder at the list level if the list may exceed 50 items
</code></pre>
<h3 id="heading-cross-referencing-skills">Cross-Referencing Skills</h3>
<p>Complex tasks may require multiple skills working together. Reference related skills explicitly in your skill body so the agent knows to load them:</p>
<pre><code class="language-markdown">## Related Skills

When this skill's rules result in widget extraction, also apply the
flutter-file-organization skill to determine the correct file location.

When the extracted component requires state management, apply the
flutter-bloc-state-management skill to determine whether it needs its own Bloc.

When writing tests for code created using this skill, apply the
dart-testing-conventions skill for test naming and structure.
</code></pre>
<h3 id="heading-skills-that-encode-hard-won-production-lessons">Skills That Encode Hard-Won Production Lessons</h3>
<p>Some of the most valuable skill content comes from specific production incidents. Document the lesson from the incident as a skill rule with enough context that anyone (and any agent) understands why it exists:</p>
<h4 id="heading-buildcontext-after-async-gaps-learned-from-production">BuildContext After Async Gaps (Learned from Production)</h4>
<p>Always check mounted before using BuildContext after any await:</p>
<pre><code class="language-dart">Future&lt;void&gt; _onSubmit() async {
  final result = await _repository.save(formData);

  // WRONG: context may be stale if widget was disposed during the await
  ScaffoldMessenger.of(context).showSnackBar(...);

  // CORRECT: check mounted first
  if (!mounted) return;
  ScaffoldMessenger.of(context).showSnackBar(...);
}
</code></pre>
<p>This error is silent in development (the widget is usually still mounted by the time the async operation completes) but causes "FlutterError (looking up a deactivated widget's ancestor)" crashes in production where network latency is higher and users navigate away while operations are in flight.</p>
<h2 id="heading-package-level-skills-teaching-the-agent-your-libraries">Package-Level Skills: Teaching the Agent Your Libraries</h2>
<p>The <code>skills</code> CLI tool (available as a Dart package at <code>pub.dev/packages/skills</code>) enables a powerful pattern: installing skills directly from your project's package dependencies.</p>
<pre><code class="language-bash"># Install the Dart skills CLI globally
dart pub global activate skills

# Install skills from all packages in your project that ship skills
skills get
</code></pre>
<p>When you add a package to your <code>pubspec.yaml</code> and run <code>skills get</code>, the CLI searches each package in your dependency tree for a <code>skills/</code> directory and installs those skills automatically. This means package authors can ship their own usage instructions directly to agent users.</p>
<h3 id="heading-why-this-matters">Why This Matters</h3>
<p>Before package-level skills, adding a new package to a Flutter project meant the agent knew the package existed (from its training data) but might not know the current API, preferred usage patterns, or common mistakes. This led to agents hallucinating method names, using deprecated APIs, or missing the idiomatic usage pattern the package author intended.</p>
<p>With package-level skills, the agent receives authoritative usage instructions directly from the people who wrote the package. When <code>go_router</code> ships a <code>skills/go-router-navigation.md</code> file, every Flutter team that runs <code>skills get</code> after adding GoRouter gets a skill that teaches the agent exactly how GoRouter works, from the GoRouter team.</p>
<h3 id="heading-writing-skills-for-your-own-packages">Writing Skills for Your Own Packages</h3>
<p>If you maintain internal Dart or Flutter packages that your team uses, shipping skills with them is a high-value investment:</p>
<pre><code class="language-plaintext">my_design_system/
  lib/
    src/
      components/
    my_design_system.dart
  skills/
    my-design-system-components.md    &lt;- teaches agents how to use your components
    my-design-system-theming.md       &lt;- teaches agents your theming system
  pubspec.yaml
  README.md
</code></pre>
<pre><code class="language-markdown">---
name: my-design-system-components
description: Use the MyDesignSystem component library for UI elements. Use when creating
any UI elements including buttons, cards, form fields, navigation elements, or any visual
component. Apply instead of raw Material or Cupertino widgets wherever a design system
component exists.
---

# MyDesignSystem Component Usage

Always use MyDesignSystem components instead of raw Flutter widgets where equivalents exist.

## Available Components

DsButton replaces ElevatedButton, TextButton, and OutlinedButton.
DsCard replaces Card.
DsTextField replaces TextFormField.
DsAvatar replaces CircleAvatar.
DsChip replaces Chip.
DsBottomSheet replaces showModalBottomSheet.

## DsButton Usage
</code></pre>
<p>Do not do this: <code>ElevatedButton( onPressed: _onSubmit, child: const Text('Submit'), )</code>.</p>
<p>Do this: <code>DsButton( label: 'Submit', onPressed: _onSubmit, variant: DsButtonVariant.primary, )</code>.</p>
<pre><code class="language-plaintext">
`DsButton.variant` accepts `primary`, `secondary`, `destructive`, and `ghost`. When loading, pass `isLoading: true` to show the button's built-in loading state.
</code></pre>
<p>When a developer on your team runs <code>skills get</code>, this skill installs automatically alongside any official Flutter or Dart skills, giving the agent complete knowledge of your internal component library.</p>
<h2 id="heading-skills-vs-rules-vs-mcp-knowing-the-difference">Skills vs Rules vs MCP: Knowing the Difference</h2>
<p>Agent skills exist alongside two other agent customization mechanisms: AI rules files and MCP servers. Understanding the distinct role of each helps you put knowledge in the right place.</p>
<h3 id="heading-three-customization-mechanisms">Three Customization Mechanisms</h3>
<h4 id="heading-1-ai-rules-always-in-context-project-wide-facts">1. AI rules (always in context, project-wide facts).</h4>
<p><code>CLAUDE.md</code>, <code>AGENTS.md</code>, and <code>.cursorrules</code> should contain facts about the project that are always true. These files are loaded for every task and every session.</p>
<p>They're best used for information such as the project name and package identifier, Flutter and Dart SDK versions, core packages like <code>flutter_bloc</code> and <code>go_router</code>, minimum platform versions such as Android API 24 and iOS 15, and the project's architecture style such as feature-first or clean architecture. Detailed how-to instructions shouldn't be placed here because those belong in skills.</p>
<h4 id="heading-2-skills-agentsskillsmd-loaded-progressively">2. Skills (<code>.agents/skills/*.md</code>, loaded progressively).</h4>
<p>Skills should contain instructions for how to perform a specific category of work. They're loaded only when the agent detects that they are relevant to the current task.</p>
<p>They're best used for instructions such as how to organize Flutter files, how to implement BLoC state management, how to write tests, how to handle errors, and other task-specific patterns that aren't always relevant. Project-wide facts shouldn't be placed in skills because those belong in the project rules.</p>
<h4 id="heading-3-mcp-servers-extend-the-agents-capabilities-with-tools">3. MCP servers (extend the agent's capabilities with tools).</h4>
<p>MCP servers are configured through the agent-specific MCP configuration and are used to extend the agent's capabilities by providing access to tools and external data. Their tools are available throughout the session.</p>
<p>They're best used for tasks such as looking up Flutter documentation through a Dart MCP server, retrieving package information from <code>pub.dev</code>, running Flutter commands in the project, reading logs from a connected device, and searching for code across the repository. Instructions, conventions, and project-specific rules shouldn't be placed in MCP servers because those belong in the rules and skills.</p>
<p>A useful heuristic: if the information would be in a README, it probably belongs in a rules file or skill. If the information requires a network call or executing a program, it belongs in an MCP server. If the information is only relevant for a specific type of task, it belongs in a skill rather than a rules file.</p>
<p>Another heuristic: context budget. Rules files are always in context, so they consume context budget on every task regardless of relevance. Keep rules files short (under 50 lines) and factual. Skills amortize their context cost because they are only loaded when relevant. MCP servers have their own cost model based on tool calls.</p>
<h2 id="heading-organizing-skills-in-a-team">Organizing Skills in a Team</h2>
<h3 id="heading-skills-as-shared-team-knowledge">Skills as Shared Team Knowledge</h3>
<p>The <code>.agents/skills/</code> directory must be committed to your Git repository. When you commit a skill, every developer on the team gets it on their next <code>git pull</code>. When a new developer joins, they clone the repo and immediately have the accumulated skill knowledge the team has built. When someone writes a skill from a production incident, that lesson is preserved in the repository alongside the code it protects.</p>
<p>This makes skills a living institutional knowledge system: the skill file is simultaneously the instruction for the AI agent and the documentation of the standard itself. Unlike a wiki page or a Confluence article, a skill is read by the tooling that actually generates code, not just by developers who may or may not remember to apply it.</p>
<h3 id="heading-skill-review-process">Skill Review Process</h3>
<p>Changes to skill files should go through the same pull request review process as code changes. A skill that encodes a wrong convention or expresses a rule too vaguely can produce incorrect output across the entire team's agent usage until it's corrected.</p>
<p>Here's a skill review checklist, to check before merging a skill change:</p>
<ul>
<li><p>The description correctly and completely describes when this skill applies.</p>
</li>
<li><p>Every rule is specific enough to change agent behavior and isn't vague guidance.</p>
</li>
<li><p>Counterexamples are provided for patterns that are common in training data.</p>
</li>
<li><p>Code examples compile correctly in isolation.</p>
</li>
<li><p>The skill doesn't duplicate content in another skill.</p>
</li>
<li><p>The skill was tested by asking the agent to perform the relevant task and verifying that the output follows the skill's rules.</p>
</li>
<li><p>The skill has been reviewed by at least one other team member who would use it in their daily work.</p>
</li>
</ul>
<h3 id="heading-keeping-skills-current">Keeping Skills Current</h3>
<p>Skills become outdated when your team's conventions change: when you migrate from one navigation library to another, adopt a new testing framework, update your design system, or refactor your error handling approach. An outdated skill is worse than no skill because it actively steers the agent toward patterns you no longer use.</p>
<p>Treat dependency upgrades as skill review triggers. When you upgrade <code>go_router</code> to a new major version, review the navigation skill to ensure it reflects the current API. When you adopt a new pattern from a team retrospective, update the relevant skill in the same PR.</p>
<h3 id="heading-skill-discoverability-within-your-team">Skill Discoverability Within Your Team</h3>
<p>As your skill library grows, developers need to be able to find the right skill for their task. Use consistent naming conventions and consider maintaining a brief skills index:</p>
<pre><code class="language-markdown"># .agents/skills/README.md (not a skill, just an index)

## Flutter Skills
flutter-feature-architecture      -- Feature folder structure and layer rules
flutter-bloc-state-management     -- Bloc events, states, and widget integration
flutter-file-organization         -- File splitting, extraction, and naming
flutter-error-handling            -- Typed failures and Either return types
flutter-navigation                -- GoRouter routes, navigation methods, deep links
flutter-theming                   -- Design tokens, color extensions, spacing constants
flutter-testing                   -- Widget tests, Bloc tests, and test naming
flutter-accessibility             -- Semantic labels, focus, and touch targets
flutter-performance               -- Const widgets, selective rebuilds, list optimization

## Dart Skills
dart-models-freezed               -- Freezed models, json_serializable, DTOs
dart-testing-conventions          -- package:test conventions, mocktail, async testing
dart-pattern-matching-idiomatic   -- Switch expressions, sealed classes, destructuring
dart-run-static-analysis          -- analysis_options.yaml, dart analyze, dart fix
</code></pre>
<p>This index isn't read by agents (it's a <code>README.md</code>, not a skill file). It's for developers who are new to the project and want to know what skills exist before asking the agent to perform tasks.</p>
<h2 id="heading-best-practices-for-writing-skills">Best Practices for Writing Skills</h2>
<h3 id="heading-start-from-real-mistakes-not-ideal-patterns">Start from Real Mistakes, Not Ideal Patterns</h3>
<p>The most effective skills come from observing AI-generated code that was wrong in a specific, reproducible way. The mistake is evidence that the agent's default behavior needs correction for your project. Every time you manually fix AI output, that fix is a skill rule.</p>
<p>Ideal-pattern skills ("here is how Bloc should work in theory") are less effective than mistake-correction skills ("the agent always produces X but we need Y, so the rule is Z"). The mistake tells you where the training data diverges from your conventions. The rule corrects it.</p>
<h3 id="heading-test-skills-before-committing">Test Skills Before Committing</h3>
<p>After writing a skill, test it by asking your agent to perform the task the skill covers. Ask the agent to create a new screen with Bloc state management, or split a large file, or write unit tests for a repository. Then verify that the output follows every rule in your skill.</p>
<p>Rules that aren't being followed need to be either more explicit, given a counterexample, or combined with a more specific description that helps the agent recognize when to load the skill.</p>
<h3 id="heading-one-skill-per-domain-of-expertise">One Skill per Domain of Expertise</h3>
<p>Resist the temptation to write one large skill that covers everything. A skill per domain (file organization, state management, testing, theming, navigation, error handling) is easier to maintain, loads progressively (so each skill is only in context when relevant), and is easier to share with other teams or publish as a community resource.</p>
<h3 id="heading-write-the-description-with-trigger-word-richness">Write the Description with Trigger-Word Richness</h3>
<p>The description is the only part of a skill that is always read. Pack it with the specific trigger words and phrases that indicate the skill is relevant:</p>
<pre><code class="language-yaml"># Trigger-poor description
description: How to set up navigation in Flutter.

# Trigger-rich description
description: Implement navigation using GoRouter in Flutter apps. Use when adding routes,
navigating between screens, setting up deep links, handling authentication redirects,
configuring nested navigation, working with ShellRoutes, or any task involving
Navigator, route, path, deep link, URL, back button, or go_router package.
</code></pre>
<p>The trigger-rich description will match a much wider range of task descriptions, ensuring the skill loads when it is relevant rather than only on exact phrase matches.</p>
<h2 id="heading-common-mistakes-when-writing-skills">Common Mistakes When Writing Skills</h2>
<h3 id="heading-rules-that-are-too-vague-to-change-behavior">Rules That Are Too Vague to Change Behavior</h3>
<pre><code class="language-markdown"># These change nothing: the agent was already trying to do these
- Write clean, maintainable code.
- Follow Flutter best practices.
- Use the appropriate state management solution.
- Organize files logically.

# These change specific behavior: the agent was doing something different
- Place every extracted widget class in the widgets/ subdirectory of its feature folder.
- Name BlocEvent subclasses as past-tense verb phrases: ProfileLoadRequested, not LoadProfile.
- Never use Navigator.push; use context.go() or context.push() from GoRouter.
- Mark every widget constructor parameter with required unless it has a default value.
</code></pre>
<p>Vague rules describe aspirations. Specific rules describe concrete, verifiable behaviors. Every rule in a skill should answer the question: "What would an agent do differently after reading this rule compared to before?"</p>
<h3 id="heading-missing-the-counterexample-for-high-frequency-wrong-patterns">Missing the Counterexample for High-Frequency Wrong Patterns</h3>
<p>Some wrong patterns appear millions of times in training data. An agent that has learned <code>_buildHeaderSection()</code> as a valid Flutter pattern from thousands of examples may not abandon it based on a text rule alone.</p>
<p>Show the exact code the agent would produce and contrast it with the code you want. This is effective because the agent recognizes the specific code pattern, and the contrast communicates the rule at the code level, not just the text level.</p>
<h3 id="heading-descriptions-that-dont-trigger-on-the-right-tasks">Descriptions That Don't Trigger on the Right Tasks</h3>
<p>A skill about Bloc state management that has a description saying "implement state management" won't load when someone asks "add a loading state to the checkout screen." The description needs to include "loading state" as a trigger phrase.</p>
<p>Test your descriptions by thinking about the variety of ways someone would describe tasks that need this skill, and ensure the description includes trigger phrases from all of those ways.</p>
<h3 id="heading-not-committing-skills-to-version-control">Not Committing Skills to Version Control</h3>
<p>Skills left on a single developer's machine are personal notes, not team knowledge. Committed skills are institutional knowledge that new hires get from day one, that agent users across the team benefit from without separate setup, and that can be reviewed, improved, and maintained like code. Always commit <code>.agents/skills/</code> to Git.</p>
<h3 id="heading-writing-skills-that-are-too-prescriptive">Writing Skills That Are Too Prescriptive</h3>
<p>A skill should encode conventions, not dictate every possible implementation decision. If your skill specifies the exact pixel dimensions of a widget, the exact color of a specific loading indicator, or the exact parameter order of a constructor, you're over-specifying in ways that prevent the agent from making reasonable decisions in novel situations.</p>
<p>Skills should capture the structural and architectural patterns that are genuinely inconsistent without guidance. Implementation details that have many equally valid choices shouldn't be in skills.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>The shift to agentic development in Flutter isn't about replacing developers. It's about multiplying what developers can accomplish.</p>
<p>An AI agent with strong skills can draft a complete, architecture-correct feature implementation that follows your team's exact conventions in minutes. A senior developer reviews it, adjusts, and ships. The skill is what bridges the gap between the agent's general knowledge and your team's specific standards.</p>
<p>What makes skills genuinely powerful is that they're the only part of the AI development workflow that contains knowledge the model wasn't trained on. The model has learned from millions of lines of public Flutter and Dart code. But it has never seen your codebase. It has never made a mistake in your project and been corrected. It has never attended your team's architecture discussions or retrospectives. It doesn't know that your team tried one pattern, found it painful, and deliberately chose a different one. Your skills are the container for all of that knowledge.</p>
<p>The official Flutter skills from <code>github.com/flutter/agent-plugins</code> and the official Dart skills from <code>github.com/dart-lang/skills</code> give you a production-quality starting point that covers the most common Flutter and Dart development patterns. The <code>skills</code> CLI tool makes installing them as simple as a single npm command. The package-level skills system means your dependencies can ship their own usage instructions and update them as the package evolves.</p>
<p>But the skills you write yourself, drawn from your own production incidents, your own code review feedback, and your own architectural decisions, are the ones with the highest leverage. They encode knowledge that's irreplaceable because it can't be found in any public repository.</p>
<p>A rule like "never separate a StatefulWidget from its State class" comes from understanding Flutter's compilation model at a level that most training data does not communicate. A rule like "use sealed class hierarchies with final concrete classes for all Bloc events and states" comes from understanding both Dart 3's type system and the real-world benefits of exhaustive switching. A rule like "check mounted before using BuildContext after any await" comes from seeing the specific crash that happens in production when this rule is violated.</p>
<p>These rules, drawn from your experience, documented as skills, and committed to your repository, transform your AI agent from a generalist Flutter developer into a developer who knows your project. That transformation is worth every minute spent writing the skills.</p>
<h2 id="heading-references">References</h2>
<p><strong>Agent skills for Flutter and Dart (Flutter Documentation):</strong> Comprehensive guide to agent skills including the progressive disclosure model, official repositories, and universal installation commands. <a href="https://docs.flutter.dev/ai/agent-skills">https://docs.flutter.dev/ai/agent-skills</a></p>
<p><strong>Get Started with AI in Flutter (Flutter Documentation):</strong> Step-by-step setup guide for Claude Code, Antigravity, Codex, Cursor, and other agents including the official Flutter plugin installation instructions for each tool. <a href="https://docs.flutter.dev/ai/get-started">https://docs.flutter.dev/ai/get-started</a></p>
<p><strong>Flutter Agent Plugins Repository (GitHub):</strong> The official repository of Flutter agent skills maintained by the Flutter team, covering responsive layouts, GoRouter navigation, JSON serialization, widget testing, integration testing, BLoC patterns, and more. <a href="https://github.com/flutter/agent-plugins">https://github.com/flutter/agent-plugins</a></p>
<p><strong>Dart Skills Repository (GitHub):</strong> The official repository of Dart agent skills maintained by the Dart team, covering unit testing, static analysis, package tooling, pattern matching, CLI apps, native assets, and more. <a href="https://github.com/dart-lang/skills">https://github.com/dart-lang/skills</a></p>
<p><strong>Flutter AI Rules Documentation (Flutter Documentation):</strong> Documentation for project-wide AI rules files (CLAUDE.md, AGENTS.md, .cursorrules) and how they complement skills. <a href="https://docs.flutter.dev/ai/ai-rules">https://docs.flutter.dev/ai/ai-rules</a></p>
<p><strong>The Agent Skills Specification:</strong> The specification site that defines the universal SKILL.md format, directory conventions, and agent compatibility requirements. The source of truth for the skills standard. <a href="https://agentskills.io">https://agentskills.io</a></p>
<p><strong>skills Dart Package (pub.dev):</strong> The Dart CLI tool for installing agent skills from project dependencies. Enables package authors to ship skills alongside their packages and teams to install them automatically. <a href="https://pub.dev/packages/skills">https://pub.dev/packages/skills</a></p>
<p><strong>skills CLI (npm):</strong> The npm-distributed CLI for installing agent skills from GitHub repositories. Used for the canonical <code>npx skills add flutter/agent-plugins</code> installation command. <a href="https://www.npmjs.com/package/skills">https://www.npmjs.com/package/skills</a></p>
<p><strong>skills-registry Serverpod:</strong> A collection of agent skills for popular Dart and Flutter packages that do not yet ship their own skills, including Riverpod, flutter-shadcn-ui, and others. Maintained by the Serverpod team. <a href="https://github.com/serverpod/skills-registry">https://github.com/serverpod/skills-registry</a></p>
<p><strong>dhruvanbhalara/skills Premium Flutter Skills Documentation:</strong> An extensive documentation project covering the full list of available Flutter agent skills with detailed descriptions of what each skill covers and teaches. <a href="https://github.com/dhruvanbhalara/skills">https://github.com/dhruvanbhalara/skills</a></p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
