<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Alexandre Amado de Castro</title>
    <description>The latest articles on Forem by Alexandre Amado de Castro (@alexandreamadocastro).</description>
    <link>https://forem.com/alexandreamadocastro</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F231645%2F9b2a4756-b10d-42b1-bae6-8d580368eca8.jpg</url>
      <title>Forem: Alexandre Amado de Castro</title>
      <link>https://forem.com/alexandreamadocastro</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/alexandreamadocastro"/>
    <language>en</language>
    <item>
      <title>Stop Architecture Drift: Operationalizing ADRs with Automated Fitness Functions</title>
      <dc:creator>Alexandre Amado de Castro</dc:creator>
      <pubDate>Tue, 07 Apr 2026 02:43:57 +0000</pubDate>
      <link>https://forem.com/alexandreamadocastro/stop-architecture-drift-operationalizing-adrs-with-automated-fitness-functions-22oi</link>
      <guid>https://forem.com/alexandreamadocastro/stop-architecture-drift-operationalizing-adrs-with-automated-fitness-functions-22oi</guid>
      <description>&lt;p&gt;Six months after we standardized on OpenSearch, a pull request introduced Datadog into a service. The ADR existed. It had been discussed, approved, and stored in the repo. The PR was still green.&lt;/p&gt;

&lt;p&gt;That is architecture drift. Not because engineers are careless. Because memory does not scale across hundreds of people and dozens of repositories.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0gr93tutwb8mhgoj1eod.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0gr93tutwb8mhgoj1eod.png" alt="A gopher standing in front of a clean architectural blueprint on the wall, oblivious to the chaotic system drifting apart behind them." width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After we started checking ADRs in CI, we caught several violations like this in the first month and dozens more in the first quarter before they reached production.&lt;/p&gt;

&lt;p&gt;This is the difference between documenting architecture and operationalizing it. Documentation tells people what the decision was. Operationalization puts that decision in the path of delivery.&lt;/p&gt;

&lt;p&gt;This post is the system behind that: take an ADR, turn it into an automated check, and run it against every PR diff. I went this direction because I needed one approach that worked across Go, Python, Terraform, and Kubernetes without maintaining a separate rule engine for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Architecture Decisions Nobody Remembers
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnolis4gyzij2qkmu33ur.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnolis4gyzij2qkmu33ur.png" alt="A gopher walking past a dusty filing cabinet labeled ADRs with cobwebs and spilling papers, while a monitor in the background shows a merged PR." width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most architecture teams do good work. They write ADRs, join design reviews, publish standards, and explain trade-offs. But documents do not participate in delivery. A decision can be technically correct and still have zero enforcement once it leaves the meeting.&lt;/p&gt;

&lt;p&gt;At scale, that becomes a latency problem. You only discover drift after merge, during an incident, or when a migration turns into a quarter-long cleanup project. The issue is not that architects need more authority. The issue is that documentation does not stop drift. Running checks do.&lt;/p&gt;

&lt;p&gt;The fix is to move architecture into the same path as tests, policy, and build checks. Make the decision executable.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Quick Primer on Architecture Decision Records [ADR]
&lt;/h2&gt;

&lt;p&gt;If you already use Architectural Decision Records, this part will feel familiar. But the template matters here because bad ADRs are hard to operationalize. If the decision is vague, the automation will be vague too.&lt;/p&gt;

&lt;p&gt;I still like &lt;a href="https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions" rel="noopener noreferrer"&gt;Michael Nygard's original format&lt;/a&gt;, with a few practical tweaks, because it keeps the decision grounded in context instead of turning it into policy theater.&lt;/p&gt;

&lt;p&gt;Here is the template I use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Architecture Decision Record&lt;/span&gt;

Date: YYYY-MM-DD

Status: proposed | accepted | deprecated | superseded (add reference)

&lt;span class="gu"&gt;## Context&lt;/span&gt;

This section describes the forces at play, including technological, political,
social, and project local. These forces are probably in tension and should be
called out as such. The language in this section is value-neutral. It is simply
describing facts.

&lt;span class="gu"&gt;## Considered Alternatives&lt;/span&gt;

This section describes the alternatives evaluated together with the pros, cons,
and reasoning why to move or not to move with it.
&lt;span class="p"&gt;
*&lt;/span&gt; Alternative 1: [description, pros, cons, reasoning]
&lt;span class="p"&gt;*&lt;/span&gt; Alternative 2: [description, pros, cons, reasoning]

&lt;span class="gu"&gt;## Decision&lt;/span&gt;

This section describes our response to these forces. It is stated in full
sentences, with an active voice. "We will ..."

&lt;span class="gu"&gt;## Consequences&lt;/span&gt;

This section describes the resulting context, after applying the decision.
All consequences should be listed here, not just the "positive" ones. A
particular decision may have positive, negative, and neutral consequences,
but all of them affect the team and project in the future.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two sections that matter most for operationalization are &lt;strong&gt;Considered Alternatives&lt;/strong&gt; and &lt;strong&gt;Consequences&lt;/strong&gt;. The first tells future readers what you rejected. The second tells you what kind of drift to watch for. If those sections are vague, your fitness function will be vague too.&lt;/p&gt;

&lt;p&gt;This is the part most teams skip. The ADR is not the enforcement mechanism; it is the source material for one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context&lt;/strong&gt; tells you where the rule applies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decision&lt;/strong&gt; tells you what must or must not happen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consequences&lt;/strong&gt; tell you what kinds of drift, exceptions, and migration warnings the check should account for.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I cannot turn those sections into a concrete detection rule, the ADR is probably not specific enough yet.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!TIP]&lt;br&gt;
Store your ADRs in version control, not a wiki. Put them in a &lt;code&gt;/docs/adr&lt;/code&gt; folder in your main repo or a dedicated architecture repo. This gives you history, blame, and—critically—the ability to reference them from CI pipelines.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  From Documentation to Enforcement: Operationalizing ADRs
&lt;/h2&gt;

&lt;p&gt;So you've got a folder full of ADRs. Now what?&lt;/p&gt;

&lt;p&gt;This is where fitness functions come in. A fitness function is any automated check that verifies an architectural constraint — the term comes from &lt;em&gt;Building Evolutionary Architecture&lt;/em&gt;, but the concept is just "automated architecture tests." Some examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dependency fitness function:&lt;/strong&gt; Does &lt;code&gt;go.mod&lt;/code&gt; contain any database drivers other than our standard (PostgreSQL)?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layer fitness function:&lt;/strong&gt; Does the &lt;code&gt;domain&lt;/code&gt; package import from &lt;code&gt;infrastructure&lt;/code&gt;? (It shouldn't in hexagonal architecture.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security fitness function:&lt;/strong&gt; Are all external API calls using our approved HTTP client with circuit breakers?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key insight is that &lt;strong&gt;every ADR that isn't explicitly deprecated or superseded should map to at least one fitness function.&lt;/strong&gt; That is the operationalizing step. In practice, if you only check ADRs marked "accepted," you'll miss most of them — plenty of ADRs sit at "proposed" or have no status at all. The filter should be inclusive: check everything that hasn't been explicitly retired.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs5zp6nlz0u6ocgvo3ie7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs5zp6nlz0u6ocgvo3ie7.png" alt="Mermaid diagram 1" width="800" height="150"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Traditional fitness functions are written in code. In Go, you might use a tool like &lt;code&gt;go-arch-lint&lt;/code&gt; or write custom tests that inspect the AST. In Java, ArchUnit is the gold standard. In TypeScript, you might use dependency-cruiser.&lt;/p&gt;

&lt;p&gt;But here's the problem: &lt;strong&gt;most organizations are not monolingual.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's say you've decided to standardize on OpenSearch for log aggregation. That decision affects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your Go services (which might import a Datadog or Splunk client)&lt;/li&gt;
&lt;li&gt;Your Python scripts (which might use different logging libraries)&lt;/li&gt;
&lt;li&gt;Your Terraform modules (which might provision the wrong resources)&lt;/li&gt;
&lt;li&gt;Your Kubernetes manifests (which might include sidecars for the wrong vendor)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Writing a fitness function in each language sounds manageable until you count the surfaces: Go dependencies, Python imports, Terraform resources, Helm values, raw YAML, and environment variables. Half the drift is in configuration, not application code.&lt;/p&gt;

&lt;p&gt;That is why I started treating fitness functions as prompts instead of parsers. I already had an AI reviewer reading PR diffs through &lt;a href="https://dev.to/blog/building-archbot-ai-code-reviewer/"&gt;Archbot&lt;/a&gt;. Adding architecture checks meant adding more instructions, not shipping another language-specific rule engine.&lt;/p&gt;

&lt;p&gt;The idea is simple. Instead of writing code that parses each language's dependency file, you describe what a violation looks like and let an LLM analyze the diff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operationalizing the ADR: Two Approaches
&lt;/h2&gt;

&lt;p&gt;The flow is straightforward: start with an ADR, express the violation patterns so an LLM can check them, and run that check against the PR diff. If the model finds a violation, return a structured comment.&lt;/p&gt;

&lt;p&gt;There are two ways to do this, and I've tried both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach 1: Separate Fitness Function Prompts
&lt;/h3&gt;

&lt;p&gt;The more structured option: for each ADR, you write a corresponding "fitness function prompt" — a standalone markdown file that describes what constitutes a violation and how to identify it. When a PR comes in, the AI analyzes the diff against all active prompts and flags any violations.&lt;/p&gt;

&lt;p&gt;This works well when you want fine-grained control over detection rules, or when the people writing fitness functions are different from the people writing ADRs.&lt;/p&gt;

&lt;p&gt;Let me show you a complete example.&lt;/p&gt;

&lt;h4&gt;
  
  
  The ADR
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# ADR-042: Standardize on OpenSearch for Log Aggregation&lt;/span&gt;

Date: 2025-09-15

Status: accepted

&lt;span class="gu"&gt;## Context&lt;/span&gt;

We currently have three different log aggregation solutions in production:
&lt;span class="p"&gt;-&lt;/span&gt; Datadog (legacy, from acquisition)
&lt;span class="p"&gt;-&lt;/span&gt; Splunk (used by compliance team)
&lt;span class="p"&gt;-&lt;/span&gt; OpenSearch (adopted by new services)

This fragmentation causes operational overhead (three dashboards, three query
languages, three billing relationships) and makes cross-service debugging
difficult. Engineers waste time context-switching between tools.

&lt;span class="gu"&gt;## Considered Alternatives&lt;/span&gt;
&lt;span class="p"&gt;
*&lt;/span&gt; &lt;span class="gs"&gt;**Keep multi-vendor approach:**&lt;/span&gt; Pros: no migration effort, teams keep familiar
  tools. Cons: ongoing operational overhead, cross-service debugging remains
  painful, costs continue to grow.
&lt;span class="p"&gt;
*&lt;/span&gt; &lt;span class="gs"&gt;**Standardize on Datadog:**&lt;/span&gt; Pros: strong APM integration, existing team
  familiarity. Cons: highest cost option, vendor lock-in concerns, no
  self-hosted option.
&lt;span class="p"&gt;
*&lt;/span&gt; &lt;span class="gs"&gt;**Standardize on OpenSearch:**&lt;/span&gt; Pros: open-source, self-hosted option available,
  good cost/feature balance, already adopted by new services. Cons: migration
  effort required, less mature APM story.

&lt;span class="gu"&gt;## Decision&lt;/span&gt;

We will standardize on OpenSearch for log aggregation. Specifically:
&lt;span class="p"&gt;-&lt;/span&gt; Application logs ship to OpenSearch via Fluent Bit sidecars
&lt;span class="p"&gt;-&lt;/span&gt; No direct integration with Datadog, Splunk, or other log vendors in
  application code
&lt;span class="p"&gt;-&lt;/span&gt; Existing Datadog/Splunk integrations should be migrated by Q2 2026

&lt;span class="gu"&gt;## Consequences&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Single pane of glass for log queries (positive)
&lt;span class="p"&gt;-&lt;/span&gt; Reduced vendor costs, ~40% savings projected (positive)
&lt;span class="p"&gt;-&lt;/span&gt; Consistent logging patterns across all services (positive)
&lt;span class="p"&gt;-&lt;/span&gt; Migration effort required for legacy services (negative)
&lt;span class="p"&gt;-&lt;/span&gt; Team retraining on OpenSearch query syntax (negative)
&lt;span class="p"&gt;-&lt;/span&gt; Some Datadog-specific features like APM correlation need alternatives (negative)
&lt;span class="p"&gt;-&lt;/span&gt; Compliance team will need updated runbooks for OpenSearch (neutral)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  The Fitness Function Prompt
&lt;/h4&gt;

&lt;p&gt;Now here's the corresponding fitness function, written as a prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Fitness Function: ADR-042 OpenSearch Standardization&lt;/span&gt;

&lt;span class="gs"&gt;**ADR Reference:**&lt;/span&gt; ADR-042
&lt;span class="gs"&gt;**Enforcement:**&lt;/span&gt; Block new integrations; warn on modifications to legacy

&lt;span class="gu"&gt;### What to Check&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Direct Datadog/Splunk SDK imports or dependencies (Go, Python, Node, Terraform)
&lt;span class="p"&gt;-&lt;/span&gt; Vendor-specific logging configuration (DD_&lt;span class="ge"&gt;*, SPLUNK_*&lt;/span&gt; env vars)
&lt;span class="p"&gt;-&lt;/span&gt; Sidecar configurations for non-approved vendors

&lt;span class="gu"&gt;### How to Respond&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; NEW integrations: Flag as BLOCKING with ADR reference and migration guide link
&lt;span class="p"&gt;-&lt;/span&gt; MODIFICATIONS to existing: Flag as WARNING with migration deadline

&lt;span class="gu"&gt;### What to Ignore&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Documentation, test fixtures, removal of vendor code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get the idea. Each ADR gets a prompt that describes what to look for, how to respond, and what to ignore. You can tune the prompt for each ADR independently.&lt;/p&gt;

&lt;p&gt;But after running this in production, I found a simpler way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach 2: Let the Agent Read ADRs Directly
&lt;/h3&gt;

&lt;p&gt;Here's what I discovered after building the system: &lt;strong&gt;you don't need separate fitness function files at all.&lt;/strong&gt; The ADR itself — specifically the Decision and Consequences sections — already contains everything the agent needs to check compliance.&lt;/p&gt;

&lt;p&gt;Think about what a well-written ADR tells you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decision&lt;/strong&gt; says "We will use X" and "No direct integration with Y" — that's your violation definition right there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consequences&lt;/strong&gt; describes what drift looks like — "migration effort required for legacy services" tells the agent what existing code might look like versus what new code should look like.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you write a separate fitness function file, you're translating the ADR into a second document. That creates a sync problem: the ADR gets updated, but the fitness function prompt doesn't. Or the ADR gets nuanced ("except for the compliance team's audit logs"), but the prompt still flags those cases.&lt;/p&gt;

&lt;p&gt;The simpler approach: give the agent tools to read ADRs directly, and let it figure out what's relevant.&lt;/p&gt;

&lt;p&gt;The agent gets two tools instead of a library of prompt files:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;list_adrs&lt;/code&gt;&lt;/strong&gt; — returns a summary table for triage:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ID&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Summary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ADR-042&lt;/td&gt;
&lt;td&gt;OpenSearch for Log Aggregation&lt;/td&gt;
&lt;td&gt;accepted&lt;/td&gt;
&lt;td&gt;Mandates OpenSearch, forbids Datadog/Splunk integrations...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ADR-043&lt;/td&gt;
&lt;td&gt;API Versioning Standard&lt;/td&gt;
&lt;td&gt;proposed&lt;/td&gt;
&lt;td&gt;Requires URL path versioning for all public APIs...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;read_adr&lt;/code&gt;&lt;/strong&gt; — fetches the full content of a specific ADR by file path.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The system prompt instructs the agent to call &lt;code&gt;list_adrs&lt;/code&gt;, identify which ADRs are relevant to the PR's changes based on summaries, then call &lt;code&gt;read_adr&lt;/code&gt; for each relevant one. It checks the diff against each ADR's Decision and Consequences sections and reports violations with specific quotes.&lt;/p&gt;

&lt;p&gt;The payoff is even bigger than the separate-files approach: adding a new architectural check means writing an ADR. That's it. The people writing ADRs don't need to write a second file, learn a prompt format, or coordinate with the platform team. The agent picks it up automatically.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Kind of Diff It Catches
&lt;/h4&gt;

&lt;p&gt;Here is the sort of change this catches without writing a Node-specific rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/package.json b/package.json
&lt;/span&gt;&lt;span class="p"&gt;@@ -18,6 +18,7 @@&lt;/span&gt;
   "dependencies": {
&lt;span class="gi"&gt;+    "dd-trace": "^5.4.0",
&lt;/span&gt;     "pino": "^9.7.0"
   }
 }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is enough context for the model to flag a new vendor-specific logging dependency and tie it back to ADR-042.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;BLOCKING:&lt;/strong&gt; This PR introduces a new Datadog integration. Per ADR-042, we've standardized on OpenSearch for log aggregation. Please use Fluent Bit sidecars instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  How it works in practice
&lt;/h4&gt;

&lt;p&gt;One production choice matters here: decide whether model failures should fail open or fail closed. For something like ADR-042, fail open makes sense — a missed Datadog import is annoying but not dangerous. For a security ADR banning unencrypted secrets in config, you probably want fail-closed. Match the failure mode to the cost of drift for the decision you're enforcing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgaojarhy60ktxjoa0mx9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgaojarhy60ktxjoa0mx9.png" alt="Mermaid diagram 2" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Lessons: What I Learned After Shipping This
&lt;/h2&gt;

&lt;p&gt;The conceptual model above is clean. Production was messier. Here are the three lessons that would have saved me the most time if someone had told me upfront.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why LLMs Excuse Architecture Violations
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv8e1qf4kxmnum5yk4gs9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv8e1qf4kxmnum5yk4gs9.png" alt="A gopher in a lab coat dismissing a clear violation on screen with a thought bubble reading 'Probably fine' while ADR papers sit ignored on the desk." width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This one surprised me. The agent saw a clear ADR mandating a specific SDK for Go services. The codebase used a different SDK. The agent's reasoning? "This codebase predates the ADR and might be an exception."&lt;/p&gt;

&lt;p&gt;It dismissed the violation without flagging it.&lt;/p&gt;

&lt;p&gt;LLMs are trained to be helpful, and "helpful" often means finding reasons why something is probably fine. That is exactly the wrong behavior for compliance checking.&lt;/p&gt;

&lt;p&gt;The fix: explicit anti-rationalization rules in the system prompt.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Report what you see. Do NOT rationalize away violations.

If an ADR mandates technology X and the code uses technology Y, that is a
violation — even if you believe the codebase predates the ADR, might have an
exception, or has a reasonable justification.

Your job is to flag deviations from the ADR text, not to judge whether
exceptions apply. Humans will decide if an exception is warranted.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a universal lesson for anyone using LLMs for compliance, policy enforcement, or audit. If the model can find a reason to say "looks fine," it will. You have to explicitly tell it not to.&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary-First Triage Is Required at Scale
&lt;/h3&gt;

&lt;p&gt;I mentioned this above, but it deserves emphasis because the failure mode is invisible. With a handful of ADRs, you can dump everything into context and it works great. At 20+ ADRs, the API starts timing out or the model loses focus in the middle of a massive context window.&lt;/p&gt;

&lt;p&gt;The two-step workflow — summaries for triage, full reads for relevant ADRs — is not an optimization. It's a requirement. Plan for it from the start if you have more than 10 ADRs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompt Iteration Is the Real Work
&lt;/h3&gt;

&lt;p&gt;Here's a timeline from my production rollout:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Iteration 1:&lt;/strong&gt; Instructions sounded optional ("you may want to check ADRs"). The agent skipped them entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iteration 2:&lt;/strong&gt; Made instructions mandatory. Agent read all ADR content at once. API timeout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iteration 3:&lt;/strong&gt; Added summary-based triage. Agent read summaries but never called the tool to read full content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iteration 4:&lt;/strong&gt; Fixed tool instructions. Agent read full content but rationalized away violations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iteration 5:&lt;/strong&gt; Added anti-rationalization rules. Agent started flagging violations but was too aggressive — flagging test fixtures and migration documentation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iteration 6:&lt;/strong&gt; Added "What to Ignore" guidance. Getting closer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iteration 7+:&lt;/strong&gt; Tuned severity levels and response formatting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Seven iterations. Each one taught me something about how LLMs interpret instructions.&lt;/p&gt;

&lt;p&gt;The biggest takeaway: &lt;strong&gt;if an instruction sounds optional, the agent will skip it.&lt;/strong&gt; Don't say "you may want to check." Say "You MUST call &lt;code&gt;read_adr&lt;/code&gt; for EACH potentially relevant ADR." Mandatory language is not rude — it's how you get consistent behavior from an LLM.&lt;/p&gt;

&lt;p&gt;Plan for at least five iterations on your prompt. Test against real PRs, not synthetic examples. The edge cases in real diffs will teach you things you can't anticipate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoffs: AI vs Static Analysis
&lt;/h2&gt;

&lt;p&gt;Let me be direct about the trade-offs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traditional code-based fitness functions&lt;/strong&gt; give you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deterministic results (same input = same output)&lt;/li&gt;
&lt;li&gt;Fast execution (no API calls)&lt;/li&gt;
&lt;li&gt;No token costs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AI-based fitness functions&lt;/strong&gt; give you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cross-language support with one implementation&lt;/li&gt;
&lt;li&gt;Natural language rules anyone can write&lt;/li&gt;
&lt;li&gt;Fuzzy matching (catches variations you didn't anticipate)&lt;/li&gt;
&lt;li&gt;Contextual understanding (knows the difference between adding and removing a dependency)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most organizations, especially those with polyglot stacks, the AI approach wins on maintainability. The cost? You're trading determinism for flexibility. Sometimes the LLM will miss something. Sometimes it'll flag a false positive.&lt;/p&gt;

&lt;p&gt;But here's the thing: manual code review has the same problems. Reviewers miss things. Reviewers have false positives ("I don't like this pattern" when it's actually fine). At least with AI fitness functions, the rules are written down and consistently applied.&lt;/p&gt;

&lt;p&gt;That said, AI fitness functions are not a replacement for linters, type systems, or targeted static analysis. I still reach for traditional code-based checks when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The rule must be deterministic and always blocking&lt;/li&gt;
&lt;li&gt;The violation is semantic rather than structural&lt;/li&gt;
&lt;li&gt;A false positive would be expensive or noisy&lt;/li&gt;
&lt;li&gt;The check needs to run with near-zero latency and no external dependency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sweet spot for AI is cross-language policy enforcement and structural drift. The sweet spot for static analysis is precise, language-local invariants. In practice, the best system uses both.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Roll This Out Without Overengineering It
&lt;/h2&gt;

&lt;p&gt;If you want to try this approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Audit your ADRs.&lt;/strong&gt; Which ones aren't deprecated or superseded? List them.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;[!TIP]&lt;br&gt;
Don't filter by "accepted" only — in most repos, half the ADRs that matter are still marked "proposed" or have no status at all. Be inclusive.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Classify by verifiability.&lt;/strong&gt; For each ADR, ask: "Can I describe what a violation looks like?" If yes, it's a candidate for automation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with one high-value check.&lt;/strong&gt; Pick an ADR that gets violated frequently. If you're using the agent-reads-ADRs approach, make sure the Decision section is specific enough. If you're writing separate prompts, write the first one. Hook it into your review pipeline.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plan for iteration.&lt;/strong&gt; Your first prompt will be wrong. The agent will skip checks, timeout on too much context, or rationalize away violations. Budget for at least five iterations against real PRs. This is the actual work.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Add anti-rationalization rules early.&lt;/strong&gt; Don't wait until you see the agent excusing violations. Add explicit "report, don't judge" instructions from the start.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Build the library.&lt;/strong&gt; Once you have a system checking 5-10 ADRs, the flywheel kicks in. New ADRs automatically get checked. Engineers start writing better ADRs because they know the Decision section will actually be enforced.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Start with the ADR that already creates expensive cleanup when it gets ignored. Do not start with your most abstract principle. Start with the one that repeatedly costs your team time.&lt;/p&gt;

&lt;p&gt;The Datadog PR from the opening was not a one-off. It was what happens when important decisions live in documentation and delivery lives somewhere else.&lt;/p&gt;

&lt;p&gt;Once we put the ADR in the review path, the feedback loop collapsed from months to minutes. In the first month, it caught several violations we would have otherwise discovered later. By the end of the first quarter, it had caught dozens more across repositories and file types that no single linter could have covered.&lt;/p&gt;

&lt;p&gt;Operationalizing architecture is not magic. It is just architecture that finally made it into code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;This is part 1 of a three-part series on operationalizing architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Part 1:&lt;/strong&gt; Operationalizing ADRs with Automated Fitness Functions (you are here)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 2:&lt;/strong&gt; Spotting the Big Changes: Automating Impactful PR Detection for Architects &lt;em&gt;(coming soon)&lt;/em&gt; — Not all PRs are equal. Learn how to automatically flag the ones that need architect review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 3:&lt;/strong&gt; Navigating Tech Choices: How to Turn Your Tech Radar into an Active Architecture Guide &lt;em&gt;(coming soon)&lt;/em&gt; — Your Tech Radar shouldn't be a PDF. Make it a dynamic fitness function.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm spending more time building architecture systems that turn standards into working guardrails. If that's the kind of platform problem you're working on too, you can grab time here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cal.com/alexandrecastrotech/mentoring" rel="noopener noreferrer"&gt;&lt;br&gt;
     Book a mentoring session&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>automation</category>
      <category>cicd</category>
      <category>devops</category>
    </item>
    <item>
      <title>Agentic AI Code Review: From Confidently Wrong to Evidence-Based</title>
      <dc:creator>Alexandre Amado de Castro</dc:creator>
      <pubDate>Sun, 15 Mar 2026 17:39:43 +0000</pubDate>
      <link>https://forem.com/alexandreamadocastro/agentic-ai-code-review-from-confidently-wrong-to-evidence-based-pne</link>
      <guid>https://forem.com/alexandreamadocastro/agentic-ai-code-review-from-confidently-wrong-to-evidence-based-pne</guid>
      <description>&lt;p&gt;✅ Links fixed!&lt;br&gt;
Archbot flagged a "blocker" on a PR. It cited the diff, built a plausible chain of reasoning, and suggested a fix.&lt;/p&gt;

&lt;p&gt;It was completely wrong. Not "LLMs are sometimes wrong" wrong — more like &lt;em&gt;convincing enough that a senior engineer spent 20 minutes disproving it&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The missing detail wasn't subtle. It was a guard clause sitting in a helper two files away.&lt;br&gt;
Archbot just didn't have that file.&lt;/p&gt;

&lt;p&gt;That failure mode wasn't a prompt problem.&lt;br&gt;
It was a &lt;strong&gt;context problem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So I stopped trying to predict what context the model would need up-front, and switched to an agentic loop: give the model tools to fetch evidence as it goes, and require it to end with a structured "submit review" action.&lt;/p&gt;

&lt;p&gt;This post is the architectural why and how (and the reliability plumbing that made it work). There are good hosted AI review tools now. This post is about the pattern underneath them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!NOTE]&lt;br&gt;
Names, repos, and examples are intentionally generalized. This is about the design patterns, not a particular company.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you want the backstory on what Archbot is and why I built it, start with the original post: &lt;a href="https://platformtoolsmith.com/blog/building-archbot-ai-code-reviewer/" rel="noopener noreferrer"&gt;Building Archbot: AI Code Review for GitHub Enterprise&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fixed pipeline (and where it breaks)
&lt;/h2&gt;

&lt;p&gt;My original design was what a lot of "LLM workflow" systems converge to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Give the model the PR diff.&lt;/li&gt;
&lt;li&gt;Give it some representation of the repo.&lt;/li&gt;
&lt;li&gt;Ask it to pick the files that matter.&lt;/li&gt;
&lt;li&gt;Feed only those files back in for the real review.&lt;/li&gt;
&lt;li&gt;Optionally run a second pass to critique the first pass.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That critique pass is helpful for catching obvious nonsense and reducing overconfident tone.&lt;br&gt;
But it can't solve the core issue: if both passes are reasoning over the same clipped context, they're both still guessing.&lt;/p&gt;

&lt;p&gt;It looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsx4974wh03no3cuxazs7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsx4974wh03no3cuxazs7.png" alt="Agentic Pipeline Flowchart" width="276" height="638"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On paper, it's elegant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The model is great at prioritizing.&lt;/li&gt;
&lt;li&gt;Keeping context small should reduce hallucinations.&lt;/li&gt;
&lt;li&gt;A critique pass should catch the worst bad takes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, it fails in a very specific way.&lt;/p&gt;
&lt;h2&gt;
  
  
  The core failure: you can't pre-select the missing piece
&lt;/h2&gt;

&lt;p&gt;Code review isn't "read these files".&lt;br&gt;
It's "follow the chain of evidence until you understand the behavior".&lt;/p&gt;

&lt;p&gt;And chains are not predictable.&lt;/p&gt;

&lt;p&gt;You start in &lt;code&gt;handler.go&lt;/code&gt;, notice it calls &lt;code&gt;validate()&lt;/code&gt;, jump to &lt;code&gt;validate.go&lt;/code&gt;, see it wraps a client, jump to &lt;code&gt;client.go&lt;/code&gt;, and only then realize the bug is a timeout default in &lt;code&gt;config/defaults.go&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fixed pipeline made that exploration impossible.&lt;/p&gt;

&lt;p&gt;Once Phase 1 picked the "important" files, Phase 2 was stuck. If the model realized it needed one more file to confirm a claim, it had no way to fetch it.&lt;/p&gt;

&lt;p&gt;That led to two bad outcomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;False positives with confidence.&lt;/strong&gt; The model would infer behavior from a partial call chain and present it as fact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missed risk.&lt;/strong&gt; The model would never see the one file that made a change dangerous, because that file didn't look important from the diff.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I could tune prompts.&lt;br&gt;
I could add more heuristics to file selection.&lt;br&gt;
I could increase the file budget.&lt;/p&gt;

&lt;p&gt;But that's all the same bet: that I can guess the right context ahead of time.&lt;/p&gt;

&lt;p&gt;That bet is wrong more often than it feels.&lt;/p&gt;
&lt;h2&gt;
  
  
  The shift: stop guessing context, let the model fetch it
&lt;/h2&gt;

&lt;p&gt;The agentic version flips the contract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The system does &lt;em&gt;not&lt;/em&gt; attempt to build a perfect context.&lt;/li&gt;
&lt;li&gt;It gives the model a toolset for finding evidence.&lt;/li&gt;
&lt;li&gt;It loops until the model either submits a review (terminal tool) or hits a budget/timeout.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of a fixed pipeline, it's an exploration loop:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F21s73eyv58ag1andlwm9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F21s73eyv58ag1andlwm9.png" alt="Agentic Loop Sequence Diagram" width="751" height="759"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can summarize the new rule as:&lt;/p&gt;

&lt;p&gt;"If you can't cite it, go fetch it."&lt;/p&gt;

&lt;p&gt;Under the hood, this ended up being three pieces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A loop that alternates between "model turn" and "tool turn", with budgets and context hygiene.&lt;/li&gt;
&lt;li&gt;A tool interface that can mark certain tools as terminal.&lt;/li&gt;
&lt;li&gt;Different toolsets per mode (full review vs chat vs command), reusing the same loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The biggest reliability improvement wasn't "more tools".&lt;br&gt;
It was making the model end by calling a &lt;strong&gt;terminal tool&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In a fixed prompt pipeline, the model ends by printing Markdown.&lt;br&gt;
That makes it tempting to optimize for &lt;em&gt;sounding right&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;In an agentic loop, the model ends by &lt;em&gt;submitting an object&lt;/em&gt;.&lt;br&gt;
That changes the incentives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's harder to hand-wave; you have to populate fields.&lt;/li&gt;
&lt;li&gt;You can enforce structure (severity, inline comments, evidence links).&lt;/li&gt;
&lt;li&gt;The loop can treat "no submission" as failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So what does the terminal tool actually look like? And how do you wire all this together without it becoming a mess? Let's build it.&lt;/p&gt;
&lt;h2&gt;
  
  
  What changed in review quality
&lt;/h2&gt;

&lt;p&gt;The qualitative shift after going agentic wasn't "more comments".&lt;br&gt;
It was &lt;strong&gt;more &lt;em&gt;explainable&lt;/em&gt; comments&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The best code review feedback has three properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It points at a specific behavior.&lt;/li&gt;
&lt;li&gt;It cites the relevant code.&lt;/li&gt;
&lt;li&gt;It proposes a fix (or at least a direction) with trade-offs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tools make that possible.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;p&gt;"This might break retries."&lt;/p&gt;

&lt;p&gt;You get:&lt;/p&gt;

&lt;p&gt;"In &lt;code&gt;foo/bar.go:123&lt;/code&gt;, the new call bypasses &lt;code&gt;withRetry(...)&lt;/code&gt;. All other call sites use &lt;code&gt;withRetry(...)&lt;/code&gt; (see matches in &lt;code&gt;search_code&lt;/code&gt;). If that's intentional, we should document why; otherwise, wrap it."&lt;/p&gt;

&lt;p&gt;Here's what that looks like in practice:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplatformtoolsmith.com%2Fassets%2Fblog%2Farchbot-blocker-example.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplatformtoolsmith.com%2Fassets%2Fblog%2Farchbot-blocker-example.png" alt="Archbot code review showing a 🔴 Blocker finding about SQL driver registration with specific file reference, detailed explanation of the panic risk, and a suggested fix" width="800" height="1157"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the model has the ability (and expectation) to fetch evidence, it stops guessing.&lt;/p&gt;

&lt;p&gt;The way I measure that shift: does it cite exact locations, or hedge? Does it go fetch more evidence when uncertain, or just keep talking? How often do I have to spend time &lt;em&gt;disproving&lt;/em&gt; a comment?&lt;/p&gt;

&lt;p&gt;In other words: does it behave like a cautious reviewer, or a persuasive one?&lt;/p&gt;
&lt;h2&gt;
  
  
  Build it yourself: the pieces you need
&lt;/h2&gt;

&lt;p&gt;Here's how the pieces fit together — the tool interface, a few representative tools, the terminal tool, and how they wire into the loop.&lt;/p&gt;

&lt;p&gt;The examples below are simplified Go to show the minimum shape. They're not production code, but they're structurally complete — you could extend these into a working system.&lt;/p&gt;

&lt;p&gt;I didn't rebuild Archbot from scratch to go agentic. I took the existing review pipeline and wrapped it in a small loop: model turn → tool turn → repeat, plus a single terminal action (&lt;code&gt;submit_code_review&lt;/code&gt;) to end the run. A hand-rolled loop is the fastest way to prove the product behavior (does it fetch evidence? does it stop guessing?) before you commit to a framework.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!TIP]&lt;br&gt;
Starting greenfield? &lt;a href="https://google.github.io/adk-docs/" rel="noopener noreferrer"&gt;Google's ADK&lt;/a&gt; is a solid default — it provides a similar tool interface with built-in orchestration, tracing, and callback hooks. The patterns below (tool contracts, terminal actions, structured output, context hygiene) apply either way.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Step 1: Define the tool contract
&lt;/h3&gt;

&lt;p&gt;Every tool implements the same contract. The key method is &lt;code&gt;IsTerminal()&lt;/code&gt; — it's what lets the loop know when to stop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Tool&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;InputSchema&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;IsTerminal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Build your context-fetching tools
&lt;/h3&gt;

&lt;p&gt;Most tools follow the same pattern: parse input, call an API, return a string. Keep them boring and deterministic.&lt;/p&gt;

&lt;p&gt;One thing I didn't appreciate early: these tools are part of your product surface area. Treat them like APIs — stable contracts, deterministic outputs, and tests that lock in behavior. If &lt;code&gt;search_code&lt;/code&gt; returns garbage, the model will reason over garbage. The shortest path from "I suspect X" to "here's the evidence" should be one tool call.&lt;/p&gt;

&lt;p&gt;Here's &lt;code&gt;get_file_content&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;GetFileContent&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ghClient&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;
    &lt;span class="n"&gt;owner&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;repo&lt;/span&gt;     &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;headSHA&lt;/span&gt;  &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;GetFileContent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"get_file_content"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;GetFileContent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;IsTerminal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;     &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;GetFileContent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"Fetch the full content of a file at the PR's head commit."&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;GetFileContent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;InputSchema&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;
        &lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"properties"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;
            &lt;span class="s"&gt;"path"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;
                &lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"description"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"File path relative to the repo root"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="s"&gt;"required"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;GetFileContent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"path is required"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ghClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetFileContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headSHA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"fetching %s: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;addLineNumbers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="c"&gt;// prefix each line with its number for precise citations&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's &lt;code&gt;search_code&lt;/code&gt; — the tool the model reaches for when it sees a function call in a diff and wants to know "where else is this used?"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;SearchCode&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;repoFiles&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="c"&gt;// path -&amp;gt; content (from repomix or local clone)&lt;/span&gt;
    &lt;span class="n"&gt;diffText&lt;/span&gt;  &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SearchCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"search_code"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SearchCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;IsTerminal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SearchCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"query is required"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;queryLower&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToLower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;matches&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;

    &lt;span class="c"&gt;// search repo files (production version uses ripgrep or tree-sitter for precision)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repoFiles&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToLower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;queryLower&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%s:%d: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// search the PR diff too&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;diffText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToLower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;queryLower&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"(diff):%d: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"No matches found for %q"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Define the terminal tool
&lt;/h3&gt;

&lt;p&gt;This is what makes the loop an &lt;em&gt;agent&lt;/em&gt; instead of a chatbot. The model doesn't print freeform text — it calls &lt;code&gt;submit_code_review&lt;/code&gt; with structured data, and the loop ends.&lt;/p&gt;

&lt;p&gt;One subtle but important implementation detail: treat the terminal tool as &lt;strong&gt;non-executable&lt;/strong&gt;. When the model emits &lt;code&gt;submit_code_review&lt;/code&gt;, the loop captures the structured payload and ends immediately. Your application code (outside the loop) turns that payload into an actual GitHub review.&lt;/p&gt;

&lt;p&gt;That also gives you a schema you can validate. Here's what the model submits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What changed + why it matters"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inline_comments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"foo/bar.go"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"line"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"blocker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"comment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Specific issue, evidence, and a suggested fix"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"high_level_feedback"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Design-level note that isn't tied to a single line"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the model puts "blocker" in the severity field but can't provide a path + line, that's the model telling you it doesn't have evidence.&lt;/p&gt;

&lt;p&gt;Here's the implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;SubmitCodeReview&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SubmitCodeReview&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"submit_code_review"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SubmitCodeReview&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;IsTerminal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SubmitCodeReview&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;`Submit your final code review. Every blocker and should-fix MUST
include a file path and line number. If you cannot cite evidence, downgrade
to nice-to-have or omit the finding.`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SubmitCodeReview&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;InputSchema&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;
        &lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"properties"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;
            &lt;span class="s"&gt;"summary"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;            &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="s"&gt;"inline_comments"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;
                &lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"items"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;
                    &lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s"&gt;"properties"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;
                        &lt;span class="s"&gt;"path"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                        &lt;span class="s"&gt;"line"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                        &lt;span class="s"&gt;"severity"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"enum"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"blocker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"should-fix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"nice-to-have"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
                        &lt;span class="s"&gt;"comment"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="s"&gt;"required"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"line"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"comment"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="s"&gt;"high_level_feedback"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;
                &lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"items"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="s"&gt;"required"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"inline_comments"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SubmitCodeReview&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// In practice, the loop intercepts this before Execute runs.&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MarshalIndent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"  "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Write the loop
&lt;/h3&gt;

&lt;p&gt;This is the core of the agent. Conceptually, it's tiny:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Simplified: the real code has retries, token accounting, and timeouts.&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;iteration&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;iteration&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;maxIterations&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;iteration&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Converse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasToolCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"submit_code_review"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToolInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"submit_code_review"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;toolMessages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;executeTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToolCalls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toolMessages&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shrinkStaleToolResults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxToolResultChars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"agent loop ended without submit_code_review"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;shrinkStaleToolResults&lt;/code&gt; line is doing more work than it looks. The loop manages context aggressively:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It keeps the diff and the most recent evidence.&lt;/li&gt;
&lt;li&gt;It truncates older tool results (the giant file dumps you needed 3 turns ago).&lt;/li&gt;
&lt;li&gt;If the model hits a context overflow anyway, it retries the same iteration after shrinking tool results harder.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why bother with all this shrinking? "Why not just stuff the entire repo into the prompt and avoid tool calls?" Because &lt;strong&gt;long context has its own failure modes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Even if your model &lt;em&gt;can&lt;/em&gt; accept huge inputs, performance doesn't scale linearly with tokens. Two patterns show up in research and in practice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"Lost in the middle."&lt;/strong&gt; Models tend to over-weight the beginning and end of long contexts, and under-use relevant info buried in the middle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distraction.&lt;/strong&gt; Irrelevant context measurably reduces accuracy; the model starts pattern-matching on junk.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to go deep on the evidence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://aclanthology.org/2024.tacl-1.9.pdf" rel="noopener noreferrer"&gt;"Lost in the Middle: How Language Models Use Long Contexts"&lt;/a&gt; (Liu et al., 2024)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/pdf/2302.00093" rel="noopener noreferrer"&gt;"Large Language Models Can Be Easily Distracted by Irrelevant Context"&lt;/a&gt; (Shi et al., 2023)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://research.trychroma.com/context-rot" rel="noopener noreferrer"&gt;"Context Rot: How Increasing Input Tokens Impacts LLM Performance"&lt;/a&gt; (Chroma Research, 2025)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical takeaway isn't "never use long context". It's this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat tokens like budget, not storage.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is boring plumbing. It is also the difference between a system that works on small PRs and a system that survives the messy ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Wire it together
&lt;/h3&gt;

&lt;p&gt;This is the entry point. You assemble your tools, set your budgets, run the loop, and handle the structured output.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;RunReview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt; &lt;span class="n"&gt;PRContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ReviewResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;GetPRInfo&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;GetPRDiff&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;GetFileContent&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ghClient&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headSHA&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HeadSHA&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;SearchCode&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;repoFiles&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RepoFiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;diffText&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Diff&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;SubmitCodeReview&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;LoopConfig&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;SystemPrompt&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reviewSystemPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;InitialMessage&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"Review PR #%d: %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Author: %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Base: %s ← Head: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Head&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;Tools&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;              &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;MaxIterations&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="m"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;MaxToolResultChars&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;120&lt;/span&gt;&lt;span class="n"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;RunLoop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TerminalToolName&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"submit_code_review"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"loop ended without submitting a review"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;parseReviewResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TerminalToolInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tools are simple. The loop is simple. The power comes from letting the model decide which tools to call, and in what order.&lt;/p&gt;

&lt;p&gt;That terminal tool pattern also gives you a clean way to run the same loop in different modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full PR review: includes &lt;code&gt;submit_code_review&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Interactive chat: no terminal tool; the loop returns assistant text.&lt;/li&gt;
&lt;li&gt;Command mode: terminal tool is "submit result" for that command.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One loop. Different endings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Add guardrails
&lt;/h3&gt;

&lt;p&gt;Agentic doesn't mean "let it run forever". It means you give the model freedom &lt;em&gt;inside a box&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Iteration caps.&lt;/strong&gt; Hard limit on round-trips.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeouts.&lt;/strong&gt; Total wall-clock budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool result limits.&lt;/strong&gt; Max characters per tool output, and stricter limits for stale outputs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-critique before submission.&lt;/strong&gt; The terminal tool instructions include a checklist: cite evidence, downgrade uncertain claims, avoid bikeshedding, don't invent runtime failures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building your own version, don't copy my numbers. Copy the pattern: budgets are part of the interface.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Evaluate on real PRs
&lt;/h3&gt;

&lt;p&gt;Don't trust vibes. Pick 5-10 PRs where you already know what the real risks were — a missed nil check, a broken migration, a security issue someone caught in manual review. Run your loop against those PRs and check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did it find the real issue?&lt;/li&gt;
&lt;li&gt;Did it cite the right file and line?&lt;/li&gt;
&lt;li&gt;Did it hallucinate problems that don't exist?&lt;/li&gt;
&lt;li&gt;When it was uncertain, did it fetch more evidence or just assert?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives you a ground-truth baseline. From there, iterate on your tools (not your prompts).&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-offs (because there are always trade-offs)
&lt;/h2&gt;

&lt;p&gt;Agentic review is not a free lunch.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency:&lt;/strong&gt; tool calls add round-trips. You have to make tools fast and cacheable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; more turns can mean more tokens. Budgeting is mandatory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool design:&lt;/strong&gt; bad tools produce bad behavior. (If &lt;code&gt;search_code&lt;/code&gt; returns garbage, the model will reason over garbage.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security:&lt;/strong&gt; tools are an exfiltration surface. Your tool layer needs authorization and redaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But for code review, the win is that the model starts behaving like a cautious reviewer: it looks things up.&lt;/p&gt;

&lt;p&gt;Don't build a bigger prompt. Build a loop where the model can fetch evidence, update its hypothesis, and only finish when it submits a structured, checkable result. When you stop forcing the model to guess context, you stop debugging prompt vibes and start debugging actual interfaces.&lt;/p&gt;

&lt;p&gt;Want to build your own agentic review system? Let's talk through the architecture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cal.com/alexandrecastrotech/mentoring" rel="noopener noreferrer"&gt;&lt;br&gt;
     Book a mentoring session&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>architecture</category>
      <category>codereview</category>
    </item>
    <item>
      <title>Challenging Assumptions in Technology: From Being Right to Getting It Right</title>
      <dc:creator>Alexandre Amado de Castro</dc:creator>
      <pubDate>Thu, 12 Feb 2026 02:34:30 +0000</pubDate>
      <link>https://forem.com/alexandreamadocastro/challenging-assumptions-in-technology-from-being-right-to-getting-it-right-43gk</link>
      <guid>https://forem.com/alexandreamadocastro/challenging-assumptions-in-technology-from-being-right-to-getting-it-right-43gk</guid>
      <description>&lt;p&gt;Here's a story I tell my mentees often—and it's one where I almost made an expensive mistake.&lt;/p&gt;

&lt;p&gt;A few years ago, my team was tasked with rebuilding the authentication system for one of our core products. We walked into that room with a massive assumption already locked and loaded: &lt;em&gt;"Authentication isn't our core business. We should buy, not build."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It made perfect sense on paper. We assumed buying a vendor solution like Auth0 would be faster, safer, and cheaper. We assumed building it ourselves was risky—authentication is hard, right? If you mess it up, you leak credentials, you destroy trust, you crater the business. It felt like the "smart senior engineer" decision to make.&lt;/p&gt;

&lt;p&gt;But when we looked at the price tag, we hesitated. It was &lt;em&gt;expensive&lt;/em&gt;. We were about to commit to substantial annual licensing fees, multi-year contracts, and integration work that everyone assumed was "just configuration."&lt;/p&gt;

&lt;p&gt;So, we did something uncomfortable: we challenged our own "smart" assumption.&lt;/p&gt;

&lt;p&gt;We looked deeper. We found that OAuth 2.0 is actually very standardized and prescriptive. We realized that integrating an external vendor with our complex web of internal legacy identity providers was going to be a nightmare regardless of who wrote the auth server. The hard part wasn't the OAuth protocol—it was mapping our Byzantine internal user hierarchies and permission structures.&lt;/p&gt;

&lt;p&gt;So we decided to be naive. We decided to build it.&lt;/p&gt;

&lt;p&gt;The result? It took us one quarter. It cost us a fraction of the vendor price—saving substantial licensing fees annually. It's been running in production for years, scaling to multiple brands.&lt;/p&gt;

&lt;p&gt;Don't get me wrong—buying isn't bad. We don't build Git from scratch; we don't build VMware from scratch. But in this specific case, the "obvious" choice was the wrong one. We nearly let a safe assumption cost us six figures and lock us into a decision we'd regret for years.&lt;/p&gt;

&lt;p&gt;This experience taught me a lesson that defines how I lead today: &lt;strong&gt;Engineering isn't about &lt;em&gt;being&lt;/em&gt; right. It's about &lt;em&gt;getting&lt;/em&gt; it right.&lt;/strong&gt; And the only way to get it right is to relentlessly challenge your assumptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assumptions Are Hidden Technical Debt
&lt;/h2&gt;

&lt;p&gt;We talk a lot about technical debt in code—variables named &lt;code&gt;temp2&lt;/code&gt;, lack of tests, hardcoded values. But there is a much more dangerous kind of debt: &lt;strong&gt;Decision Inertia.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy4ltkt1hoyft9okqsdd9.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy4ltkt1hoyft9okqsdd9.jpeg" alt="Gopher navigating around the iceberg of hidden technical debt" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every time you say, "Well, we just use Git because that's what we use," or "We can't rewrite this legacy app, it's too big," or "We can't move off this bare-metal MySQL cluster, it's too stateful," you are taking out a loan. You are deferring the work of thinking. You are snowballing unverified beliefs into a mountain of constraints that eventually makes it impossible to move.&lt;/p&gt;

&lt;p&gt;Why do we do this?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Survivorship Bias:&lt;/strong&gt; We see that legacy system running and assume it's just "old code"—but forget it solved real business problems that still exist today. That spaghetti monolith? It handles edge cases you don't even know about yet.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Vendor Demo Trap:&lt;/strong&gt; We've all seen the slick demo where the vendor solution handles every use case with one click. But we forget that the integration is always harder than the demo, and our snowflake infrastructure never fits their assumptions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Migration PTSD:&lt;/strong&gt; We've been burned by past migrations that went sideways—databases that didn't replicate correctly, services that couldn't handle production load. So we assume this one will fail too, even when the circumstances are completely different.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to grow from a senior engineer to a principal or staff level, you have to stop accepting "that's just how it is" as an answer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Assumptions are the hidden technical debt of architecture.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The ACT Framework
&lt;/h2&gt;

&lt;p&gt;Over the years, I've leaned on a simple mental model to help teams break out of this inertia. I call it the &lt;strong&gt;ACT Framework&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Full disclosure: This isn't some new magic. It's the &lt;a href="https://en.wikipedia.org/wiki/Scientific_method" rel="noopener noreferrer"&gt;&lt;strong&gt;scientific method&lt;/strong&gt;&lt;/a&gt;, simplified and repackaged for the chaos of daily engineering. I use it because it's memorable enough to apply in the heat of the moment, without needing a textbook to recall the steps.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  1. Awareness
&lt;/h3&gt;

&lt;p&gt;You can't fix what you don't see. Most of the time, we aren't even aware we're making an assumption.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Recognize the trap:&lt;/strong&gt; You assume the train knows the way to the destination until you end up in the wrong city.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Daily Question:&lt;/strong&gt; Ask yourself, "What is one thing I am taking for granted today?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Challenge "Facts":&lt;/strong&gt; Maybe it's that your database needs to run on bare metal. Maybe it's that a service &lt;em&gt;needs&lt;/em&gt; to be written in Go. Be aware of the "facts" that are actually just habits.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Challenge
&lt;/h3&gt;

&lt;p&gt;Once you spot an assumption, poke it. Even if you think it's right, challenge it anyway.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What if we &lt;em&gt;didn't&lt;/em&gt; use Kubernetes for this?"&lt;/li&gt;
&lt;li&gt;"What if we &lt;em&gt;did&lt;/em&gt; rewrite this 10-year-old monolith?"&lt;/li&gt;
&lt;li&gt;"What if the 'industry standard' doesn't apply to our specific constraints?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This fosters psychological safety. When you normalize challenging ideas, you create an environment where people aren't afraid to be wrong. You shift the focus from "Who has the best idea?" to "What is the best idea?"&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Test (Evidence is King)
&lt;/h3&gt;

&lt;p&gt;This is the most critical step. Challenging without testing is just intellectual debate—it's just noise.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start small:&lt;/strong&gt; We didn't build the whole thing first; we spun up a quick POC and load-tested it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gather data:&lt;/strong&gt; We ran benchmarks and proved that modern virtualized instances could match or exceed the bare metal performance we thought we needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide with evidence:&lt;/strong&gt; Opinions are cheap. Senior engineers have opinions; Staff engineers have data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flmc5d0ui74qggxn2g3bx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flmc5d0ui74qggxn2g3bx.png" alt="ACT Diagram" width="800" height="709"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Scaling to the Team: The Assumption Audit
&lt;/h3&gt;

&lt;p&gt;The ACT framework is great for checking your own biases, but challenging assumptions is ultimately a team sport. If you're the only one doing it, you'll quickly become "that person" who blocks every PR with philosophical questions. You need to bring the team along.&lt;/p&gt;

&lt;p&gt;I like to use a technique called &lt;strong&gt;Inversion&lt;/strong&gt; during design reviews.&lt;/p&gt;

&lt;p&gt;Instead of asking "Is this the right choice?", I ask the room:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"What conditions would have to be true for the **opposite&lt;/em&gt;* of this decision to be the right choice?"*&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This forces the team to articulate the hidden constraints they are assuming exist.&lt;/p&gt;

&lt;p&gt;For example, if the team assumes "We must use Microservices," the inversion asks: &lt;em&gt;"What would have to be true for a Monolith to be the better choice here?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Usually, the answer is something like, &lt;em&gt;"Well, we'd need to not care about independent deployments."&lt;/em&gt; And then you can check: &lt;em&gt;"Do we actually care about independent deployments for this specific internal tool?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here's the irony: we platform teams spend all day promoting microservices to product engineers—"decouple your domains!" "independent deployability!"—but then we turn around and build monolithic internal tools because "it's just for us" and "we don't need to scale that." When you invert your own assumptions, you often find you're giving advice you don't follow.&lt;/p&gt;

&lt;p&gt;Suddenly, the assumption is visible. Now you can debate the constraint, not the solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Power of Naive Questions
&lt;/h2&gt;

&lt;p&gt;There is a trap that experienced engineers fall into. We get cynical. We've seen projects fail, we've seen technologies hype cycle and die, and we start to assume that "it's complicated" means "it's impossible."&lt;/p&gt;

&lt;p&gt;The most valuable engineers I know retain a degree of "naivety"—the willingness to ask "How hard can it be?" even when the "smart" answer is "don't even try."&lt;/p&gt;

&lt;p&gt;I saw this play out recently when someone asked if we could make our database infrastructure fully self-service using Ansible and Terraform. The room went quiet. Databases are the third rail—stateful, critical, full of subtle operational constraints.&lt;/p&gt;

&lt;p&gt;I could have said no. I could have cited all the reasons it wouldn't work—stateful workloads are different, replication is hard, compliance adds complexity, we'd need a whole platform team just for this.&lt;/p&gt;

&lt;p&gt;But instead, I said: "I don't know. How hard can it be? Let's try and find out."&lt;/p&gt;

&lt;p&gt;We gave ourselves one quarter to prove it could work. We defined clear success criteria: spin up a production-ready, compliant Postgres instance in under an hour. The first attempt took three weeks and broke in spectacular ways. But we learned. We iterated. We automated the pain points one by one.&lt;/p&gt;

&lt;p&gt;Nine months later, you can spin up a production-ready, compliant Postgres instance in minutes with a single PR. We unlocked velocity that we assumed was impossible because we were willing to try.&lt;/p&gt;

&lt;p&gt;The lesson? Sometimes "it's complicated" is true. But sometimes it's just a story we tell ourselves to avoid the work of finding out.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Sell the Risk
&lt;/h2&gt;

&lt;p&gt;Now, I can hear you saying: &lt;em&gt;"This sounds great, Alex, but my PM wants this feature shipped yesterday. I don't have time to play scientist."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I get it. The pressure is real.&lt;/p&gt;

&lt;p&gt;When you challenge an assumption—especially a "safe" one like buying a vendor or using a standard library—you are introducing risk. You are trading certainty (even if it's expensive or slow) for uncertainty.&lt;/p&gt;

&lt;p&gt;Management hates uncertainty. So, don't sell them uncertainty. Sell them a &lt;strong&gt;Timeboxed Spike&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is the script I use:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I have a hunch this assumption might be wrong. Give me 2 days to verify it. If I'm right, we save weeks of complexity and significant budget. If I'm wrong, we lost 2 days. Is that a trade you're willing to make?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Why does this work?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Capped Downside:&lt;/strong&gt; You explicitly state the maximum loss (2 days).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asymmetric Upside:&lt;/strong&gt; The potential gain (weeks of time, budget) massively outweighs the cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business Language:&lt;/strong&gt; You aren't asking to "refactor" or "explore"; you are proposing a calculated bet with positive expected value.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most reasonable leaders will take that bet every single time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Goal: The Best Answer Wins
&lt;/h2&gt;

&lt;p&gt;If you take one thing from this post, let it be this: &lt;strong&gt;Check your ego at the door.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Challenging assumptions isn't about being the smartest person in the room who points out logical fallacies. It's about vulnerability. It's about saying, "I might be wrong about this, and I want to find out."&lt;/p&gt;

&lt;p&gt;It's uncomfortable. It requires you to talk to other teams, to push back on managers, to question legacy decisions made by people who are still at the company.&lt;/p&gt;

&lt;p&gt;But that is the job.&lt;/p&gt;

&lt;p&gt;The best engineers I know are the ones who are happiest when their own assumptions are proven wrong—because that means they just learned something, and the team is about to build something better.&lt;/p&gt;

&lt;p&gt;So, here is my challenge to you: &lt;strong&gt;What is one assumption you are holding onto right now?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Document it. Challenge it. Test it.&lt;/p&gt;

&lt;p&gt;You might just save your company a significant amount of budget, or better yet, you might build something you didn't know was possible.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>leadership</category>
      <category>security</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>You are a Senior Engineer, Mastering Communication &amp; Influence (Part 3)</title>
      <dc:creator>Alexandre Amado de Castro</dc:creator>
      <pubDate>Tue, 13 Jan 2026 15:30:00 +0000</pubDate>
      <link>https://forem.com/alexandreamadocastro/you-are-a-senior-engineer-mastering-communication-influence-part-3-3p65</link>
      <guid>https://forem.com/alexandreamadocastro/you-are-a-senior-engineer-mastering-communication-influence-part-3-3p65</guid>
      <description>&lt;p&gt;I once watched a brilliant engineer lose a promotion to someone with half his technical depth. The difference? The other guy could explain his work in a way that made executives actually care.&lt;/p&gt;

&lt;p&gt;This is a hard truth, but it's one we need to face. As we continue our journey through your professional development, it's time to zero in on the "soft skills"—communication, influence, and leadership—that often determine your career trajectory and impact more than your code does.&lt;/p&gt;

&lt;p&gt;As a Principal Engineer, I have the great opportunity to mentor many seniors, and I can tell you right now: the difference between a senior who stays at that level and one who advances to staff, principal, or tech lead positions almost always comes down to these soft skills. So, grab your favorite brew, and let's dive into the essentials that will set you apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Art of Communication
&lt;/h2&gt;

&lt;p&gt;Communication isn't just about conveying information; it's about making connections, inspiring your team, and presenting your ideas effectively. Here's the truth: you could design the most elegant system in the world, but if you can't explain why it matters, it won't get built. Whether you're explaining complex technical details to stakeholders or mentoring a newcomer, the way you communicate can make all the difference.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adaptability: Tailoring Your Message
&lt;/h3&gt;

&lt;p&gt;In the fast-paced world of tech, your audience can range from highly technical peers to business-oriented stakeholders. The key to effective communication lies in your ability to adapt, and this is something I had to learn the hard way early in my career.&lt;/p&gt;

&lt;p&gt;Imagine you're explaining a new architectural decision. To your engineering team, you'll dive deep into the technical specifics—maybe you're discussing the trade-offs between using a message queue versus direct API calls, the implications for eventual consistency, or the specifics of your chosen design pattern. You might talk about how this aligns with SOLID principles or how it improves your system's observability.&lt;/p&gt;

&lt;p&gt;However, when presenting that same decision to executives, your focus should shift entirely to the business impact. They don't care about your elegant use of the Strategy pattern. What they care about is: how does this decision enhance scalability? Will it reduce our cloud costs? Does it help us ship features faster? Will it improve our uptime and reduce those 3 AM incidents that wake up the on-call team?&lt;/p&gt;

&lt;p&gt;Here's a practical example: I once had to justify migrating our monolith to microservices. To the engineering team, I talked about service boundaries, deployment independence, and technology diversity. To the executive team, I framed it as: "This will allow us to deploy new features 10x faster without risking the entire platform, and it will reduce our incident recovery time from hours to minutes." Same decision, completely different framing.&lt;/p&gt;

&lt;p&gt;The best part? You don't need to dumb anything down. You're just translating between contexts. Think of it like being bilingual—you're fluent in both technical and business language, and you know when to use each.&lt;/p&gt;

&lt;h3&gt;
  
  
  Patience and Repetition: Reinforcing Concepts
&lt;/h3&gt;

&lt;p&gt;Here's something nobody tells you about being a senior: you're going to explain the same thing over and over again. And that's okay! In fact, it's part of the job.&lt;/p&gt;

&lt;p&gt;Explaining the same concept multiple times, from different angles, until it clicks isn't a sign that you're bad at explaining or that your team isn't listening. It's simply how humans learn complex topics. Embrace this repetition—it's an opportunity to refine your explanations and ensure everyone's on the same page.&lt;/p&gt;

&lt;p&gt;Think of it as an iterative process, much like software development itself. Each explanation is a new iteration, improving understanding and clarity. Your first explanation might be too technical. Your second might be too abstract. By the third or fourth time, you'll find that sweet spot where it finally clicks for everyone.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I remember when I was introducing my team to event-driven architecture. The first time I explained it, I lost them in the technical details of event sourcing and CQRS. The second time, I used a restaurant ordering system as an analogy—the waiter takes your order (event), the kitchen prepares it (event handler), and the waiter brings it out (another event). That's when I saw the lightbulbs go off. Sometimes you just need to find the right metaphor or example.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This patience not only helps in solidifying concepts but also in building a collaborative and inclusive environment where people feel safe asking questions and admitting when they don't understand something.&lt;/p&gt;

&lt;h3&gt;
  
  
  Embracing Questions and Uncertainty
&lt;/h3&gt;

&lt;p&gt;Questions are inevitable, and how you handle them sets you apart. Here's something I've learned: the moment you pretend to know everything is the moment you lose credibility. Embrace curiosity, admit when you don't know something, and commit to finding the answer. This openness builds trust and fosters a learning culture.&lt;/p&gt;

&lt;p&gt;For instance, in a meeting, if a junior developer asks a question that stumps you, instead of deflecting or making something up, acknowledge it honestly. Say something like, "That's a great question, and honestly, I don't have the answer right now. Let me look into it and get back to you by end of day." This not only shows humility but also models the behavior you want to see in your team.&lt;/p&gt;

&lt;p&gt;I've had moments where a junior engineer asked about a specific edge case in our distributed system that I hadn't considered. Instead of brushing it off, I said, "You know what? That's a scenario I haven't thought through. Let's whiteboard this together and figure out what would happen." We ended up discovering a real bug that could have caused data inconsistency. That junior engineer's question saved us from a production incident, and they felt empowered to keep asking tough questions.&lt;/p&gt;

&lt;p&gt;The best engineers I know are the ones who aren't afraid to say "I don't know, but let's find out together."&lt;/p&gt;

&lt;h3&gt;
  
  
  Feedback and Suggestions: Two-Way Communication
&lt;/h3&gt;

&lt;p&gt;Constructive feedback is a two-way street, and getting good at both giving and receiving it will transform your team dynamics. For example, when reviewing a colleague's code, focus on providing specific, actionable feedback rather than vague criticisms. Instead of "This code is messy," try "I noticed this function is doing three different things. What if we extracted the validation logic into a separate function? It would make this easier to test and understand."&lt;/p&gt;

&lt;p&gt;On the flip side, when receiving feedback, approach it with an open mind. This is hard—nobody likes hearing their work could be better. But here's the thing: even seemingly minor suggestions can spark innovation. I've had colleagues point out patterns in my code reviews that led to us establishing new team standards. I've had junior developers ask "why" questions that made me reconsider assumptions I'd been making for years.&lt;/p&gt;

&lt;p&gt;The best feedback loop I've seen worked like this: during code reviews, we'd frame suggestions as questions. "Have you considered...?" or "What was the reasoning behind...?" instead of "You should..." This small shift made feedback feel collaborative rather than critical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagrams: The Blueprint of Understanding
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqud5x0lqs7afs5p8g4cr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqud5x0lqs7afs5p8g4cr.png" alt="A gopher presenting an architecture diagram on a whiteboard, showing system components and their relationships." width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a Senior Engineer, explaining complex systems clearly is a big part of your role, and this is where diagrams become your best friend. I can't tell you how many times a 10-minute whiteboard session with a diagram has solved a problem that hours of Slack messages couldn't.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mastering Diagramming Techniques
&lt;/h3&gt;

&lt;p&gt;There are three main diagramming approaches you should have in your toolkit, and each serves a different purpose:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UML (Unified Modeling Language)&lt;/strong&gt;: This is your go-to for detailing class structures and interactions. Use UML when you need to show how objects relate to each other, their methods, and their relationships. It's particularly useful when discussing design patterns with your team or planning out a new module's structure. For example, when I'm introducing the Factory pattern to my team, I'll use a UML class diagram to show the relationship between the factory, the interface, and the concrete implementations. It makes the abstract concept concrete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;C4 (Context, Containers, Components, Code)&lt;/strong&gt;: This is ideal for contextualizing software architecture within its environment, and honestly, it's become my favorite for most architecture discussions. The beauty of C4 is that it works at multiple levels of zoom. At the Context level, you're showing how your system fits into the broader ecosystem—what external systems does it talk to? Who are the users? Then you zoom into Containers to show the high-level technical building blocks (web app, database, message queue), then Components to show how the container is structured, and finally Code if you need that level of detail.&lt;/p&gt;

&lt;p&gt;I recently used C4 diagrams to explain our platform architecture to new team members. The Context diagram showed how we integrate with payment providers and our mobile apps. The Container diagram revealed our microservices, databases, and message queues. By the time we got to the Component diagram, they understood exactly where they'd be working and how their code would fit into the bigger picture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ArchiMate&lt;/strong&gt;: This is your enterprise architecture tool. It's more formal and structured than C4, making it perfect when you're dealing with complex business domains and need to show not just technical architecture but also business processes, organizational structure, and how they all interconnect. I'll be honest, I don't reach for ArchiMate often, but when I'm working with enterprise architects or documenting large-scale organizational changes, it's invaluable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Diagram Guidelines: Clarity and Consistency
&lt;/h3&gt;

&lt;p&gt;Here's the thing about diagrams: a bad diagram is worse than no diagram at all. A confusing diagram will derail your meeting and leave everyone more confused than when you started.&lt;/p&gt;

&lt;p&gt;Creating effective diagrams is both an art and a science. Here are the rules I follow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every element must earn its place.&lt;/strong&gt; If a box, arrow, or label doesn't add clarity, remove it. I've seen diagrams with 50 boxes trying to show everything at once. Instead, create multiple diagrams at different levels of detail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clear titles and labels are non-negotiable.&lt;/strong&gt; Your diagram should be understandable without you explaining it. I test this by showing diagrams to someone unfamiliar with the project and asking them to explain it back to me. If they can't, I simplify.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consistency in visual language is crucial.&lt;/strong&gt; Pick a color scheme and stick to it. For example, in my C4 diagrams, I always use blue for internal services, gray for external systems, and orange for databases. This consistency means that when someone sees a blue box, they immediately know it's something we own and control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use arrows deliberately.&lt;/strong&gt; Show direction of data flow, not just "these things are related." A bidirectional arrow should mean something different than a unidirectional one.&lt;/p&gt;

&lt;p&gt;I learned this the hard way after creating a beautiful, detailed diagram that nobody understood. Now I follow the principle: start simple, add complexity only when necessary. Your first diagram should be something you could sketch on a napkin.&lt;/p&gt;

&lt;p&gt;Here's what this looks like in practice. Let's say you're documenting a payment processing flow. Here's what happens when you ignore these guidelines:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before: Cluttered and Confusing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F08wincu4l25xb8ygw9se.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F08wincu4l25xb8ygw9se.png" alt="Diagram before" width="800" height="261"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This diagram is technically accurate—all these components exist. But does it help you understand the payment flow? Probably not. You're drowning in infrastructure noise. Which path does a payment actually take? What's essential vs. what's just support infrastructure?&lt;/p&gt;

&lt;p&gt;Now, here's the same flow following the principles I just covered:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After: Clear and Purposeful&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqka244b5y5d4umgfilf3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqka244b5y5d4umgfilf3.png" alt="After diagram" width="800" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;See the difference? Every element earns its place. The numbered arrows show you exactly how data flows through the system. The colors are consistent: blue for our internal services, orange for data stores, green for external systems. You could sketch this on a napkin in 30 seconds, and a new team member could understand the payment flow immediately.&lt;/p&gt;

&lt;p&gt;The logging service, monitoring service, and config service? They're important, but they don't help explain the payment flow, so they don't belong here. If you need to document how monitoring works, create a separate diagram for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Art of Presentation
&lt;/h2&gt;

&lt;p&gt;Presenting effectively is crucial, whether in a formal setting or an informal team meeting. I've seen great ideas die because they were poorly presented, and mediocre ideas gain traction because they were presented well. Your technical skills got you to senior, but your presentation skills will determine how far you go beyond that.&lt;/p&gt;

&lt;p&gt;Here's something I wish someone had taught me earlier: know your audience and format before you build your presentation. A live presentation where you're persuading stakeholders needs minimal slides and compelling visuals—you are the presentation, and your slides just support you. An RFC or architecture decision record that people will read asynchronously needs comprehensive detail and full context, because you won't be there to explain it.&lt;/p&gt;

&lt;p&gt;The worst mistake? Creating a dense document and trying to present it live, or creating a sparse slide deck and sending it to people expecting them to understand it without you. Match the format to the medium, and you'll save yourself hours of rework.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leadership: Influence Over Authority
&lt;/h2&gt;

&lt;p&gt;Here's something that surprised me when I became a senior: leadership in tech isn't about wielding authority; it's about inspiring and guiding your team. You don't have direct reports (unless you transition to management), but you're expected to lead. This is what we call "influence without authority," and it's one of the most important skills you'll develop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leading by Example
&lt;/h3&gt;

&lt;p&gt;This sounds cliché, but it's true: demonstrate the behaviors and practices you want to see in your team. Leading by example builds trust and respect faster than any amount of talking.&lt;/p&gt;

&lt;p&gt;For instance, if you advocate for clean code, ensure your contributions to the codebase reflect those principles. I learned this when I was pushing my team to write better tests. I realized that some of my recent PRs had skimped on test coverage because I was rushing. How could I expect them to prioritize testing if I wasn't? So I made a point of writing comprehensive tests, adding thoughtful comments explaining my testing strategy, and during code reviews, I'd reference my own PRs as examples. The shift in team behavior was noticeable within weeks.&lt;/p&gt;

&lt;p&gt;If continuous learning is a value, share resources and lead knowledge-sharing sessions. I started a bi-weekly "Tech Talk Tuesday" where anyone could present something they learned—a new tool, a design pattern, a debugging technique. I made sure to present first to set the tone and show it was okay to present on simple topics or to say "I don't fully understand this yet, but here's what I've learned so far."&lt;/p&gt;

&lt;p&gt;Another true test of leadership is how you handle failure. When a production incident happens, it's easy to point fingers. But as a Senior Engineer, I learned to lead &lt;strong&gt;blameless post-mortems&lt;/strong&gt;. Instead of asking "Who broke this?", I asked "How did our system allow this to happen?" and "What information were we missing?" By shifting the focus from blame to process improvement, you don't just fix the bug—you build psychological safety for the whole team. That's leadership.&lt;/p&gt;

&lt;p&gt;Here's the key: your team is watching you. If you say documentation is important but never update the docs, they won't either. If you say work-life balance matters but you're sending Slack messages at midnight, that's the culture you're creating. Lead by example, not by decree.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pushing Boundaries
&lt;/h3&gt;

&lt;p&gt;Growth comes from stepping out of your comfort zone, and as a senior, part of your job is to push both yourself and your team to grow. Whether it's learning a new technology, leading a challenging project, or advocating for a controversial architectural decision, continuous personal and professional development is key.&lt;/p&gt;

&lt;p&gt;Don't shy away from opportunities that stretch your abilities—embrace them as a chance to grow. When my manager asked if I wanted to lead the migration from our monolith to microservices, my first thought was "I've never done this at this scale before." My second thought was "This is exactly why I should say yes." That project pushed me harder than anything in my career, and I learned more in those six months than in the previous two years.&lt;/p&gt;

&lt;p&gt;But here's the thing: pushing boundaries doesn't mean being reckless. It means calculated risks. It means doing your homework, building proof of concepts, getting feedback from trusted peers, and then going for it. Some of the best innovations I've seen came from engineers who said, "I know this isn't how we've always done it, but what if we tried..."&lt;/p&gt;

&lt;p&gt;Encourage your teammates to do the same. When a mid-level engineer proposes something ambitious, instead of listing all the reasons it might fail, ask them "What would you need to make this work?" and help them get there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Speaking Business
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frypjukofav24wv5p89ep.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frypjukofav24wv5p89ep.png" alt="A professional gopher in business attire standing in a conference room with an ROI chart showing upward growth." width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Understanding and speaking the language of business is crucial, and this is where many technical folks struggle. You need to master negotiation, understand risk, and use business terminology to align technical solutions with business objectives.&lt;/p&gt;

&lt;p&gt;Here's what I mean: when proposing a technical initiative, frame it in terms of business benefits—how it will drive revenue, reduce costs, or improve customer satisfaction. Don't just say "We need to refactor this service." Instead, say "Refactoring this service will reduce our cloud costs by 30% and cut our time to ship new features from two weeks to two days, which means we can respond to customer requests faster than our competitors."&lt;/p&gt;

&lt;p&gt;Learn to speak in terms of ROI (Return on Investment), TCO (Total Cost of Ownership), and OKRs (Objectives and Key Results). When you're discussing technical debt, frame it as risk: "This legacy system represents significant business risk. If it fails, we lose $50K per hour of downtime. Modernizing it is an investment in business continuity."&lt;/p&gt;

&lt;p&gt;I literally treat it like an equation: &lt;code&gt;(Current Pain $$ + Risk $$) &amp;gt; (Migration Cost $$) = Green Light&lt;/code&gt;. If I can't quantify the 'Current Pain' in dollars or time, I know I'm not ready to pitch it to leadership.&lt;/p&gt;

&lt;p&gt;I'll be honest: this felt uncomfortable at first. I became an engineer because I loved code, not because I wanted to think about quarterly revenue targets. But here's the reality: the work you do has to serve business goals. The better you are at connecting your technical work to business outcomes, the more autonomy you'll have, the more resources you'll get, and the more impact you'll make.&lt;/p&gt;

&lt;p&gt;Some practical ways to develop this skill: sit in on product planning meetings, read your company's quarterly reports, talk to your product managers about what customers are asking for, and most importantly, ask "why" about business decisions until you understand the reasoning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep Growing, Keep Leading
&lt;/h2&gt;

&lt;p&gt;Your role as a Senior Engineer is complex and rewarding. The technical skills got you here, but these soft skills—communication, presentation, and leadership—will determine where you go next. By mastering these skills, you'll elevate not only your career but also those around you.&lt;/p&gt;

&lt;p&gt;Remember, every staff engineer, every principal engineer, every tech lead you admire got there not just because they were brilliant coders, but because they could communicate their brilliance, lead their teams, and create impact beyond their individual contributions.&lt;/p&gt;

&lt;p&gt;Stay curious, stay communicative, and keep leading the way. You've got this ❤️&lt;/p&gt;

&lt;p&gt;Communication is the leverage that multiplies your technical impact. Want to level up your leadership? Let's tackle your challenges together.&lt;/p&gt;

</description>
      <category>career</category>
      <category>softwareengineering</category>
      <category>leadership</category>
      <category>c4</category>
    </item>
    <item>
      <title>Building a Custom AI Code Reviewer for GitHub Enterprise with Bedrock and Go</title>
      <dc:creator>Alexandre Amado de Castro</dc:creator>
      <pubDate>Tue, 09 Dec 2025 14:44:05 +0000</pubDate>
      <link>https://forem.com/alexandreamadocastro/building-a-custom-ai-code-reviewer-for-github-enterprise-with-bedrock-and-go-1jaj</link>
      <guid>https://forem.com/alexandreamadocastro/building-a-custom-ai-code-reviewer-for-github-enterprise-with-bedrock-and-go-1jaj</guid>
      <description>&lt;p&gt;Two weeks after deploying our custom AI code reviewer, it caught a bug that would have crashed our payment processing. A senior engineer's PR looked clean—it compiled, passed linting, followed Go idioms. But the AI spotted a subtle pattern: we'd accidentally turned a soft dependency into a hard one. If our config service blinked, our main service would die.&lt;/p&gt;

&lt;p&gt;The engineer's response? "Oh."&lt;/p&gt;

&lt;p&gt;That's when the team stopped treating &lt;strong&gt;Archbot&lt;/strong&gt; like a toy.&lt;/p&gt;

&lt;p&gt;Here's the problem we were solving: we run &lt;a href="https://github.com/enterprise" rel="noopener noreferrer"&gt;GitHub Enterprise&lt;/a&gt; behind a VPN. Modern AI code review tools like &lt;a href="https://cursor.sh/" rel="noopener noreferrer"&gt;Cursor&lt;/a&gt;, &lt;a href="https://github.com/features/copilot" rel="noopener noreferrer"&gt;Github Copilot&lt;/a&gt;, and &lt;a href="https://coderabbit.ai/" rel="noopener noreferrer"&gt;CodeRabbit&lt;/a&gt; expect cloud-hosted repos. We couldn't pipe proprietary code into them. But we still wanted AI code review on every PR.&lt;/p&gt;

&lt;p&gt;So I built it myself.&lt;/p&gt;

&lt;p&gt;This is the story of &lt;strong&gt;Archbot&lt;/strong&gt;, the custom AI code reviewer I built to run securely inside enterprise networks. It started as a weekend hack ("how hard could it be?"). Spoiler alert: it didn't start well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned (and want to teach you):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why naive git diff + LLM approaches fail&lt;/li&gt;
&lt;li&gt;How to manage context windows for large repos&lt;/li&gt;
&lt;li&gt;Using Bedrock Tool schemas for structured LLM output&lt;/li&gt;
&lt;li&gt;Two-phase AI architecture for intelligent filtering&lt;/li&gt;
&lt;li&gt;Production patterns for running AI tools in CI/CD&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Git Diff Alone Fails for AI Code Review
&lt;/h2&gt;

&lt;p&gt;My first iteration was painfully simple. I wrote a &lt;a href="https://go.dev/" rel="noopener noreferrer"&gt;Go&lt;/a&gt; program that fetched the &lt;code&gt;git diff&lt;/code&gt; of a PR and sent it to the LLM with a prompt like: "Find bugs in this code."&lt;/p&gt;

&lt;p&gt;I felt like a genius for about five minutes. Then the results came in.&lt;/p&gt;

&lt;p&gt;The AI would confidently claim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Variable &lt;code&gt;userRepository&lt;/code&gt; is undefined.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I’d look at the code. It was imported right there at the top of the file. But because the &lt;code&gt;git diff&lt;/code&gt; only shows &lt;em&gt;changed&lt;/em&gt; lines (and a few lines of context), the AI couldn't see the imports.&lt;/p&gt;

&lt;p&gt;It was the equivalent of trying to review a book by reading only the sentences that were edited in the second draft. You lose the plot immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; Context is king. Without the file definitions, imports, and surrounding scope, the AI is just guessing.&lt;/p&gt;

&lt;p&gt;So I went back to the drawing board.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub API Rate Limits and the Sequential Fetch Problem
&lt;/h2&gt;

&lt;p&gt;My next attempt was obvious: don't just send the diff—fetch the full files from GitHub's API.&lt;/p&gt;

&lt;p&gt;I updated the bot to parse the diff, identify which files were changed, and then use the GitHub API to fetch the &lt;em&gt;full content&lt;/em&gt; of those files.&lt;/p&gt;

&lt;p&gt;This worked... until someone opened a PR that touched 40 files.&lt;/p&gt;

&lt;p&gt;My bot fired off 40 sequential API calls to GitHub.&lt;br&gt;
GitHub fired back a &lt;code&gt;429 Too Many Requests&lt;/code&gt;.&lt;br&gt;
The bot crashed.&lt;br&gt;
The developer waited 5 minutes for a review that never came.&lt;/p&gt;

&lt;p&gt;And even when it &lt;em&gt;did&lt;/em&gt; work, it was slow. We were burning network time just shuttling strings around. I realized I was trying to rebuild &lt;code&gt;git clone&lt;/code&gt; over HTTP, poorly.&lt;/p&gt;
&lt;h2&gt;
  
  
  Using Repomix to Pack Repository Context for AI
&lt;/h2&gt;

&lt;p&gt;I needed a way to pack the repository context efficiently. That's when I found &lt;strong&gt;&lt;a href="https://github.com/yamadashy/repomix" rel="noopener noreferrer"&gt;Repomix&lt;/a&gt;&lt;/strong&gt; (formerly &lt;code&gt;repopack&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Repomix solves a subtle problem: AI models don't understand file systems. You can't just tar a repo and send it—you need structure. Repomix walks your tree and outputs either XML or Markdown with clear file boundaries, respecting &lt;code&gt;.gitignore&lt;/code&gt;, and including metadata like file sizes and token counts. It's like &lt;code&gt;pandoc&lt;/code&gt; for codebases.&lt;/p&gt;

&lt;p&gt;Perfect, right?&lt;/p&gt;

&lt;p&gt;There was just one catch: &lt;strong&gt;Repomix is a Node.js tool.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Archbot is written in Go.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why didn't I just write the bot in Python/TypeScript? Two reasons: &lt;strong&gt;Concurrency&lt;/strong&gt; and &lt;strong&gt;Deployment&lt;/strong&gt;. We needed to handle bursts of webhook events without spinning up heavy worker processes, and shipping a single static binary to our distroless containers keeps our security team happy.&lt;/p&gt;

&lt;p&gt;I didn't want to rewrite my entire bot in TypeScript. I didn't want to manage two separate deployment pipelines. And I definitely didn't want to write a Go port of Repomix (that's a two-month side quest).&lt;/p&gt;

&lt;p&gt;So I did what platform engineers do best: I made the tools work together.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Multi-Stage Build Solution
&lt;/h3&gt;

&lt;p&gt;The solution was a standard Docker multi-stage build: compile the Go binary in one stage, run it in a Node runtime in the second.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stage 1: Build the Go binary&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;golang:latest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;go mod download
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nv"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;linux go build &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /app/archbot ./cmd/.

&lt;span class="c"&gt;# Stage 2: Runtime with Node for Repomix&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:lts-bullseye&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; ca-certificates tzdata git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; repomix

&lt;span class="c"&gt;# Copy the pre-built Go binary from stage 1&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/archbot /app/&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 9000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["/app/archbot"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We simply copy the compiled Go binary into a Node.js base image. The Go binary runs as the main process and uses &lt;code&gt;exec.Command&lt;/code&gt; to shell out to the globally-installed &lt;code&gt;repomix&lt;/code&gt; command when needed.&lt;/p&gt;

&lt;p&gt;It's not the purest architectural pattern, but it shipped the feature in 2 hours instead of 2 weeks. Sometimes, platform engineering is about making the tools work for you, not being a purist.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Performance Win
&lt;/h3&gt;

&lt;p&gt;Here's what this actually improved: &lt;strong&gt;we stopped hammering the GitHub API&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When a PR comes in, Archbot clones the repo once (network call), runs &lt;code&gt;repomix&lt;/code&gt; locally (disk I/O), and gets a nice XML string of the codebase in ~2 seconds. Compare that to the "Attempt 2" approach where we made 40+ sequential API calls per PR.&lt;/p&gt;

&lt;p&gt;Git cloning is fast. S3 caching (we cache the repomix output) handles way more load than the on-prem GitHub API could. The bottleneck shifted from network to disk, which is exactly what you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context Window Management for Large Repositories
&lt;/h2&gt;

&lt;p&gt;We deployed the Repomix version. It worked great for small microservices.&lt;/p&gt;

&lt;p&gt;Then we tried it on The Monolith™.&lt;/p&gt;

&lt;p&gt;You know the one. Millions of lines of code. Hundreds of directories. When Repomix packed that repo, the resulting text file was massive—way larger than the context window of even the generous 200k token models.&lt;/p&gt;

&lt;p&gt;And even if it &lt;em&gt;did&lt;/em&gt; fit, sending 150k tokens for every simple bug fix is a great way to burn through your budget.&lt;/p&gt;

&lt;p&gt;I realized I couldn't just "feed the repo" to the AI. I had to replicate how &lt;em&gt;I&lt;/em&gt; review code.&lt;/p&gt;

&lt;p&gt;When I review a PR, I don't read the entire codebase.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I look at the diff.&lt;/li&gt;
&lt;li&gt;I check the file tree to see where the changes live.&lt;/li&gt;
&lt;li&gt;I open &lt;em&gt;only&lt;/em&gt; the files that are relevant to the change to verify imports and usage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We needed a &lt;strong&gt;Two-Phase Architecture&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of blindly packing the entire repo, we split the review into two phases:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://mermaid.live/edit#pako:eNqNVG1v2jAQ_iuWP20SZUkgBfJhUgtau4muUWk1aUmFQnKARWJnttPBSv_7_JKkoKKq_uI739tzd757xinLAAd4xZNyje4vY4rUeRDAP0UTeIKclcAfP6Ozs6_72xKo-PJQZokEgcK7PfoFizVjm-iKyOtq0bCP1knNGdN7TlYr4GKPLni6XjAZ1Te6TmiWqxAxtVaiWlgs4ToR4EYxNgRyAzQhImVPwHcxrkPokxEOqSSMtuj1mZDlUsMyxIH2PQeIJsaE8R2aSV6lsuJwoBK68-n0JrqEjLN0gxR9IPxGcpgSIaMZ5MoHZOZFoB-z259tDvo0-ansDYbTEg3nGLR5thCOUZ8UWN6WmLE8QMLAmi81qH2L1hoAzd5WOUk3hK5UmceMStjK5gWdoSlLk_wDtb6DkhVkqwFENX1YMZZWArKx3EY1iepQR_VqoJo0DzyeDGOUXh2_k57-O177iTz1iQBK5eqJwN8PpBZ67_yFMSsKoFJE1l3Lv_0Mr1BtF73jLtq-d09ILH_QXzWNS8aLuR7bOTdx923gN3VoEemoV9fzi_B7M6uKrFOx7yZGyIRSbqz2asajsMpzVa8_FQj5iDu4AF4kJFMr41mbx1iuoYAYB4rMEr6JcUxflF5SSTbb0RQHasSggzmrVmscLJNcKK4yS2RCEtWmolEpE_qbsZZdcR2mtlZJAR-zikocuL1zo4yDZ7zFwcjv-r2RP-z5fXcwcLRwp5X8bt_peY4_OB-6PWfw0sH_jHenO-p7rtf3h_7I6TnnQ6-DISNqH9zYXWhW4st_ieSTBg" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNqNVG1v2jAQ_iuWP20SZUkgBfJhUgtau4muUWk1aUmFQnKARWJnttPBSv_7_JKkoKKq_uI739tzd757xinLAAd4xZNyje4vY4rUeRDAP0UTeIKclcAfP6Ozs6_72xKo-PJQZokEgcK7PfoFizVjm-iKyOtq0bCP1knNGdN7TlYr4GKPLni6XjAZ1Te6TmiWqxAxtVaiWlgs4ToR4EYxNgRyAzQhImVPwHcxrkPokxEOqSSMtuj1mZDlUsMyxIH2PQeIJsaE8R2aSV6lsuJwoBK68-n0JrqEjLN0gxR9IPxGcpgSIaMZ5MoHZOZFoB-z259tDvo0-ansDYbTEg3nGLR5thCOUZ8UWN6WmLE8QMLAmi81qH2L1hoAzd5WOUk3hK5UmceMStjK5gWdoSlLk_wDtb6DkhVkqwFENX1YMZZWArKx3EY1iepQR_VqoJo0DzyeDGOUXh2_k57-O177iTz1iQBK5eqJwN8PpBZ67_yFMSsKoFJE1l3Lv_0Mr1BtF73jLtq-d09ILH_QXzWNS8aLuR7bOTdx923gN3VoEemoV9fzi_B7M6uKrFOx7yZGyIRSbqz2asajsMpzVa8_FQj5iDu4AF4kJFMr41mbx1iuoYAYB4rMEr6JcUxflF5SSTbb0RQHasSggzmrVmscLJNcKK4yS2RCEtWmolEpE_qbsZZdcR2mtlZJAR-zikocuL1zo4yDZ7zFwcjv-r2RP-z5fXcwcLRwp5X8bt_peY4_OB-6PWfw0sH_jHenO-p7rtf3h_7I6TnnQ6-DISNqH9zYXWhW4st_ieSTBg%3Ftype%3Dpng" alt="Mermaid Diagram with Process flow" width="547" height="1469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: Intelligent File Selection
&lt;/h3&gt;

&lt;p&gt;Instead of sending the code immediately, we first send the AI the &lt;strong&gt;Directory Structure&lt;/strong&gt; and the &lt;strong&gt;Diff&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We ask it a simple question: &lt;em&gt;"Given these changes, which files do you need to read to verify correctness?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The system prompt for this phase is carefully engineered to guide the AI's selection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a senior code reviewer. You must use the select_files tool to identify
which files are needed to verify the correctness of these changes. Consider:
imports, interface definitions, test coverage, and related business logic.

Be selective—choose only files directly relevant to understanding the change.
If a PR modifies a handler, you likely need the service it calls and any
interfaces it implements. You don't need unrelated files in the same directory.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI is surprisingly good at this. It will say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I see you modified &lt;code&gt;user_handler.go&lt;/code&gt;. I need to see &lt;code&gt;user_service.go&lt;/code&gt; to check the interface and &lt;code&gt;user_handler_test.go&lt;/code&gt; to verify coverage."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Forcing Structured Responses with Bedrock Tools
&lt;/h4&gt;

&lt;p&gt;Cool, but how do we actually tell Bedrock what format to return?&lt;/p&gt;

&lt;p&gt;This is where &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/tool-use.html" rel="noopener noreferrer"&gt;AWS Bedrock Tools&lt;/a&gt; become critical. Instead of hoping the LLM returns parseable text, you define a JSON schema that &lt;strong&gt;forces&lt;/strong&gt; the model to return structured data in exactly the format you specify.&lt;/p&gt;

&lt;p&gt;The model guarantees it returns valid JSON matching that schema. No regex parsing. No "I hope the AI formatted this correctly." Just typed data you can unmarshal directly into a Go struct.&lt;/p&gt;

&lt;p&gt;We use this pattern for both phases: Phase 1 uses a &lt;code&gt;select_files&lt;/code&gt; tool schema (returns an array of file paths), and Phase 2 uses a &lt;code&gt;perform_code_review&lt;/code&gt; tool schema (returns structured review comments with severity levels and inline suggestions). The AI can't return freeform markdown even if it wants to. The schema is the contract.&lt;/p&gt;

&lt;p&gt;We'll see the actual code for these schemas in the implementation section below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2: The Focused Review
&lt;/h3&gt;

&lt;p&gt;Now that we have the list of 5-10 relevant files, we use Repomix to pack them into a focused context. But here's the thing—even those selected files come with baggage.&lt;/p&gt;

&lt;p&gt;Generated code. Vendored dependencies. Lock files. Binary artifacts. Test fixtures.&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;Repomix's exclusion list feature&lt;/strong&gt; becomes critical. Instead of naively packing everything, Repomix lets you define exclusion patterns that filter out files that don't make sense for AI review. Think &lt;code&gt;.gitignore&lt;/code&gt;, but specifically tuned for context packing.&lt;/p&gt;

&lt;p&gt;We configure Repomix to exclude:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generated proto files and mocks&lt;/li&gt;
&lt;li&gt;Third-party vendored code&lt;/li&gt;
&lt;li&gt;Large JSON fixtures and test data&lt;/li&gt;
&lt;li&gt;Binary assets and compiled artifacts&lt;/li&gt;
&lt;li&gt;Package lock files (sorry &lt;code&gt;package-lock.json&lt;/code&gt;, you're 50k lines of noise)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result? I take the selected files and pack them cleanly—stripping out the noise that would confuse the AI or burn tokens unnecessarily.&lt;/p&gt;

&lt;p&gt;This reduces the context size from ~100k tokens (full repo) to ~5k tokens (filtered, focused context). It's faster, cheaper, and because the noise is removed, the AI hallucinates less.&lt;/p&gt;

&lt;p&gt;Let's talk money. The naive approach of sending 150k input tokens plus ~5k output tokens to &lt;a href="https://www.anthropic.com/news/claude-haiku-4-5" rel="noopener noreferrer"&gt;Claude Haiku 4.5&lt;/a&gt; via &lt;a href="https://aws.amazon.com/bedrock/" rel="noopener noreferrer"&gt;Bedrock&lt;/a&gt; would cost roughly $0.18-0.20 per review (based on direct API pricing of $1/$5 per million input/output tokens—Bedrock pricing may vary). With the two-phase approach and Repomix's exclusion filtering, I'm averaging 8k input tokens and ~2.5k output tokens per review—about $0.02 per review. At 50 PRs/day, that's the difference between $300/month (naive) vs $30/month (optimized). The two-phase architecture pays for itself immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation: Bedrock Tools and Structured LLM Output
&lt;/h2&gt;

&lt;p&gt;Okay, enough story time. Let's look at the code.&lt;/p&gt;

&lt;p&gt;The secret sauce isn't just the prompting—it's forcing the LLM to return &lt;strong&gt;structured data&lt;/strong&gt;. Instead of parsing freeform markdown (nightmare fuel), we use AWS Bedrock's "Tool" feature—essentially a JSON schema that constrains the model's output. You define the shape you want, and the model guarantees it returns valid JSON matching that schema. No regex. No "I hope the AI formatted this correctly." Just typed data.&lt;/p&gt;

&lt;p&gt;One quick note: we're using Bedrock's &lt;strong&gt;Converse API&lt;/strong&gt;, not the older &lt;code&gt;InvokeModel&lt;/code&gt; approach that required raw JSON payload construction. The Converse API handles message formatting, tool schemas, and multi-turn conversations for you. If you're still using &lt;code&gt;InvokeModel&lt;/code&gt;, upgrade—it'll save you hours of JSON wrangling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: The Contract (Bedrock Tools)
&lt;/h3&gt;

&lt;p&gt;First, we define the schema for our "tools". In AWS Bedrock (or OpenAI), this tells the model exactly what JSON format we expect back.&lt;/p&gt;

&lt;p&gt;Here is the &lt;code&gt;select_files&lt;/code&gt; tool. Note how we explicitly ask for an array of file paths and a reasoning field.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Imports:&lt;/span&gt;
&lt;span class="c"&gt;// "github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types"&lt;/span&gt;
&lt;span class="c"&gt;// "github.com/aws/aws-sdk-go-v2/service/bedrockruntime/document"&lt;/span&gt;

&lt;span class="c"&gt;// BuildSelectFilesTool creates a tool for selecting relevant files from a PR&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;BuildSelectFilesTool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;bedrockTypes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToolConfiguration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;schemaMap&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;
        &lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"properties"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;
            &lt;span class="s"&gt;"files"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;
                &lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"items"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;
                    &lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s"&gt;"description"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"File path relative to repo root (e.g., 'src/main.go')"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="s"&gt;"description"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"List of relevant file paths to analyze."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="s"&gt;"reasoning"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{&lt;/span&gt;
                &lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"description"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Explanation of why these files were selected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="s"&gt;"required"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"reasoning"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Wrap the schema in Bedrock's ToolConfiguration&lt;/span&gt;
    &lt;span class="n"&gt;toolSpec&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;bedrockTypes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToolMemberToolSpec&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bedrockTypes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToolSpecification&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"select_files"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Select the most relevant files for analysis based on the PR diff"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;InputSchema&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;bedrockTypes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToolInputSchemaMemberJson&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewLazyDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schemaMap&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;bedrockTypes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToolConfiguration&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Tools&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;bedrockTypes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;toolSpec&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;ToolChoice&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;bedrockTypes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToolChoiceMemberTool&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bedrockTypes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SpecificToolChoice&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"select_files"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This schema tells Bedrock: "You must return a JSON object with a &lt;code&gt;files&lt;/code&gt; array of strings and a &lt;code&gt;reasoning&lt;/code&gt; string. No other format is acceptable."&lt;/p&gt;

&lt;p&gt;When you pass this tool configuration to the Converse API, Bedrock guarantees it returns valid JSON matching that schema. The same pattern applies to Phase 2—we define a &lt;code&gt;perform_code_review&lt;/code&gt; tool with fields for severity levels, inline comments, and code suggestions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Phase 1 - File Selection
&lt;/h3&gt;

&lt;p&gt;When a PR comes in, we grab the diff and the file tree (Repomix gives us a nice "Directory Structure" summary we can reuse). We send that to the model.&lt;/p&gt;

&lt;p&gt;Here's the core logic from &lt;code&gt;selectFilesForReview&lt;/code&gt;. This is production code with the defensive patterns you need when running this on every PR across 30 repositories.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;CodeReviewAnalyzer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;selectFilesForReview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prDiff&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Add timeout for production resilience&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c"&gt;// 1. Get directory structure from Repomix (cached)&lt;/span&gt;
    &lt;span class="n"&gt;repoStructure&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChangeContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Repomix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DirectoryStructure&lt;/span&gt;

    &lt;span class="c"&gt;// 2. Build the prompt asking "Which files do you need?"&lt;/span&gt;
    &lt;span class="n"&gt;phase1Prompt&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SelectFilesForReviewPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repoStructure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prDiff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// 3. Call Bedrock with the Tool Configuration&lt;/span&gt;
    &lt;span class="n"&gt;toolConfig&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;bedrock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BuildSelectFilesTool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BedrockClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Converse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SystemPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phase1Prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toolConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bedrock converse failed: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// 4. Extract structured tool response&lt;/span&gt;
    &lt;span class="n"&gt;toolJSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;bedrock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExtractToolJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to extract tool response: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// 5. Unmarshal into typed struct&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;selectResponse&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Files&lt;/span&gt;     &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"files"`&lt;/span&gt;
        &lt;span class="n"&gt;Reasoning&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;   &lt;span class="s"&gt;`json:"reasoning"`&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unmarshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toolJSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;selectResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"invalid json structure: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// 6. Guard against runaway file selection&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;selectResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AI selected too many files, truncating"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"requested"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;selectResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Files&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;selectResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;selectResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Files&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;selectResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the defensive programming: timeouts with &lt;code&gt;context.WithTimeout&lt;/code&gt;, explicit error wrapping with &lt;code&gt;fmt.Errorf&lt;/code&gt;, structured logging with &lt;code&gt;slog.Warn&lt;/code&gt;, and the guardrail cap that truncates oversized file selections. When you're running this on every PR across 30 repositories, you need to assume the AI will occasionally hallucinate, the network will flake, or someone will open a 500-file refactor. The first week we deployed this, the AI tried to select 47 files for a 3-line CSS change. Without the cap, we would have blown our context window budget. Production systems need guardrails.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Packing Context (The Repomix Magic)
&lt;/h3&gt;

&lt;p&gt;Now that we have the &lt;code&gt;selectedFiles&lt;/code&gt; list, we don't need to re-clone anything. We have the Repomix data structure in memory (or cached). We just filter it to extract the context we need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// BuildFileContext filters RepomixData to selected files and returns a packed string.&lt;/span&gt;
&lt;span class="c"&gt;// Note: This example shows XML format for illustration. The production version outputs&lt;/span&gt;
&lt;span class="c"&gt;// JSON using structured serialization for better type safety and easier parsing.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Runner&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;BuildFileContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;RepomixData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;selectedFiles&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// 1. Create a lookup map for O(1) checks&lt;/span&gt;
    &lt;span class="n"&gt;keep&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;selectedFiles&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// 2. Filter the pre-packed files from Repomix&lt;/span&gt;
    &lt;span class="c"&gt;// We assume data.Files contains the parsed output from Repomix&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;filteredFiles&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;repomix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Files&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;filteredFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filteredFiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filteredFiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"no relevant files found in context"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// 3. Reconstruct the XML/Context string&lt;/span&gt;
    &lt;span class="c"&gt;// This replicates Repomix's output format but only for the subset&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Builder&lt;/span&gt;
    &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;repository&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;filteredFiles&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;file path=%q&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;%s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/file&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;/repository&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This transforms a 50MB repo into a precise 10KB context window containing &lt;em&gt;only&lt;/em&gt; the files that matter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Phase 2 - The Review
&lt;/h3&gt;

&lt;p&gt;Finally, I run the actual review. I feed it the focused context and ask for specific actionable feedback.&lt;/p&gt;

&lt;p&gt;Crucially, we map the response to a Go struct that matches GitHub's comment API structure. We also make sure to handle errors gracefully here too—no &lt;code&gt;_&lt;/code&gt; assigns allowed in production!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;CodeReviewAnalyzer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;performCodeReview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fileContext&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prDiff&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;CodeReviewResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// 1. Build the prompt with the focused context&lt;/span&gt;
    &lt;span class="n"&gt;phase2Prompt&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ReviewPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fileContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prDiff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// 2. Call Bedrock with the "perform_code_review" tool&lt;/span&gt;
    &lt;span class="n"&gt;toolConfig&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;bedrock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BuildCodeReviewTool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BedrockClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Converse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SystemPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phase2Prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toolConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// 3. Unmarshal the structured response&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;reviewData&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Summary&lt;/span&gt;        &lt;span class="kt"&gt;string&lt;/span&gt;       &lt;span class="s"&gt;`json:"summary"`&lt;/span&gt;
        &lt;span class="n"&gt;KeyConcerns&lt;/span&gt;    &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;KeyConcern&lt;/span&gt; &lt;span class="s"&gt;`json:"key_concerns"`&lt;/span&gt;
        &lt;span class="n"&gt;InlineComments&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;FilePath&lt;/span&gt;      &lt;span class="kt"&gt;string&lt;/span&gt;      &lt;span class="s"&gt;`json:"file_path"`&lt;/span&gt;
            &lt;span class="n"&gt;Line&lt;/span&gt;          &lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="s"&gt;`json:"line"`&lt;/span&gt;
            &lt;span class="n"&gt;Body&lt;/span&gt;          &lt;span class="kt"&gt;string&lt;/span&gt;      &lt;span class="s"&gt;`json:"body"`&lt;/span&gt;
            &lt;span class="n"&gt;SuggestedCode&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;      &lt;span class="s"&gt;`json:"suggested_code"`&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="s"&gt;`json:"inline_comments"`&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;toolJSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;bedrock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExtractToolJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to extract tool json: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unmarshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toolJSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;reviewData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to unmarshal review data: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Now we have structured data to send to GitHub API!&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;processReviewData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reviewData&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result? I get a list of &lt;code&gt;InlineComments&lt;/code&gt; that I can loop over and POST directly to the GitHub Pull Request API. No regex parsing, no "I hope the AI formatted the markdown block correctly." Just typed data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Use an Existing Tool?
&lt;/h2&gt;

&lt;p&gt;Fair question. Tools like CodeRabbit and Codeium Enterprise exist and they are fantastic. But here's the deal: none of them worked with our specific constraints.&lt;/p&gt;

&lt;p&gt;The primary driver was infrastructure. We run GitHub Enterprise on-premise behind a strict VPN. While many SaaS tools claim enterprise support, they typically expect your git instance to be reachable from the public internet (or require complex tunnel setups that our security team vetoed immediately). We simply couldn't pipe our repositories into a cloud SaaS.&lt;/p&gt;

&lt;p&gt;Once we accepted we had to build, we realized four massive benefits that validated the decision:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Control&lt;/strong&gt;: We could integrate deeply with our internal knowledge base. I'm talking about feeding the AI our specific architecture docs and Go style guides.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt;: SaaS tools usually charge per-seat ($20-50/user/month). I wanted per-PR pricing I could control. If we have a quiet month, I pay less.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data residency&lt;/strong&gt;: This was non-negotiable. With a custom build using Bedrock in our VPC, zero code ever leaves our controlled environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customization&lt;/strong&gt;: We have specific architectural rules (like "no direct DB calls in handlers"). Phase 2 adds these as custom fitness functions—something no off-the-shelf tool offered.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you can use a SaaS tool, use it. This post is for when you can't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Save (And Why This Matters)
&lt;/h2&gt;

&lt;p&gt;Catching syntax errors is cute. Catching logic bombs is why we built this.&lt;/p&gt;

&lt;p&gt;Two weeks after deploying Archbot, it caught a bug that would have turned a minor network hiccup into a SEV-1 outage.&lt;/p&gt;

&lt;p&gt;A Senior Engineer was implementing a "Feature Flag" loader. The pattern is common: fetch configuration from a remote service, but keep a local fallback for safety. The goal is resilience.&lt;/p&gt;

&lt;p&gt;Here is a simplified version of the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ConfigLoader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;GetFeatureFlag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// 1. Try to fetch from the remote config service&lt;/span&gt;
    &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remote&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
         &lt;span class="c"&gt;// The "Safe" move? Fail fast.&lt;/span&gt;
         &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"remote config failed: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// 2. If remote returns nothing (e.g. 404), fallback to local defaults&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do you see it? It looks clean. It compiles. It passes the linter. It follows the standard Go idiom: &lt;code&gt;if err != nil { return err }&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But Archbot saw the architectural implication:&lt;/p&gt;

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

&lt;p&gt;The engineer paused. "Oh."&lt;/p&gt;

&lt;p&gt;We had inadvertently built a "kill switch" into our startup sequence. If the config service blinked, our main application would crash. The fallback code—written specifically for that scenario—was unreachable code during an outage.&lt;/p&gt;

&lt;p&gt;That's when the team stopped treating Archbot like a toy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Impact: Shifting Code Review from "Find Problems" to "Verify Intent"
&lt;/h2&gt;

&lt;p&gt;Now, don't get me wrong—catching production bugs is the headline reason we built this. That's what gets management buy-in. That's what makes the case for the infrastructure investment.&lt;/p&gt;

&lt;p&gt;But here's what actually changed day-to-day: &lt;strong&gt;Archbot became the first filter on every PR, freeing human reviewers to focus on architecture instead of syntax.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Think about the traditional code review process. You open a PR. A senior engineer context-switches from their deep work, loads your branch, scans 15 files looking for imports, error handling, edge cases, architectural violations—all while trying to remember what &lt;em&gt;you&lt;/em&gt; were trying to accomplish in the first place. It's exhausting. By the time they get to the interesting architectural question ("Should this live in the service layer or the handler?"), their brain is already fried from scanning for nil checks.&lt;/p&gt;

&lt;p&gt;Archbot inverts that. It handles the cognitive load of the "scan for obvious issues" phase. It's not replacing human review—it's &lt;strong&gt;augmenting&lt;/strong&gt; it.&lt;/p&gt;

&lt;p&gt;When Archbot leaves a comment, it's doing one of two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Spotlighting complexity:&lt;/strong&gt; "Hey, this function has three nested error paths and I can't tell if all branches are covered." Even when Archbot is &lt;em&gt;wrong&lt;/em&gt;, it's usually highlighting code that deserves a second look.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catching the mundane:&lt;/strong&gt; Missing error handling, unreachable fallback logic, off-by-one bugs in loops—the stuff that's easy to miss when you're mentally juggling architectural intent.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result? Human reviewers spend less time hunting for bugs and more time asking the questions that matter: &lt;em&gt;"Is this the right design? Does this scale? Is there a simpler way?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And yes, Archbot nitpicks sometimes. It'll flag a variable name it doesn't like. It'll suggest a refactor that doesn't make sense in context. But the team learned quickly: &lt;strong&gt;if Archbot comments on it, it's probably worth at least reading twice.&lt;/strong&gt; Complex code attracts AI attention. That's actually a feature, not a bug.&lt;/p&gt;

&lt;p&gt;The shift we saw was subtle but real. Code review went from "find all the problems" to "verify the architectural intent." Archbot doesn't have architectural taste—but it frees up the humans who do to actually apply it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;The difference was night and day.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Naive Approach:&lt;/strong&gt; Hallucinations, missed context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hammering:&lt;/strong&gt; Slow, rate-limited.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monolith (Full Repo):&lt;/strong&gt; Context overflow, expensive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two-Phase Archbot:&lt;/strong&gt; Fast, accurate, cheap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What we're seeing:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Catching production-impacting logic bugs before merge&lt;/li&gt;
&lt;li&gt;Identifying missing test coverage in critical paths&lt;/li&gt;
&lt;li&gt;High acceptance rate for comments with minimal false positives&lt;/li&gt;
&lt;li&gt;Drastically lower API costs compared to per-seat SaaS pricing&lt;/li&gt;
&lt;li&gt;Zero data leaked outside our VPC&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But catching bugs is only half the battle. The next evolution is &lt;strong&gt;Architecture Fitness Functions&lt;/strong&gt; — teaching the AI to enforce our specific architectural rules (like "no direct DB calls in handlers" or "use circuit breakers for external APIs"). That's a topic for another day.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>codereview</category>
      <category>go</category>
      <category>productivity</category>
    </item>
    <item>
      <title>You are a Senior Engineer, Mastering Software Architecture and Design (Part 2)</title>
      <dc:creator>Alexandre Amado de Castro</dc:creator>
      <pubDate>Wed, 15 May 2024 03:24:05 +0000</pubDate>
      <link>https://forem.com/alexandreamadocastro/you-are-a-senior-engineer-mastering-software-architecture-and-design-part-2-549k</link>
      <guid>https://forem.com/alexandreamadocastro/you-are-a-senior-engineer-mastering-software-architecture-and-design-part-2-549k</guid>
      <description>&lt;p&gt;Hey there, Senior Engineer! You've reached a significant milestone in your career journey, and now it's time to chart the course for what's next. In this blog post, we're diving deep into a crucial aspect of seniority: mastering software architecture and design.&lt;/p&gt;

&lt;p&gt;These skills aren't just nice-to-haves they're essential for staying sharp and competitive in today's tech landscape. So, grab your coffee and get ready to explore the ins and outs of software architecture and design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking up new skills: Software architecture and design
&lt;/h2&gt;

&lt;p&gt;To continue evolving your software design and architecture skills, there are two key pillars you need to focus on, and one helpful "shortcut." First and foremost, you should always make it a priority to continue studying and learning in this field. Technology is constantly evolving, and staying up-to-date with the latest tools, techniques, and approaches is essential to remaining competitive in your career.&lt;/p&gt;

&lt;p&gt;Additionally, it's important to understand that design and architecture go hand in hand. You can't excel at one without a good understanding of the other. If you only focus on design, you might miss critical architecture considerations. On the other hand, if you only focus on architecture, you might end up over-architecting and neglecting the importance of good design.&lt;/p&gt;

&lt;p&gt;Learning software architecture and design can be a challenge, especially since it's not as straightforward as learning a new coding language. As the saying goes, software architecture and design are the things you can't simply Google. This means that you'll need to rely on two main sources for learning:&lt;/p&gt;

&lt;h2&gt;
  
  
  📕 Books
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc5v6j0yrsinfeie22g9s.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc5v6j0yrsinfeie22g9s.jpeg" alt="Gopher reading a book." width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When it comes to these topics, there's no substitute for diving deep into the subject matter. While video tutorials can be helpful, books are the raw source that will give you the depth of knowledge you need to truly understand these complex topics. If you're looking for a good place to start, I highly recommend checking out these books:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://amzn.to/3UAChYK" rel="noopener noreferrer"&gt;"Head First Design Patterns" by Eric Freeman and Elisabeth Robson&lt;/a&gt; is a friendly and engaging introduction to design patterns and object-oriented programming. This book presents the material in a fun and approachable way, making it easy to understand and retain.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://amzn.to/3UJsQYa" rel="noopener noreferrer"&gt;"Fundamentals of Software Architecture: An Engineering Approach" by Mark Richards and Neal Ford&lt;/a&gt; covers the essential principles of software architecture and provides practical guidance for designing scalable and maintainable systems. The authors break down complex topics into easy-to-understand explanations and provide real-world examples to illustrate their points.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://amzn.to/3WrZ4Ze" rel="noopener noreferrer"&gt;"Refactoring: Improving the Design of Existing Code" by Martin Fowler&lt;/a&gt; is a classic book that every software developer should read. It teaches you how to improve the design of existing code by making small, focused changes. While this book is a challenging read, it's well worth the effort if you want to improve your design skills.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🗞️ News
&lt;/h2&gt;

&lt;p&gt;Keeping up-to-date with the latest technologies is critical, as they will undoubtedly shape the future of software engineering. For example, Kubernetes, which was introduced less than a decade ago, has become the default choice for many in the industry. Therefore, it's crucial to stay informed about the latest tech news to remain relevant and competitive.&lt;/p&gt;

&lt;p&gt;When seeking out news, focus on topics that are specific to the technologies you want to learn, such as Golang, Kubernetes, and Microservices. Additionally, broad technology trends will give you a better understanding of the bigger picture, enabling you to anticipate and prepare for future developments. By staying on top of tech news, you'll be exposed to innovative ideas and emerging trends that will help you think outside the box and stay ahead of the curve. Some of my personal sources for news are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Newsletters:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.freecodecamp.org/news/tag/newsletter/" rel="noopener noreferrer"&gt;freeCodeCamp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tldr.tech/" rel="noopener noreferrer"&gt;TLDR Newsletter - A Byte-Sized Daily Tech Newsletter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://go.libhunt.com/newsletter" rel="noopener noreferrer"&gt;Awesome Go&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Coding Blogs:

&lt;ul&gt;
&lt;li&gt;Like mine!&lt;/li&gt;
&lt;li&gt;&lt;a href="https://martinfowler.com/" rel="noopener noreferrer"&gt;Martin Fowler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/"&gt;Dev.to&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Youtube Channels:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/@Fireship" rel="noopener noreferrer"&gt;Fireship&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/@TechLinked" rel="noopener noreferrer"&gt;TechLinked&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Practice, practice, and more practice
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa6h2gune0tv3a46sx34e.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa6h2gune0tv3a46sx34e.jpeg" alt="Gopher practicing in front of a computer." width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The other pillar, as you probably already guessed, is experience. You must expose yourself to an ever-growing list of new things.&lt;/p&gt;

&lt;p&gt;If you're not feeling uncomfortable, then you're doing it wrong. Every day should be a challenge, where you feel like you have no idea how to do what you need to do.&lt;/p&gt;

&lt;p&gt;But with time, you will realize that you can do it and be proud of yourself. Then, you'll be scared of the next day's challenge, and the cycle continues. A few good ways to gain experience in software architecture and design include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you have the bandwidth, work on side projects. Even if they never make it into production or never become a thing, building something outside of your main job can help you explore technologies that you wouldn't have the opportunity to work with otherwise. For instance, you could play with Rust if you're into that.&lt;/li&gt;
&lt;li&gt;Ask your manager to move you to different projects within your team. This will enable you to work on different parts of the system and expose you to different types of problems and solutions.&lt;/li&gt;
&lt;li&gt;Participate in tech exchange programs. You'll learn a lot from your peers, and it's an excellent opportunity to share your knowledge and skills.&lt;/li&gt;
&lt;li&gt;Write documentation about systems you don't own but that your team uses. This will help you gain a deeper understanding of how the system works and how different components interact with one another.&lt;/li&gt;
&lt;li&gt;Work on a lot of proofs-of-concept (POCs). This will allow you to try out different approaches and experiment with new technologies and methodologies.&lt;/li&gt;
&lt;li&gt;Try to solve problems that don't need solving, just to learn a new thing. For example, you could explore how to re-write an application using NoSQL DB instead of a relational one, even if it's not necessary. This will help you learn new things and expand your skill set.&lt;/li&gt;
&lt;li&gt;Contribute to Open Source!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By embracing these experiences, you'll be able to learn new things, expand your skill set, and build the confidence to take on more significant challenges.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lowering the problem level
&lt;/h2&gt;

&lt;p&gt;Finally, let's discuss the "shortcut" for tackling complex problems and concepts. By breaking them down into smaller, more manageable concepts, you can better understand and build upon them. For example:&lt;/p&gt;

&lt;p&gt;Let's say you are feeling lost in a project that requires you to use Kafka but have no idea what Kafka is or what it's for. Don't worry, you're not alone. Kafka is a distributed system consisting of servers and clients communicating through a high-performance TCP network protocol.&lt;/p&gt;

&lt;p&gt;But if that sounds too technical, let's simplify it further: think of Kafka as a topic that functions like a queue, with multiple consumers. Okay, now we're getting somewhere.&lt;/p&gt;

&lt;p&gt;Further simplifying: Kafka is essentially a database full of records called messages that you can consume in order. With this understanding, you can now start building on top of it. For instance, knowing that Kafka is a database, you can begin exploring key concepts that databases have and use that to further your understanding of Kafka. Don't be intimidated by Kafka or any other technology.&lt;/p&gt;

&lt;p&gt;Break it down into smaller, more understandable pieces, and you'll be well on your way to becoming a master.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep learning, keep growing, keep updated
&lt;/h2&gt;

&lt;p&gt;Becoming a senior engineer is an outstanding achievement that you should be proud of. However, it's not the end of the road. The journey continues, and there's always more to learn and explore. As a senior engineer, you should focus on developing both technical depth and breadth to stay adaptable and versatile.&lt;/p&gt;

&lt;p&gt;Additionally, understanding that software design and architecture go hand in hand is crucial for staying competitive in your career. To achieve this, you can rely on books and other sources to continue learning and deepening your understanding of these complex topics. Remember, staying up-to-date with the latest tools, techniques, and approaches is essential to remain competitive in your career.&lt;/p&gt;

&lt;p&gt;If you're seeking more personalized guidance or mentoring, I’m here to help. You can easily book a time with me &lt;a href="https://cal.com/alexandrecastrotech/mentoring" rel="noopener noreferrer"&gt;here&lt;/a&gt; or through the menu on the left — let’s tackle your challenges together!&lt;/p&gt;

</description>
      <category>mentorship</category>
      <category>career</category>
      <category>softwareengineering</category>
      <category>senior</category>
    </item>
    <item>
      <title>You are a Senior Engineer, now what? (Part 1)</title>
      <dc:creator>Alexandre Amado de Castro</dc:creator>
      <pubDate>Wed, 08 May 2024 03:15:39 +0000</pubDate>
      <link>https://forem.com/alexandreamadocastro/you-are-a-senior-engineer-now-what-part-1-4bih</link>
      <guid>https://forem.com/alexandreamadocastro/you-are-a-senior-engineer-now-what-part-1-4bih</guid>
      <description>&lt;p&gt;First of all, congratulations! You’ve earned the rank of Senior Engineer. That's no small feat, and you should be proud of your hard work and dedication.&lt;/p&gt;

&lt;p&gt;Now, you might be wondering, "What's next?" This is not only a common question but also one that I frequently get asked. As a Principal Engineer, I have the great opportunity to mentor many seniors and tech leads. That's precisely why I'm writing this blog post—to extend my personal observations to more people who are trying to navigate their next steps in their technology career.&lt;/p&gt;

&lt;p&gt;But the truth is, becoming a senior engineer is only the beginning of a new chapter in your professional journey. So, there's no need to be afraid! I'm here to help, I will delve into some insights on how you can continue to thrive and achieve even greater success as a Senior Engineer. Let's dive in! 👏&lt;/p&gt;

&lt;p&gt;Now that you are a senior, the junior days are over, does that mean that you should know everything? Absolutely not. Being a senior is about knowing how much you don't know. However, that also means you should be constantly seeking more technical knowledge in both depth and breadth, but what... what does that mean?&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Depth and Breadth
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx9jtcg28z2zy9lqovbdt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx9jtcg28z2zy9lqovbdt.png" alt="Pyramid with technical dept at the top and technical breadth at the bottom." width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Technical Depth
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Technical depth&lt;/strong&gt; refers to a software engineer's deep understanding of a specific area or technology. This means that they have extensive knowledge and expertise in a particular programming language, framework, or tool, which allows them to optimize it, solve complex problems related to it, and work efficiently in that area.&lt;/p&gt;

&lt;p&gt;Your technical depth, includes for example, your main programming language, the packages you use the most, what package manager you use, what frameworks you use, what databases you use, what cloud provider you use, etc. This is the area where you are the most comfortable and where you can solve problems the fastest!&lt;/p&gt;

&lt;p&gt;For example, a software engineer who specializes in Java may have in-depth knowledge of the language, the Spring framework, and deployment on Tomcat in various configuration scenarios. This enables them to quickly and efficiently troubleshoot issues and optimize not only their work, but also the team's work and sometimes even assist company wise with the general developer experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Technical Breadth
&lt;/h3&gt;

&lt;p&gt;On the other hand, &lt;strong&gt;technical breadth&lt;/strong&gt; refers to a software engineer's broad understanding of multiple areas or technologies. They have a good understanding of various programming languages, frameworks, and tools, and can work on different projects that require different skills and knowledge.&lt;/p&gt;

&lt;p&gt;I like to explain technical breadth as the technologies you know exist, you know how they work, what they are good for, but you never had the opportunity to use them directly on your day-to-day.&lt;/p&gt;

&lt;p&gt;For example, if you are a Javascript engineer, you know there are a lot of different libraries and frameworks, each with its own specific use case. You know what they are good for, you know what problem they solve, you never had the opportunity to use them, but you could pick it up and start using it in a small time frame if the right opportunity arises.&lt;/p&gt;

&lt;p&gt;While technical depth and breadth are distinct, they're not mutually exclusive. In fact, the most successful senior engineers have both. They have a strong foundation of technical depth in one or more areas, which enables them to tackle complex problems and optimize their work.&lt;/p&gt;

&lt;p&gt;At the same time, they also have technical breadth, which allows them to adapt to new technologies and projects, and collaborate effectively with colleagues from different technical backgrounds. So, strive to develop both technical depth and breadth, and become a well-rounded and versatile senior engineer.&lt;/p&gt;

&lt;p&gt;Besides technical skills, there are also plenty of soft skills that you'll need to master over time. But, I'll save that conversation for another blog post. For now, let's focus on how you can continue to improve your technical skills and stay up-to-date throughout your career.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep grinding
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjrgczmtcdabc0y4nz1ej.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjrgczmtcdabc0y4nz1ej.jpg" alt="A determined gopher grinds away at work, intently focused at a cluttered desk in a charming black and white sketch." width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Okay, so here's the deal: once you hit that senior level, the path to seniority is pretty similar across the board, unless you decide to transition into a management role. It's like leveling up in a video game - you're always striving to get better and better, but the gameplay stays pretty consistent.&lt;/p&gt;

&lt;p&gt;Now, don't get me wrong - when you start taking on tech lead, staff engineer, or principal engineer roles, you're going to need to add some new soft skills to your toolbelt (I will write another blog post on this). Things like leadership, mentorship, and influence become increasingly important. But when it comes to hard skills, the path stays pretty consistent. So let's say that reaching principal engineer status just makes you a "more senior senior" - you're still building on the same foundation of technical skills that got you to where you are.&lt;/p&gt;

&lt;p&gt;Stay tuned for Part 2 of our blog series, where we'll dive into one of the most crucial topics for advancing your seniority: software architecture and design.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>mentorship</category>
      <category>seniority</category>
      <category>coding</category>
    </item>
    <item>
      <title>Kafka Retries: Implementing Consumer Retry with Go</title>
      <dc:creator>Alexandre Amado de Castro</dc:creator>
      <pubDate>Thu, 02 Feb 2023 00:30:58 +0000</pubDate>
      <link>https://forem.com/alexandreamadocastro/kafka-retries-implementing-consumer-retry-with-go-3l6m</link>
      <guid>https://forem.com/alexandreamadocastro/kafka-retries-implementing-consumer-retry-with-go-3l6m</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxljkld5x97tb1lj60i4c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxljkld5x97tb1lj60i4c.png" alt="Three kafka topics with messages inside of it and a Golang gopher running to retry." width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You don’t “need retries in Kafka” until the day one of your handlers starts failing and you’re forced into a choice: block consumption (and watch lag climb) or keep consuming and retry somewhere else.&lt;/p&gt;

&lt;p&gt;This post is about one very pragmatic approach: &lt;strong&gt;commit the Kafka offset even when processing fails&lt;/strong&gt;, then push the failed message into a Go retry queue. Kafka keeps moving, and your application owns the retry policy.&lt;/p&gt;

&lt;p&gt;Quick context (assuming you already speak Kafka): consumer groups split partitions across consumers for parallelism. Offsets are committed per partition. In this approach, an offset commit means “Kafka can move on,” not necessarily “side effects succeeded.”&lt;/p&gt;

&lt;p&gt;If you’re looking for the other style of “consumer retry” (don’t advance the offset until the handler succeeds), that’s a different design with different tradeoffs. This post is explicitly about keeping consumption moving and absorbing failures in a retry pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working with examples
&lt;/h2&gt;

&lt;p&gt;Let's create an example. Imagine you work for a company named ACME, and you have a Kafka topic that receives a new message every time a new customer is created. This customer needs to receive an email saying that their account is fully created and that they need to verify their email.&lt;/p&gt;

&lt;p&gt;When we read something like "when something happens" do "something else," always think about events! Events, in Kafka, are messages!&lt;/p&gt;

&lt;p&gt;We're going to create a microservice that will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Receive this new customer message from Kafka.&lt;/li&gt;
&lt;li&gt;Compose a nice email.&lt;/li&gt;
&lt;li&gt;Send it to the customer.&lt;/li&gt;
&lt;li&gt;Add a new message to another topic saying that the message was sent!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's diagram that for more visibility:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdx9q7cmbzbxsrelvrvwu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdx9q7cmbzbxsrelvrvwu.png" alt="Diagram to visualize the concept above." width="800" height="656"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where are the retries?
&lt;/h2&gt;

&lt;p&gt;Cool, but, this is a blog post about retries right? Where are the retries in all of it?&lt;/p&gt;

&lt;p&gt;Well, any part of the processing can go wrong, and we don't want our customers to miss out on their account creation emails, do we? So, let's talk about a few things that could go wrong and how we can handle them with retries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The email template database might be down. No problem, we'll just keep trying to connect until it's back up.&lt;/li&gt;
&lt;li&gt;The template persisted inside of it might be invalid. We'll check for this and alert the team to update it if needed.&lt;/li&gt;
&lt;li&gt;The SMTP server might be down or drop a message. We'll keep trying to send the email until it goes through.&lt;/li&gt;
&lt;li&gt;The application might compose the SMTP message badly and the SMTP server reject it. We'll catch this error and fix the composition before trying again.&lt;/li&gt;
&lt;li&gt;Producing the message to the other Kafka topic might return an error on Kafka's server side. We'll keep trying until it's successfully sent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some of the errors mentioned may require code or data modifications to resolve, such as updating a template or correcting the way the application composes an SMTP message. These types of errors may not be suitable for retries as they may require manual intervention.&lt;/p&gt;

&lt;p&gt;On the other hand, other errors such as temporary database downtime, flaky message production, or overloaded SMTP servers can potentially be resolved by retrying the operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unlocking Kafka Consumer Retries
&lt;/h2&gt;

&lt;p&gt;This comes up constantly in event-driven systems: Kafka gives you a great log, but it doesn’t give you a “retry policy” button.&lt;/p&gt;

&lt;p&gt;I’ve tried a bunch of architectures for retrying message processing. None is perfect, but one is consistently the easiest to operate when your top priority is “keep consuming,” as long as you’re explicit about the semantics trade.&lt;/p&gt;

&lt;p&gt;Before diving into the solution, let me share some background on my previous attempts.&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS SQS: A Great Option, But With Limitations
&lt;/h3&gt;

&lt;p&gt;As a strong advocate for AWS, my first thought when considering retries was AWS SQS.&lt;/p&gt;

&lt;p&gt;Amazon Simple Queue Service (SQS) is a fully managed message queuing service that enables the decoupling and scaling of microservices, distributed systems, and serverless applications. It comes with built-in retry behavior (visibility timeout + redrive policies + DLQ) and supports a maximum message size of 256 KB.&lt;/p&gt;

&lt;p&gt;Now, don’t get me wrong: the grown-up solution is the pointer pattern — put the real payload in object storage (like S3) and send a small pointer through SQS.&lt;/p&gt;

&lt;p&gt;That pattern works, but it adds moving parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You’ve introduced a second persistence system.&lt;/li&gt;
&lt;li&gt;You need retention/lifecycle for blobs.&lt;/li&gt;
&lt;li&gt;You need consistency between “pointer message” and “payload object”.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also, quick correction on Kafka: Kafka message size is configurable (defaults vary, but ~1MB is common). You &lt;em&gt;can&lt;/em&gt; tune limits, but you still need an explicit payload contract, or producers will eventually surprise you in production.&lt;/p&gt;

&lt;p&gt;So while SQS is a great option, it wasn’t the best fit for what I wanted here: keep the entire pipeline “Kafka-native” without introducing a second storage path just to make retries easier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kafka
&lt;/h3&gt;

&lt;p&gt;Yes, that's exactly it, since SQS isn't a suitable solution, why not use Kafka itself?&lt;/p&gt;

&lt;p&gt;I attempted to create an internal application Kafka topic that would only be used by the application, where the application would push messages to it, and in case of a failure, it would enqueue the message again with a new count attribute in the header. Let me diagram that to make it more clear:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8uzvss6puqsey2plv367.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8uzvss6puqsey2plv367.png" alt="Diagram to visualize the concept above." width="800" height="810"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While this approach does work, it has some significant downsides. Firstly, setting up Kafka topics is not as straightforward as SQS, creating multiple topics can be somewhat cumbersome, and dealing with all the consumers and producers within the code can become quite messy.&lt;/p&gt;

&lt;p&gt;Kafka also doesn’t give you a first-class “retry this message N times” feature at the broker level, and that’s mostly a good thing: Kafka can’t know whether your side effects are safe to repeat. Retries are an application-level decision tied to offsets, commits, backpressure, and idempotency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Databases
&lt;/h3&gt;

&lt;p&gt;Desperate Times Call for Desperate Measures, right?&lt;/p&gt;

&lt;p&gt;As a last resort, I turned to use databases for message retries. Modern SQL databases have LOCKING mechanisms that can be leveraged to use a table as a queue.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx1gqi1ysiddt20vm1k8s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx1gqi1ysiddt20vm1k8s.png" alt="Diagram to visualize the concept above." width="800" height="619"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To my surprise, this approach worked! However, it's not a perfect solution. Databases are not designed to function as queues, and using them in this way can be a stretch. There are a few downsides to this approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Databases are built for consistency using transactions, which can make them performant, but they will never perform as well as a specialized tool like Kafka. This can easily become a bottleneck in the system.&lt;/li&gt;
&lt;li&gt;Managing all of this code can easily become problematic, and it is not an easy pattern to share across an entire company.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  So, what now? Memory
&lt;/h3&gt;

&lt;p&gt;After trying all of the above solutions and being unsatisfied with the results, I decided to look at how other companies and frameworks handle retries.&lt;/p&gt;

&lt;p&gt;One framework that stood out to me is Spring, an open-source Java framework that provides a comprehensive programming and configuration model for building modern enterprise applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Spring way
&lt;/h3&gt;

&lt;p&gt;One of the modules within Spring is Spring Kafka, which provides several ways to handle retries when consuming messages. These include using the &lt;code&gt;RetryTemplate&lt;/code&gt; and &lt;code&gt;RetryCallback&lt;/code&gt; interfaces from the Spring Retry library to define a retry policy, using the &lt;code&gt;@KafkaListener&lt;/code&gt; annotation to configure retry behavior on a per-method basis, or using AOP to handle retries by using a &lt;code&gt;RetryInterceptor&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Spring Kafka allows developers to choose the approach that best fits their use case, whether it is a global retry policy or a more fine-grained per-method retry configuration.&lt;/p&gt;

&lt;p&gt;It's been a long time since I worked with Java, and Spring code is far from easy to understand (it's pretty advanced stuff), but after diving deeper into the Spring Kafka framework, I discovered that it implements an error handler using a strategy for handling exceptions thrown by the consumers.&lt;/p&gt;

&lt;p&gt;When an exception that is not a &lt;code&gt;BatchListenerFailedException&lt;/code&gt; (which Spring knows is impossible to retry) is thrown, the error handler will retry the batch of records from memory. This prevents a consumer rebalance during an extended retry sequence and allows for a more elegant solution.&lt;/p&gt;

&lt;p&gt;In my honest opinion, the Spring Kafka framework offers a robust and flexible approach to handling retries that is well worth considering for any enterprise application.&lt;/p&gt;

&lt;p&gt;It provides a range of options for developers to choose from and allows for more fine-grained control over retry behavior, making it a great choice for any organization looking to implement a retry mechanism for their messaging system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inspired by Spring Kafka (but different semantics) in Go
&lt;/h3&gt;

&lt;p&gt;I currently work with (and love ) Go, and, as far as my research went, I didn't find any rewrite of the Spring Kafka and Retry framework in Go, so, let's build our own!&lt;/p&gt;

&lt;p&gt;Here’s the key difference in my implementation compared to Spring Kafka’s approach: I’m not trying to pause a partition and “hold the offset line” until processing succeeds.&lt;/p&gt;

&lt;p&gt;Instead, I commit the offset and move on, then retry in the background.&lt;/p&gt;

&lt;p&gt;My approach includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating a consumer that reads from the topic.&lt;/li&gt;
&lt;li&gt;Creating a Go channel that handles retries.&lt;/li&gt;
&lt;li&gt;If a message fails to process, it is sent to the retry channel.&lt;/li&gt;
&lt;li&gt;The main consumer continues to process the main topic while the other messages are retried.&lt;/li&gt;
&lt;li&gt;If retries are exhausted, the message is sent to a persisted DLQ for later investigation.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Warning:&lt;br&gt;
This pattern intentionally decouples Kafka offsets from processing success.&lt;/p&gt;

&lt;p&gt;If you commit a message and your process crashes before the retry succeeds, Kafka will not redeliver that message. With an in-memory Go channel, that means a restart can drop “in-flight retries.”&lt;/p&gt;

&lt;p&gt;If you need stronger guarantees than that, the retry queue must be durable (retry topic, DB table, external queue) and your handler must be idempotent.&lt;/p&gt;

&lt;p&gt;Also note that retries can happen later than subsequent messages, so you’re trading away strict ordering for throughput.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  When not to use this pattern
&lt;/h4&gt;

&lt;p&gt;If you need strict ordering guarantees, or you must be able to restart at any time without losing “in-flight retries,” don’t use an in-memory retry queue behind an early commit. In those cases, you either need a durable retry queue, or you need an offset-holding strategy (pause/seek and commit only after success).&lt;/p&gt;

&lt;h4&gt;
  
  
  Here’s the full example
&lt;/h4&gt;

&lt;p&gt;This example uses an in-memory Go channel as the retry queue. That keeps Kafka consumption moving, but it also means retries are lost on process restart. In real systems, I usually evolve this into a durable retry queue.&lt;/p&gt;

&lt;p&gt;One more practical note: your retry queue is part of your backpressure story. If it fills up, enqueueing can block and slow consumption. The "keep consuming no matter what" version of this pattern needs an explicit overflow strategy (drop, spill to durable storage, or fail fast into DLQ). Also consider adding jitter to your backoff to avoid retry storms when multiple messages fail simultaneously.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Quick orientation&lt;/strong&gt;: &lt;code&gt;ProcessRetryHandler&lt;/code&gt; is the interface your handler implements — &lt;code&gt;Process&lt;/code&gt; does the work, &lt;code&gt;MoveToDLQ&lt;/code&gt; handles exhausted retries. &lt;code&gt;ConsumerWithRetryOptions&lt;/code&gt; wires everything together. The main loop reads from Kafka and enqueues failures; the goroutine retries them with backoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Semantics (don’t skip this)
&lt;/h2&gt;

&lt;p&gt;This pattern is “Kafka keeps moving,” which means you’re making a deliberate trade: Kafka offsets can advance even when processing hasn’t succeeded yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Kafka guarantees (in this pattern)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you commit before success (or your client auto-commits on read), Kafka won’t redeliver on restart. That’s effectively &lt;em&gt;at-most-once&lt;/em&gt; for your side effects.&lt;/p&gt;

&lt;p&gt;If you’re following along with &lt;code&gt;kafka-go&lt;/code&gt;, note that &lt;code&gt;Reader.ReadMessage&lt;/code&gt; with a &lt;code&gt;GroupID&lt;/code&gt; advances committed offsets independently of whether your handler succeeds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to get stronger guarantees&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want at-least-once for the side effects, the retry queue can’t be in-memory. It needs to be durable (retry topic, DB table, external queue) and your handler needs to be idempotent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency (practical version)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If processing record X twice causes two emails, two charges, or two rows, you’re going to have a bad week. The fix is usually an idempotency key + write-once constraint (or upsert).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Poison pills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A poison pill is a record that will never succeed without code/data changes. This pattern is nice because poison pills don’t block Kafka consumption — they show up as retry storms and DLQ events instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision table
&lt;/h2&gt;

&lt;p&gt;In my head, this “commit early + retry elsewhere” idea has a few maturity levels:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Kafka consumption&lt;/th&gt;
&lt;th&gt;Failure semantics&lt;/th&gt;
&lt;th&gt;Operational complexity&lt;/th&gt;
&lt;th&gt;When I’d pick it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;In-memory retry channel&lt;/td&gt;
&lt;td&gt;Never blocks consumption&lt;/td&gt;
&lt;td&gt;Retries are lost on restart&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Quick wins, low blast radius workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Durable retry queue (Kafka retry topic / external queue)&lt;/td&gt;
&lt;td&gt;Never blocks consumption&lt;/td&gt;
&lt;td&gt;At-least-once is achievable (with idempotency)&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Most production systems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB-backed state machine / outbox&lt;/td&gt;
&lt;td&gt;Never blocks consumption&lt;/td&gt;
&lt;td&gt;Strongest auditability and control&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Complex workflows, compliance, long retries&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  In conclusion
&lt;/h2&gt;

&lt;p&gt;Kafka retries are not a broker feature; they’re a semantics decision.&lt;/p&gt;

&lt;p&gt;This “commit early + retry elsewhere” approach is great when your top priority is keeping Kafka consumption unimpaired. The cost is that you’re now running a second system: your retry pipeline. Treat it like a first-class dependency.&lt;/p&gt;

&lt;p&gt;If I were shipping this pattern, I’d page on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retry queue depth (and time-in-queue).&lt;/li&gt;
&lt;li&gt;Retry exhaustion rate (messages going to DLQ).&lt;/li&gt;
&lt;li&gt;DLQ age (DLQ isn’t success; it’s a promise you’ll look).&lt;/li&gt;
&lt;li&gt;Processing success latency (p95/p99) for the retry worker.&lt;/li&gt;
&lt;li&gt;Consumer lag as a sanity check (it should stay flat even during downstream outages).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dealing with distributed systems challenges or building reliable event-driven architectures? Let's talk.&lt;/p&gt;

</description>
      <category>go</category>
      <category>kafka</category>
      <category>resiliency</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
