<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: pponali</title>
    <description>The latest articles on DEV Community by pponali (@pponali).</description>
    <link>https://dev.to/pponali</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1238066%2F98aafa2c-caaf-4488-9c96-f143321b4acb.png</url>
      <title>DEV Community: pponali</title>
      <link>https://dev.to/pponali</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pponali"/>
    <language>en</language>
    <item>
      <title>I Ran a Health Check on My Claude Code Setup — Here's What Was Actually Slowing It Down</title>
      <dc:creator>pponali</dc:creator>
      <pubDate>Sun, 26 Jul 2026 12:41:18 +0000</pubDate>
      <link>https://dev.to/pponali/i-ran-a-health-check-on-my-claude-code-setup-heres-what-was-actually-slowing-it-down-6oa</link>
      <guid>https://dev.to/pponali/i-ran-a-health-check-on-my-claude-code-setup-heres-what-was-actually-slowing-it-down-6oa</guid>
      <description>&lt;p&gt;I've been running Claude Code against a mid-size monorepo (a Node.js + Spring Boot + Flutter + Kotlin agricultural platform, if you're curious — five components, a strangler-fig microservices migration in progress) for a few months now, wired up with a fairly aggressive set of hooks: repo memory that indexes symbols on every edit, a Postgres-backed transcript store for every prompt and tool call, automatic RCA triggers when a subagent fails, feedback capture on session end. It's a lot of automation, and automation you don't audit eventually silts up.&lt;/p&gt;

&lt;p&gt;So I ran &lt;code&gt;/doctor&lt;/code&gt; — a setup-health check that treats the Claude Code installation itself as a system worth debugging: duplicate installs, dead config, unused extensions burning context, and — the part that turned out to matter most — hooks that run on every single turn and have quietly gotten slow.&lt;/p&gt;

&lt;p&gt;This post is the account of that audit: what it found, what surprised me, and the one bug that was costing 10+ seconds on every conversation turn for a reason that had nothing to do with the code doing the actual work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother auditing a CLI tool's config
&lt;/h2&gt;

&lt;p&gt;Hooks in Claude Code are shell commands the harness runs automatically around specific events — before a tool call, after a tool call, when a prompt comes in, when the model finishes responding. They're how you bolt persistent memory, telemetry, or guardrails onto an otherwise stateless agent loop. Mine do things like: re-index the codebase's symbol graph after an edit, capture the git diff for feature attribution, and log every prompt/response pair to a database for a later confidence-scoring pass.&lt;/p&gt;

&lt;p&gt;The problem with hooks specifically is that they're invisible by design. They don't show up in the conversation. They just... run, in the background, adding latency to every turn, and unless you go looking, a hook that's degraded from "instant" to "12 seconds" reads to the user as "the model got slower today" — nobody thinks to blame the plumbing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the audit actually checks
&lt;/h2&gt;

&lt;p&gt;The scan pulls from local, read-only sources only — no telemetry upload, nothing sent anywhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Installation health.&lt;/strong&gt; Are there duplicate installs (native launcher vs. an old npm-global leftover)? Does the resolved binary match what the config thinks is installed? Any broken or colliding agent definition files?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usage signal for extensions.&lt;/strong&gt; Every skill, plugin, and MCP server (a connection to an external tool) has either a lifetime usage counter or — for MCP servers, which have none — transcript evidence from recent sessions. Cross-reference both against a window of the 50 most-recently-touched session transcripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checked-in memory file bloat.&lt;/strong&gt; &lt;code&gt;CLAUDE.md&lt;/code&gt; files get loaded into every session's context. Anything in them a fresh session could reconstruct by reading the code (a docker-compose port table, standard &lt;code&gt;flutter build&lt;/code&gt; commands already in &lt;code&gt;pubspec.yaml&lt;/code&gt;) is dead weight paid every session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hook performance.&lt;/strong&gt; Aggregate &lt;code&gt;durationMs&lt;/code&gt; per hook, per event, from the transcript's attachment records. Flag anything that's both frequent (fires every prompt or every tool call) and slow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission friction.&lt;/strong&gt; Which safe, read-only commands keep getting denied and re-prompted for, that could be pre-approved once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I ran it, then acted on the findings, which is the part worth walking through.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was actually wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A duplicate install.&lt;/strong&gt; &lt;code&gt;which -a claude&lt;/code&gt; turned up two resolutions — the native launcher I actually use, and a stale &lt;code&gt;@anthropic-ai/claude-code@2.1.76&lt;/code&gt; sitting in an npm-global prefix, two minor versions behind. Harmless on its own, but it's the kind of thing that causes "wait, which version am I even running" confusion six months later. Removed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One broken, silently-dead agent file.&lt;/strong&gt; Out of 193 project-level agent definitions, one had no &lt;code&gt;description:&lt;/code&gt; in its frontmatter — which means Claude Code never loads it at all. It also happened to collide in &lt;code&gt;name:&lt;/code&gt; with a working sibling file in the same directory, which is a separate failure mode (when two files in one directory share a &lt;code&gt;name&lt;/code&gt;, the loser is discarded based on directory-read order, which isn't guaranteed stable across machines). Deleted the broken one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Genuinely unused extensions.&lt;/strong&gt; A plugin with zero recorded uses since install. An MCP server (Dropbox) with zero invocations across the whole scan window. Both disabled — reversibly, one command each to bring back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checked-in &lt;code&gt;CLAUDE.md&lt;/code&gt; files carrying content the code already says.&lt;/strong&gt; My Flutter app's memory file listed the full &lt;code&gt;flutter build&lt;/code&gt;/&lt;code&gt;flutter test&lt;/code&gt; command set and a directory tree — both fully derivable by reading &lt;code&gt;pubspec.yaml&lt;/code&gt; and &lt;code&gt;ls lib/&lt;/code&gt;. Same pattern in three other component files: a Kotlin Gradle command list, a Node.js &lt;code&gt;npm run&lt;/code&gt; list, a docker-compose port table. None of it was &lt;em&gt;wrong&lt;/em&gt;, it was just cost paid on every single session for something a few &lt;code&gt;ls&lt;/code&gt;/&lt;code&gt;cat&lt;/code&gt; calls reconstruct for free. Trimmed all four down to the parts that &lt;em&gt;aren't&lt;/em&gt; derivable — setup steps that need an external file, a gotcha about a migration naming convention, a pointer to an external QA test-case spreadsheet. Those stay, because no amount of reading the repo tells you a Google Sheet exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the big one: hook latency.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The hook that was eating 12 seconds a turn
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Stop&lt;/code&gt; hook — the one that fires after every model response — was averaging 12.1 seconds, with a worst case of 113 seconds, across 251 recorded runs in the scan window. The &lt;code&gt;UserPromptSubmit&lt;/code&gt; hook (fires on every message you send) was averaging 5.5 seconds. For context: the rule of thumb is that anything running on every prompt or every tool call should stay under roughly 2 seconds, and even the less-frequent session-boundary hooks (&lt;code&gt;SessionStart&lt;/code&gt;, &lt;code&gt;Stop&lt;/code&gt;) should stay under 10.&lt;/p&gt;

&lt;p&gt;My first assumption, walking in, was that the hook scripts referenced a hardcoded path from when I'd set this up on a different machine (a Mac) and were now silently failing on this Linux box — every &lt;code&gt;python3 /Users/me/project/.claude/hooks/whatever.py&lt;/code&gt; call would just error out fast and move on. That turned out to be half right: I &lt;em&gt;did&lt;/em&gt; find exactly that bug, but in a smaller, unrelated script (a debug-logging hook that was writing to a log path that no longer existed — fast to fail, ~5ms, not the source of the 12-second problem). The actual &lt;code&gt;Stop&lt;/code&gt; and &lt;code&gt;UserPromptSubmit&lt;/code&gt; hooks had already been fixed to use &lt;code&gt;$PWD&lt;/code&gt; and &lt;code&gt;$HOME&lt;/code&gt; instead of a hardcoded path.&lt;/p&gt;

&lt;p&gt;The real cause was simpler and less obvious: the &lt;code&gt;Stop&lt;/code&gt; hook chains six separate calls to a repo-memory CLI, each one invoked as &lt;code&gt;npx -y @invariance/gps &amp;lt;subcommand&amp;gt;&lt;/code&gt;. &lt;code&gt;npx -y&lt;/code&gt; doesn't just run a cached binary — every invocation re-resolves the package and, in the &lt;code&gt;-y&lt;/code&gt; (auto-confirm) case, checks the registry for whether a newer version exists, before it runs anything. Timed cold: &lt;strong&gt;1.6 seconds per call.&lt;/strong&gt; Six of those, sequentially, in one hook, before the turn is considered "done." &lt;code&gt;UserPromptSubmit&lt;/code&gt; chains three more. That's the 12-second average baseline right there — and the 113-second worst case is almost certainly a slow or flaky registry round-trip on top of that, since &lt;code&gt;npx -y&lt;/code&gt;'s freshness check is a live network call every single time, not a cached one.&lt;/p&gt;

&lt;p&gt;The fix was almost embarrassingly small: install the package once, globally, so it's a real binary on &lt;code&gt;$PATH&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @invariance/gps &lt;span class="nt"&gt;--prefix&lt;/span&gt; ~/.npm-global
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then swap every &lt;code&gt;npx -y @invariance/gps &amp;lt;subcommand&amp;gt;&lt;/code&gt; in the hook config for a plain &lt;code&gt;gps &amp;lt;subcommand&amp;gt;&lt;/code&gt;. Same six calls, same behavior — just no per-call registry check and no npm package-resolution overhead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Before:  npx &lt;span class="nt"&gt;-y&lt;/span&gt; @invariance/gps brief &lt;span class="nt"&gt;--root&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   → ~1.6s, network round-trip every &lt;span class="nb"&gt;time
&lt;/span&gt;After:   gps brief &lt;span class="nt"&gt;--root&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;                        → ~0.7s, resolved once, no network
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six calls at 1.6s each versus six at 0.7s each is roughly the difference between a 10-second tax and a 4-second one, on &lt;em&gt;every single turn&lt;/em&gt; — and it eliminates the network-flakiness tail that was almost certainly behind that 113-second outlier. I verified the fix with a direct timing test before and after: 1.601s cold via &lt;code&gt;npx&lt;/code&gt;, 0.740s via the global binary, consistently, across repeated runs.&lt;/p&gt;

&lt;p&gt;It's worth being precise about what that 0.7 seconds still &lt;em&gt;is&lt;/em&gt;, because it's not zero. It's still a fresh Python/Node process spin-up, still a full CLI argument parse, still whatever the tool does internally before it touches the filesystem. Installing globally removes exactly one specific tax — the registry freshness check — and nothing else. If I wanted to go further, the next lever would be collapsing the six sequential subcommands into one long-running call, or making the non-critical ones (&lt;code&gt;suggest&lt;/code&gt;, &lt;code&gt;prune&lt;/code&gt;) asynchronous so the turn doesn't block on them at all. I didn't do that here, on purpose: the ask was "why is this slow," not "rewrite this tool's invocation model," and a six-line diff that's easy to verify beats a bigger one that's easy to get subtly wrong. Diminishing returns are still returns, but there's a point where the next fix belongs in its own, separately-reviewed change.&lt;/p&gt;

&lt;p&gt;The diagram below is the shape of the fix: four hook events (&lt;code&gt;UserPromptSubmit&lt;/code&gt;, &lt;code&gt;PreToolUse&lt;/code&gt;, &lt;code&gt;PostToolUse&lt;/code&gt;, &lt;code&gt;Stop&lt;/code&gt;) fire in sequence around every turn, three of them call into the repo-memory CLI, and that's where the cold-start tax was hiding. A separate transcript-logging hook writes every prompt/response pair to Postgres — I profiled that too, out of caution, and it's a non-issue: about 50ms per write, confirmed with a direct &lt;code&gt;psql&lt;/code&gt; round-trip test.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff6g3zk0yt4nl74h7btmn.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff6g3zk0yt4nl74h7btmn.png" alt=" " width="799" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I didn't touch, and why
&lt;/h2&gt;

&lt;p&gt;A couple of things came up during the audit that I deliberately left alone.&lt;/p&gt;

&lt;p&gt;The repo memory MCP server (&lt;code&gt;gps&lt;/code&gt;, exposed as a set of MCP tools inside Claude Code itself, distinct from the CLI binary discussed above) showed zero tool invocations in the scan window. By the "unused → disable" logic that applied everywhere else, that's a removal candidate. But the project's own guidance mandates using it before any non-trivial edit, and the cost of leaving it enabled is close to zero — MCP tool schemas are deferred by default, meaning only the tool &lt;em&gt;name&lt;/em&gt; sits in context until it's actually called, not the full schema. One thin transcript window showing zero calls isn't strong enough evidence to override an explicit, repo-wide instruction, especially when the downside of being wrong (removing something actually load-bearing) is worse than the upside (saving essentially nothing). Sometimes the right call from an audit is "leave it, and say why," not "remove everything unused."&lt;/p&gt;

&lt;p&gt;I also didn't touch anything requiring destructive or hard-to-reverse action without asking first — no force-pushes, no permission-mode changes applied without a separate confirmation step, no deleting anything that wasn't obviously dead. Every change made was either trivially reversible (a plugin disable is one command to undo) or a working-tree edit left for review before it got anywhere near a commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;None of these problems were exotic. A stale duplicate install. A broken frontmatter field. Memory files repeating what the code already states. And a six-line hook chain that had quietly picked up a per-call network round-trip nobody asked for, because &lt;code&gt;npx -y&lt;/code&gt; optimizes for "always get the latest version" in a context (a hook that fires hundreds of times a session) where that tradeoff makes no sense at all.&lt;/p&gt;

&lt;p&gt;The pattern underneath all of it: automation you don't periodically re-examine degrades in ways that are individually invisible and collectively expensive. A hook doesn't announce that it's gotten 8x slower. It just quietly adds a few seconds to every response until "Claude feels slow today" becomes the ambient explanation, and the actual cause — six npx cold starts nobody's looked at in months — never gets named.&lt;/p&gt;

&lt;p&gt;If you're running any nontrivial hook setup, it's worth timing it occasionally. Not because any single hook is doing something exotic, but because "small tax, every single turn" compounds in a way that's easy to stop noticing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>devtools</category>
      <category>automation</category>
    </item>
    <item>
      <title>We Audited Our Claude Code Setup Against Anthropic's Own Context-Engineering Rules — Here's What We Found</title>
      <dc:creator>pponali</dc:creator>
      <pubDate>Sun, 26 Jul 2026 12:32:46 +0000</pubDate>
      <link>https://dev.to/pponali/we-audited-our-claude-code-setup-against-anthropics-own-context-engineering-rules-heres-what-we-3mme</link>
      <guid>https://dev.to/pponali/we-audited-our-claude-code-setup-against-anthropics-own-context-engineering-rules-heres-what-we-3mme</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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgzpm248oh1i3t1lyle6p.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgzpm248oh1i3t1lyle6p.png" alt=" " width="800" height="290"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The question that started this
&lt;/h2&gt;

&lt;p&gt;We run Claude Code against a fairly large, fairly automated repository — a farming-assistance platform with a Node.js backend, a Flutter app, a React dashboard, an in-progress Spring Boot microservices migration, and a home-grown "repo memory" layer called &lt;code&gt;gps&lt;/code&gt; that captures invariants, lessons, and preferences across sessions. Over several months we'd wired up a lot of automation: session-start hooks, prompt-submit hooks, auto-captured preferences, persona plugins, a mandatory agent-dispatch table. It felt sophisticated. It also felt, some days, slow to get going — every session seemed to start with a wall of text before any real work happened.&lt;/p&gt;

&lt;p&gt;So when Anthropic published &lt;a href="https://claude.com/blog/the-new-rules-of-context-engineering-for-claude-5-generation-models" rel="noopener noreferrer"&gt;"The New Rules of Context Engineering for Claude 5 Generation Models"&lt;/a&gt;, we asked the obvious question: &lt;strong&gt;are we actually following our own advice, or have we just accumulated automation that looks like good practice?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This post is the audit, the root cause we found, and the fix — including a mistake we made mid-fix that's worth telling on ourselves for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the blog post actually says
&lt;/h2&gt;

&lt;p&gt;Stripped of marketing language, the post boils down to five concrete rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Keep &lt;code&gt;CLAUDE.md&lt;/code&gt; lightweight.&lt;/strong&gt; Describe gotchas and non-obvious patterns, not everything you know about the repo. Organize by relevance, not comprehensiveness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Progressive disclosure.&lt;/strong&gt; Load context at the right time — skills, references, and detail should be pulled in when needed, not front-loaded into every session regardless of task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust the model's judgment.&lt;/strong&gt; Remove redundant guardrails and standing instructions that the newer models don't need spelled out every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rely on automatic memory, not manual dumps.&lt;/strong&gt; Don't hand-maintain a giant preferences block in a markdown file — let the memory system surface the right thing at the right time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design tools and interfaces, not prose.&lt;/strong&gt; Push instructions into tool schemas and parameter design rather than repeating them in the system prompt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this is radical. It's the same discipline good engineers apply to code: don't load everything into scope just because you might need it, don't repeat yourself, don't keep dead configuration around because removing it feels risky. The interesting part is applying it literally, to an actual production Claude Code setup, and seeing what falls out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we found when we actually measured it
&lt;/h2&gt;

&lt;p&gt;We started by tracing exactly what gets injected into the context window at &lt;code&gt;SessionStart&lt;/code&gt;, since that's the one moment every single session pays the cost.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.claude/settings.json&lt;/code&gt; &lt;code&gt;SessionStart&lt;/code&gt; hook was running a chain of &lt;code&gt;gps&lt;/code&gt; commands. Three of them, in sequence, all touched the exact same data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gps preferences &lt;span class="nt"&gt;--write&lt;/span&gt;      &lt;span class="c"&gt;# bakes a markdown block into CLAUDE.md&lt;/span&gt;
gps preferences &lt;span class="nt"&gt;--markdown&lt;/span&gt;   &lt;span class="c"&gt;# prints the same list to stdout&lt;/span&gt;
gps prime                    &lt;span class="c"&gt;# emits a session primer that overlaps both&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--write&lt;/code&gt; persisted a &lt;code&gt;&amp;lt;!-- gps:auto-prefs --&amp;gt;...&amp;lt;!-- /gps:auto-prefs --&amp;gt;&lt;/code&gt; block directly into &lt;code&gt;CLAUDE.md&lt;/code&gt;, which is read every session. &lt;code&gt;--markdown&lt;/code&gt; printed the identical list again as hook stdout, which Claude Code surfaces as a second system reminder. &lt;code&gt;prime&lt;/code&gt; added a third pass that substantially overlapped both. Same ~60-line list, injected three times, every single session, before any actual task began.&lt;/p&gt;

&lt;p&gt;That's rule #1 and #4 violated in one shot: a hand-persisted dump in &lt;code&gt;CLAUDE.md&lt;/code&gt; (the opposite of "lightweight, gotchas only") that was also duplicated by the very memory system that was supposed to replace it.&lt;/p&gt;

&lt;p&gt;Then we looked at what was actually &lt;em&gt;in&lt;/em&gt; that preferences list, because 60 lines of genuine cross-cutting invariants would at least be defensible even if wastefully injected three times. It wasn't. Pulling the raw store (&lt;code&gt;.gps/preferences.yml&lt;/code&gt;) showed &lt;strong&gt;105 entries&lt;/strong&gt;, and a good two-thirds of them weren't standing preferences at all — they were things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A verbatim quote from an eval judge's commentary: &lt;em&gt;"Judge weakness: 'never specifies idempotency for event consumers.'"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;A code comment lifted out of context: &lt;code&gt;// replace, never append&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A fragment of a test assertion: &lt;code&gt;test('refresh() replaces list, never appends', () async { ...&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Duplicate phrasings of the same secrets rule, recorded three separate times with three separate IDs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mechanism responsible was &lt;code&gt;gps capture-preference --emit&lt;/code&gt; and &lt;code&gt;gps capture-directive --emit&lt;/code&gt;, both wired into the &lt;code&gt;UserPromptSubmit&lt;/code&gt; hook — meaning they fired on &lt;em&gt;every single message&lt;/em&gt; in every session. Their logic was a bare regex cue: anything matching &lt;code&gt;\b(?:please\s+)?never\b&lt;/code&gt; or &lt;code&gt;\balways\b&lt;/code&gt;, recorded as a standing, always-injected "preference," with &lt;code&gt;source: auto&lt;/code&gt; and no expiry. It didn't matter whether the match came from the user's actual instruction, a pasted log, a quoted test file, or the agent's own prior output describing what an eval judge said. If the substring was there, it got written to disk and re-injected every session forever.&lt;/p&gt;

&lt;p&gt;This is the sharpest version of the trap the blog post warns about: "automatic memory" sounds like the responsible, judgment-trusting choice — right up until the automation itself has no judgment. A regex doesn't know the difference between a user's hard rule and a stray quote in a transcript. Left alone, it just accumulates noise at the same rate it accumulates signal, and every session pays for both.&lt;/p&gt;

&lt;p&gt;On top of that, we found two plugins — &lt;code&gt;caveman&lt;/code&gt; (terse response style) and &lt;code&gt;ponytail&lt;/code&gt; (anti-overengineering behavioral rules) — set to always-on in the global &lt;code&gt;enabledPlugins&lt;/code&gt; config. Both inject their full rule text (roughly 40 lines each) at every &lt;code&gt;SessionStart&lt;/code&gt;, regardless of whether the task at hand has anything to do with response verbosity or engineering discipline. Useful personas, wrong default: standing injection instead of on-demand invocation is exactly the "load everything just in case" pattern rule #2 argues against.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;We worked through it in four concrete steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Collapse the triplicated emission.&lt;/strong&gt; Dropped &lt;code&gt;--write&lt;/code&gt; and &lt;code&gt;--markdown&lt;/code&gt; from the &lt;code&gt;SessionStart&lt;/code&gt; hook, keeping a single &lt;code&gt;gps prime&lt;/code&gt; pass. One source of truth, one injection per session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Strip the manual dump from &lt;code&gt;CLAUDE.md&lt;/code&gt;.&lt;/strong&gt; Removed the &lt;code&gt;&amp;lt;!-- gps:auto-prefs --&amp;gt;&lt;/code&gt; block entirely. &lt;code&gt;CLAUDE.md&lt;/code&gt; went back to being what rule #1 asks for: hand-curated gotchas, not an auto-regenerated preference log.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Prune the store.&lt;/strong&gt; We pulled the full 105-entry list and manually classified every one. About 35 were genuine, cross-cutting invariants worth injecting into every session regardless of task — secrets handling, PII rules, git-push restrictions, migration reversibility, PCI scope boundaries. The other 70 were dropped: judge commentary, code fragments, near-duplicate phrasings, symbol-specific one-off fixes that only matter when that exact piece of code is touched again (those still exist — just surfaced on demand via &lt;code&gt;gps prepare_edit&lt;/code&gt; when relevant, not blasted into every session).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Fix the actual leak, not just the symptom.&lt;/strong&gt; This is the part worth dwelling on, because the first three steps only clean up what had already accumulated — they don't stop it from happening again. The regex-based &lt;code&gt;capture-preference&lt;/code&gt;/&lt;code&gt;capture-directive&lt;/code&gt; hooks were still wired in and would refill the store with the same class of noise within days. We replaced them with a small filtered wrapper script that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reads only the literal current user prompt (&lt;code&gt;$CLAUDE_USER_PROMPT&lt;/code&gt;), never the transcript, never the agent's own output, never quoted or pasted content;&lt;/li&gt;
&lt;li&gt;requires an actual imperative sentence shape — the text has to &lt;em&gt;start&lt;/em&gt; with &lt;code&gt;never&lt;/code&gt;, &lt;code&gt;always&lt;/code&gt;, &lt;code&gt;don't&lt;/code&gt;, &lt;code&gt;must&lt;/code&gt;, or &lt;code&gt;should&lt;/code&gt;, not just contain the word anywhere;&lt;/li&gt;
&lt;li&gt;rejects lines that look like code, quotes, or table rows (&lt;code&gt;//&lt;/code&gt;, &lt;code&gt;#&lt;/code&gt;, &lt;code&gt;|&lt;/code&gt;, &lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;,&lt;/code&gt;&amp;gt;`);&lt;/li&gt;
&lt;li&gt;checks the existing store for a near-duplicate before writing anything new.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We disabled &lt;code&gt;caveman&lt;/code&gt; and &lt;code&gt;ponytail&lt;/code&gt; as always-on plugins and left them invokable via their slash commands, on demand, exactly when their behavior is actually wanted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake worth admitting
&lt;/h2&gt;

&lt;p&gt;Midway through pruning that 105-entry list by hand, we cut a handful of rules that were real, not noise — a "reference the config key, never the value" secrets-handling convention, a backend mock-fallback contract invariant, an API schema-evolution rule ("add fields, never remove"), a known config-server caching gotcha. They looked narrow enough to fold into "one-off" at a glance, but they were genuinely cross-repo conventions, not symbol-specific trivia.&lt;/p&gt;

&lt;p&gt;The user caught it with a simple question: &lt;em&gt;"is it losing existing functionality?"&lt;/em&gt; That's the right question to ask after any aggressive prune, automated or manual — and it's the reason we're writing this section at all. Nothing was destroyed; the full pre-prune store was still sitting in git history, recoverable with one &lt;code&gt;git show&lt;/code&gt;. But it's a useful reminder that "delete the noise" and "delete everything that isn't obviously load-bearing" are not the same operation, and doing the second by accident while intending the first is an easy mistake to make when you're skimming 105 entries under time pressure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying the automated version wasn't just automating the same mistake
&lt;/h2&gt;

&lt;p&gt;Before trusting the new filtered capture script, we tested it against an isolated scratch copy of the &lt;code&gt;.gps&lt;/code&gt; store — not the live repository — feeding it a mix of a genuine directive and the exact kind of noise that had polluted the original store:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;plaintext&lt;br&gt;
Never log raw request bodies for the payment endpoint.&lt;br&gt;
Always validate the webhook signature before processing.&lt;br&gt;
The judge said: never sent to the network, that's just eval commentary.&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The result: both real directives were captured and written to the store correctly. The judge-commentary sentence was rejected, because it doesn't open with an imperative verb — it opens with "The judge said." That's a small test, but it's exactly the discriminator the original regex lacked, and it's cheap enough to run before wiring anything back into a live hook.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually bought us
&lt;/h2&gt;

&lt;p&gt;Token-wise, the honest estimate is roughly &lt;strong&gt;~1,200+ tokens injected at every &lt;code&gt;SessionStart&lt;/code&gt; before the fix, down to ~400 after&lt;/strong&gt; — a triplicated 60-line list plus a noisy 105-entry store plus two always-on plugins, versus one clean primer pass plus a curated 35-entry store plus plugins that only cost anything when explicitly invoked. That's not a huge absolute number in isolation, but it's paid on every single session, compounds across a team, and — more importantly than the raw token count — it was diluting the signal-to-noise ratio of the one list that's supposed to carry the invariants Claude should never violate. A 35-item list of real rules is something a model can actually hold onto. A 105-item list where two-thirds is eval-transcript noise trains exactly the wrong instinct: skim past it, because it's mostly not going to matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The generalizable lesson
&lt;/h2&gt;

&lt;p&gt;None of the individual fixes here are exotic — collapse duplicate emissions, prune a noisy store, tighten a regex, gate a plugin behind explicit invocation. The useful part is the audit discipline itself: pick a set of concrete, checkable rules (Anthropic gave us five good ones), and then actually trace what your own automation does against them, hook by hook, rather than assuming that "we have automatic memory" or "we use skills" means the rules are satisfied by construction. Automation that was designed well can still drift into violating the very principles it was built to serve — usually quietly, one auto-captured preference at a time, until someone asks what's actually in the store.&lt;/p&gt;

&lt;p&gt;If you're running Claude Code with any nontrivial hook setup — &lt;code&gt;gps&lt;/code&gt;, a custom memory layer, auto-capture of any kind — it's worth doing the same trace. Open the SessionStart hook chain, count how many times the same information gets emitted, and go look at what your "automatic" capture mechanism has actually been recording for the last few months. You may be surprised what's in there.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>devtools</category>
      <category>llm</category>
    </item>
    <item>
      <title>From Monolith to 23 Microservices: A Strangler-Fig Migration</title>
      <dc:creator>pponali</dc:creator>
      <pubDate>Mon, 13 Jul 2026 12:10:23 +0000</pubDate>
      <link>https://dev.to/pponali/from-monolith-to-23-microservices-a-strangler-fig-migration-4j6b</link>
      <guid>https://dev.to/pponali/from-monolith-to-23-microservices-a-strangler-fig-migration-4j6b</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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxo1ynuaymixe4uykvqyg.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxo1ynuaymixe4uykvqyg.png" alt="Kheti Sahayak platform architecture diagram showing client apps flowing through the API gateway into 23 Spring Boot microservices, the data and messaging layer, and the observability stack" width="800" height="565"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've spent the last several months rebuilding the backend for &lt;strong&gt;Kheti Sahayak&lt;/strong&gt;, an agricultural assistance platform, from the ground up — taking it from a single Node.js/Express monolith to 23 Spring Boot microservices. Not because microservices are trendy, but because the monolith had genuinely started working against us: every deploy touched everything, every schema change was a coordination problem, and the team (well, mostly just me) couldn't ship one feature without risking three others.&lt;/p&gt;

&lt;p&gt;Here's how the migration actually went, and what I'd tell someone about to do the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strangler-fig, not big-bang
&lt;/h2&gt;

&lt;p&gt;The instinct when a monolith gets painful is to stop, design the "real" architecture, and rewrite. I didn't do that, and I'd argue you shouldn't either unless you can afford months of feature freeze.&lt;/p&gt;

&lt;p&gt;Instead I used the &lt;strong&gt;strangler-fig pattern&lt;/strong&gt;: new functionality got built as standalone Spring Boot services from day one, while the old Node.js backend kept serving existing traffic. Over time, more and more surface area moved to the new services, until the monolith's job shrank to "the stuff nobody's gotten around to migrating yet." The system stayed shippable the entire time — there was never a week where the app was down for "the migration."&lt;/p&gt;

&lt;h2&gt;
  
  
  One front door: the API Gateway
&lt;/h2&gt;

&lt;p&gt;With 23 services, you can't have every client know 23 different addresses. Every request — from the Kotlin/Compose Android app or the React admin dashboard — goes through a single Spring Cloud Gateway instance. It's the only thing external clients ever talk to directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# gateway-service application.yml — routing to internal services&lt;/span&gt;
&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cloud&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user-service&lt;/span&gt;
          &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lb://user-service&lt;/span&gt;
          &lt;span class="na"&gt;predicates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Path=/api/users/**&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payment-service&lt;/span&gt;
          &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lb://payment-service&lt;/span&gt;
          &lt;span class="na"&gt;predicates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Path=/api/payments/**&lt;/span&gt;
          &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RequestRateLimiter&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gateway also owns JWT verification before a request ever reaches a downstream service — auth logic doesn't get duplicated 23 times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding each other without hardcoded IPs
&lt;/h2&gt;

&lt;p&gt;Services need to find each other, and hardcoding hostnames doesn't survive contact with real deployments. I run a &lt;code&gt;discovery-service&lt;/code&gt; (Eureka) that every service registers with on startup, and a &lt;code&gt;config-service&lt;/code&gt; that serves configuration — including secrets — encrypted at rest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# any service's bootstrap config&lt;/span&gt;
&lt;span class="na"&gt;eureka&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;service-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;defaultZone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://discovery-service:8761/eureka&lt;/span&gt;
&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;import&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;configserver:http://config-service:8888"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second point matters more than it looks: no service carries plaintext secrets in an env var or a properties file. The config server decrypts on the fly using a key that never leaves the server itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not everything needs to be synchronous
&lt;/h2&gt;

&lt;p&gt;Some of these services genuinely don't need to talk to each other in real time. When an order gets placed, the notification service doesn't need to block the checkout flow to send a push notification — it just needs to know the order happened, eventually. That's where Kafka comes in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderEventPublisher&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;KafkaTemplate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;OrderEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;kafkaTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;publishOrderPlaced&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderEvent&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;kafkaTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders.placed"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getOrderId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;notification-service&lt;/code&gt; consumes from that topic independently and fires push notifications (via Firebase Cloud Messaging), SMS, and text-to-speech — the last one matters a lot for farmers who may not read English fluently. That's an accessibility requirement that shaped the architecture, not an afterthought bolted on later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability isn't optional past a certain scale
&lt;/h2&gt;

&lt;p&gt;With one service, you can &lt;code&gt;tail -f&lt;/code&gt; a log file and mostly know what's happening. With 23, you can't. The stack I run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prometheus&lt;/strong&gt; scraping metrics from every service&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grafana&lt;/strong&gt; for dashboards&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elasticsearch, Logstash, Kibana&lt;/strong&gt; (the ELK stack) for centralized logs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An APM server&lt;/strong&gt; for distributed tracing across service boundaries&lt;/li&gt;
&lt;li&gt;Purpose-built exporters for Postgres, Redis, and Kafka broker metrics
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# prometheus scrape config (excerpt)&lt;/span&gt;
&lt;span class="na"&gt;scrape_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gateway-service'&lt;/span&gt;
    &lt;span class="na"&gt;metrics_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/actuator/prometheus'&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gateway-service:8080'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fourteen components total in that stack. It sounds like a lot for a side project, but the alternative — debugging a cross-service failure blind — is worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the AI actually lives
&lt;/h2&gt;

&lt;p&gt;One service doesn't run on the JVM at all: a FastAPI service handles crop disease detection from photos farmers upload, sitting alongside the Java stack and reached the same way every other service is — through the gateway. Polyglot isn't a religion here; it's just that Python's ML tooling is better than Java's for this specific job, so that's what that one service uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd actually tell you
&lt;/h2&gt;

&lt;p&gt;If you're staring down a monolith that's slowing your team (or just yourself) down: don't wait for permission to do a "proper" rewrite. Strangle it. Ship the new service, route one path to it, watch it work in production, then move the next path. The migration becomes a series of small, reversible bets instead of one enormous one — and you never have to explain to anyone why the app is down for a week.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Kheti Sahayak is a solo project — architecture, migration, and all the mistakes along the way are mine. Happy to go deeper on any piece of this in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>microservices</category>
      <category>java</category>
      <category>architecture</category>
      <category>devops</category>
    </item>
    <item>
      <title>Scoring AI Agents: Deterministic Metrics + an LLM Judge</title>
      <dc:creator>pponali</dc:creator>
      <pubDate>Thu, 18 Jun 2026 05:57:49 +0000</pubDate>
      <link>https://dev.to/pponali/scoring-ai-agents-deterministic-metrics-an-llm-judge-poj</link>
      <guid>https://dev.to/pponali/scoring-ai-agents-deterministic-metrics-an-llm-judge-poj</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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frbmm496poos6kgnpagej.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frbmm496poos6kgnpagej.png" alt=" " width="799" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I run a lot of small autonomous agents — backend, frontend, mobile, devops, monitoring tiers, each one a prompt with a job. The moment you have more than a handful, a question gets uncomfortable: &lt;em&gt;are they actually any good, and did my last prompt edit make them better or worse?&lt;/em&gt; "It looked fine when I tried it" doesn't scale. So I built a small evaluation framework that answers it with numbers, and then closes the loop by improving the prompts automatically.&lt;/p&gt;

&lt;p&gt;Here's how it's put together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deterministic first, LLM second
&lt;/h2&gt;

&lt;p&gt;The core principle: &lt;strong&gt;measure what you can measure deterministically, and only reach for an LLM judge where you must.&lt;/strong&gt; Deterministic metrics are free, instant, and reproducible. An LLM judge is none of those things — so it's opt-in and purely additive.&lt;/p&gt;

&lt;p&gt;The harness runs each agent as an &lt;strong&gt;isolated subprocess&lt;/strong&gt;, feeds it a fixed fixture on stdin, captures stdout, and scores the result against expected outputs. No shared state, no network, no flakiness.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 harness/evaluate.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--agents-dir&lt;/span&gt; ./agents &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--out-dir&lt;/span&gt; ./out &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--seed&lt;/span&gt; 42 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt; 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single command produces &lt;code&gt;report.json&lt;/code&gt;, a human-readable &lt;code&gt;report.txt&lt;/code&gt;/&lt;code&gt;.html&lt;/code&gt;, a &lt;code&gt;failures.json&lt;/code&gt;, and appends to &lt;code&gt;history.jsonl&lt;/code&gt; so you can track drift over time. No SDK, no API key required.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent contract is dead simple
&lt;/h2&gt;

&lt;p&gt;Every agent is just a program that reads a task from stdin and writes an answer to stdout. That's the whole interface — which is exactly why subprocess isolation works.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# agents/sample_agent/agent.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# ... the agent's real logic ...
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer&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;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the contract is a process boundary, an "agent" can be Python, a shell script, or anything that respects stdin/stdout. The harness doesn't care.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five metrics, one threshold gate
&lt;/h2&gt;

&lt;p&gt;Each run is scored on five deterministic metrics, checked against thresholds declared in &lt;code&gt;metrics.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;thresholds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;accuracy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.8&lt;/span&gt;                  &lt;span class="c1"&gt;# exact normalized matches&lt;/span&gt;
  &lt;span class="na"&gt;fuzzy_score&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.7&lt;/span&gt;               &lt;span class="c1"&gt;# average sequence similarity 0-1&lt;/span&gt;
  &lt;span class="na"&gt;timeout_rate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;              &lt;span class="c1"&gt;# fraction of runs that timed out&lt;/span&gt;
  &lt;span class="na"&gt;safety_violations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;           &lt;span class="c1"&gt;# outputs matching unsafe patterns&lt;/span&gt;
  &lt;span class="na"&gt;reproducibility_variance&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.05&lt;/span&gt; &lt;span class="c1"&gt;# std-dev across repeated runs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;reproducibility_variance&lt;/code&gt; is the one people forget. Running an agent once tells you what it did; running it several times and measuring the spread tells you whether you can &lt;em&gt;trust&lt;/em&gt; what it did. A correct-but-nondeterministic agent is a latent bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The LLM judge, when correctness isn't enough
&lt;/h2&gt;

&lt;p&gt;Some qualities aren't string-comparable: did the agent stay in role? Did it respect its constraints? Is the output well-formed and complete? For those, an opt-in judge sends the rubric, the task, and the agent's real output to Claude and gets back a &lt;strong&gt;structured verdict&lt;/strong&gt; — validated against a JSON schema so a malformed judgment can't poison the report.&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;"overall"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;7.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dimensions"&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;"contract_adherence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"role_fidelity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"constraint_safety"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"output_format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"completeness"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&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;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"needs_improvement"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"weaknesses"&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;"dimension"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"output_format"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"prompt_fix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Require a fenced JSON block in the system prompt."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The judge runs three ways depending on what you have: the Anthropic API (&lt;code&gt;--llm-judge&lt;/code&gt;), the headless Claude Code CLI for subscription-only setups (&lt;code&gt;--llm-judge-cli&lt;/code&gt;), or pre-computed verdicts from any source (&lt;code&gt;--llm-verdicts&lt;/code&gt;). Same report either way. Identical outputs are judged once to bound cost.&lt;/p&gt;

&lt;p&gt;The important detail: every weakness must map to &lt;strong&gt;a fixable line in the agent's prompt&lt;/strong&gt;. The judge isn't there to vibe-check; it produces edits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the loop: the prompts improve themselves
&lt;/h2&gt;

&lt;p&gt;This is where it gets fun. A &lt;code&gt;fail&lt;/code&gt; verdict and its prompt fixes land in &lt;code&gt;failures.json&lt;/code&gt;, which feeds a GEPA-style improve loop: judge each candidate prompt &lt;strong&gt;per dimension&lt;/strong&gt;, mutate the frontier candidate that owns the &lt;em&gt;weakest&lt;/em&gt; dimension, keep a pool of candidates rather than greedily chasing one best, and write back only the best pool member. Scores and mutations are persisted to repo memory so the next run starts informed, and a nightly job commits improvements.&lt;/p&gt;

&lt;p&gt;The diagram above shows the whole flow: inputs → harness → (metrics + judge) → reports → improve loop, with a feedback edge carrying mutated prompts back to re-evaluation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell my past self
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic metrics are the foundation, not the LLM judge.&lt;/strong&gt; The judge is a scalpel, not a hammer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate the judge's output against a schema.&lt;/strong&gt; An LLM that returns malformed JSON shouldn't be able to corrupt your report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track history.&lt;/strong&gt; A single score is a snapshot; &lt;code&gt;history.jsonl&lt;/code&gt; is the trend that tells you whether you're actually getting better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make every critique actionable.&lt;/strong&gt; "This is weak" is noise. "Add a fenced JSON block to line 12" is a commit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The payoff is a system where I can change a prompt, run one command, and know — numerically — whether I helped or hurt, with the loop quietly fixing the easy regressions for me.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>testing</category>
      <category>programming</category>
    </item>
    <item>
      <title>I built a Claude Code skill that discovers which automations to build — by mining my own work</title>
      <dc:creator>pponali</dc:creator>
      <pubDate>Thu, 18 Jun 2026 05:34:53 +0000</pubDate>
      <link>https://dev.to/pponali/i-built-a-claude-code-skill-that-discovers-which-automations-to-build-by-mining-my-own-work-8lp</link>
      <guid>https://dev.to/pponali/i-built-a-claude-code-skill-that-discovers-which-automations-to-build-by-mining-my-own-work-8lp</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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7c1hadlsgcwos8qbouwe.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7c1hadlsgcwos8qbouwe.png" alt=" " width="799" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most automation projects build &lt;em&gt;an&lt;/em&gt; automation. This one builds a system that &lt;strong&gt;discovers which automations to build&lt;/strong&gt; — by mining your own recent work — and then packages the high-confidence ones into reusable tools. It's a self-improving skill for &lt;a href="https://claude.com/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: repeated manual work is invisible
&lt;/h2&gt;

&lt;p&gt;You feel it but rarely measure it: the same multi-step sequence done by hand for the third time. A focused bug-fix → branch → conventional commit → PR cycle. A QA-sweep dispatch. A release deploy. Each repetition is a signal that an abstraction is missing — but nobody systematically &lt;em&gt;finds&lt;/em&gt; those signals. They're scattered across git history, terminal sessions, and memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;distill-workflows&lt;/code&gt; does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;distill-workflows&lt;/code&gt; is a Claude Code skill that turns that scattered signal into reusable tooling, in one pass:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scan&lt;/strong&gt; three sources of "what was done recently":

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Git commit-shape frequency&lt;/strong&gt; — normalized subjects, ranked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session transcripts&lt;/strong&gt; (&lt;code&gt;*.jsonl&lt;/code&gt;) — repeated bash command patterns &lt;em&gt;and&lt;/em&gt; recurring user-request phrasings. What you &lt;em&gt;did&lt;/em&gt; and what you were &lt;em&gt;asked&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repo memory&lt;/strong&gt; (GPS) — lessons, decisions, and promotion candidates already flagged.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cluster&lt;/strong&gt; the raw signal into named, repeated procedures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconcile&lt;/strong&gt; against a persistent cross-session ledger so a pattern seen &lt;em&gt;once per session&lt;/em&gt; still accumulates toward the bar over time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score&lt;/strong&gt; each candidate on &lt;em&gt;frequency × determinism × friction&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Propose&lt;/strong&gt; a ranked table and &lt;strong&gt;confirm with the human&lt;/strong&gt; before doing anything irreversible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scaffold&lt;/strong&gt; the winners from templates into the right artifact — a &lt;strong&gt;skill&lt;/strong&gt;, a &lt;strong&gt;subagent&lt;/strong&gt;, or a &lt;strong&gt;workflow&lt;/strong&gt; — and record the rationale back into memory.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;The diagram above maps the flow left-to-right: &lt;strong&gt;Signal Sources → Scanner (&lt;code&gt;scan.sh&lt;/code&gt;) → Distill Engine → Packaging → Outputs&lt;/strong&gt;, with a &lt;strong&gt;Persistent Ledger&lt;/strong&gt; the engine reads and writes, and a dashed &lt;strong&gt;cross-session feedback loop&lt;/strong&gt; that feeds each run's recorded rationale back into the next run's sources.&lt;/p&gt;

&lt;p&gt;A few design choices worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read-and-propose first, scaffold on confirmation.&lt;/strong&gt; Nothing lands on disk without a human gate. Automation that edits your repo should ask first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extend before create.&lt;/strong&gt; If 80% of a candidate already lives in an existing skill, the engine says so instead of producing a near-duplicate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ledger fixes the frequency blind spot.&lt;/strong&gt; A workflow you do once per session looks rare inside a single run. Accumulating "seen" counts across runs is what lets genuinely recurring work eventually qualify.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  It found real work — on itself
&lt;/h2&gt;

&lt;p&gt;Run against its own repo, the scanner surfaced a pattern repeated &lt;strong&gt;50+ times&lt;/strong&gt; that nobody had abstracted: a single-concern Android/Kotlin fix shipped as a small PR with the same conventions every time (branch naming, conventional commit scope, base branch, size limit, no AI attribution). The engine packaged it into an &lt;code&gt;android-fix-pr&lt;/code&gt; skill — exactly the kind of convention layer the generic &lt;code&gt;commit&lt;/code&gt;/&lt;code&gt;create-pr&lt;/code&gt; tools don't encode.&lt;/p&gt;

&lt;p&gt;The higher-frequency QA-sweep pattern was &lt;em&gt;also&lt;/em&gt; detected, but the engine correctly declined to package it: it already existed as a parameterized workflow. Knowing when &lt;strong&gt;not&lt;/strong&gt; to build is half the value.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it's built
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;SKILL.md&lt;/code&gt; procedure the agent follows.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;scan.sh&lt;/code&gt; signal miner — &lt;code&gt;jq&lt;/code&gt; over transcripts, &lt;code&gt;git log&lt;/code&gt; shape analysis, &lt;code&gt;gps recall&lt;/code&gt;, artifact inventory — emitting one structured report.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;watchlist.md&lt;/code&gt; ledger for cross-session frequency.&lt;/li&gt;
&lt;li&gt;Artifact templates for skill / subagent / workflow.&lt;/li&gt;
&lt;li&gt;GPS repo memory for persisted decisions.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Schedule it to run automatically and surface a weekly "candidate digest."&lt;/li&gt;
&lt;li&gt;Rank promotion candidates via the GPS MCP tool.&lt;/li&gt;
&lt;li&gt;Share distilled skills across repos.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built with Claude Code. The core idea generalizes well beyond one repo: point a discovery loop at your own work history, score what repeats, and let the high-confidence patterns become tools.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>I tried to run Google Antigravity on an arm64 VM. It did not go well.</title>
      <dc:creator>pponali</dc:creator>
      <pubDate>Sun, 24 May 2026 06:08:35 +0000</pubDate>
      <link>https://dev.to/pponali/i-tried-to-run-google-antigravity-on-an-arm64-vm-it-did-not-go-well-278p</link>
      <guid>https://dev.to/pponali/i-tried-to-run-google-antigravity-on-an-arm64-vm-it-did-not-go-well-278p</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I tried to install Google Antigravity (the new agent dev IDE) on a headless arm64 Ubuntu VM. It is a x86_64 Electron app. Three emulation layers later, I gave up and uninstalled everything. Here is the trail of pain so you do not repeat it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Oracle Cloud Ampere VM, aarch64, Ubuntu 24.04 LTS&lt;/li&gt;
&lt;li&gt;Headless (no display server, SSH only)&lt;/li&gt;
&lt;li&gt;96GB disk, 24GB RAM&lt;/li&gt;
&lt;li&gt;Antigravity build: &lt;code&gt;linux-x64&lt;/code&gt; tarball, 168MB, Electron-based&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The plan: install a desktop environment + RDP, emulate x86_64, run Antigravity, connect from my laptop. Sounds reasonable. It is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round 1: qemu-user-static
&lt;/h2&gt;

&lt;p&gt;The classic approach. Install &lt;code&gt;qemu-user-static&lt;/code&gt; + &lt;code&gt;binfmt-support&lt;/code&gt;, register &lt;code&gt;qemu-x86_64&lt;/code&gt; in &lt;code&gt;binfmt_misc&lt;/code&gt;, point at an amd64 sysroot, run the binary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; qemu-user-static binfmt-support
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First exec attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;x86_64-binfmt-P: Could not open '/lib64/ld-linux-x86-64.so.2': No such file or directory
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No amd64 dynamic linker. Bootstrap one with &lt;code&gt;debootstrap&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;debootstrap &lt;span class="nt"&gt;--arch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;amd64 &lt;span class="nt"&gt;--variant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;minbase noble /opt/amd64-sysroot &lt;span class="se"&gt;\&lt;/span&gt;
  http://archive.ubuntu.com/ubuntu
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;debootstrap finished extracting but post-install scripts crashed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Setting up libpam-modules:amd64 (1.5.3-5ubuntu5) ...
x86_64-binfmt-P: QEMU internal SIGSEGV {code=MAPERR, addr=0x20}
Segmentation fault (core dumped)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;qemu-user segfaulted on basic dpkg postinst. If &lt;code&gt;passwd&lt;/code&gt; cannot configure itself, Chromium has zero chance. This is a known qemu-user limitation: threading + atomic ops + signal handling get exotic enough that complex binaries crash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict: dead.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Round 2: FEX-Emu
&lt;/h2&gt;

&lt;p&gt;FEX-Emu is purpose-built for this: userspace x86_64 -&amp;gt; aarch64 translation, designed with Chromium and Wine in mind. There is a PPA.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;add-apt-repository &lt;span class="nt"&gt;-y&lt;/span&gt; ppa:fex-emu/fex
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; fex-emu-armv8.2 fex-emu-binfmt64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Pick the armv8.X package matching your CPU — check &lt;code&gt;/proc/cpuinfo&lt;/code&gt; features. Ampere Altra has &lt;code&gt;lse&lt;/code&gt;, &lt;code&gt;lrcpc&lt;/code&gt;, &lt;code&gt;asimddp&lt;/code&gt; =&amp;gt; armv8.2.)&lt;/p&gt;

&lt;p&gt;FEX needs a rootfs. The shipped fetcher (&lt;code&gt;FEXRootFSFetcher&lt;/code&gt;) opens a zenity GUI even with &lt;code&gt;-y --assume-yes&lt;/code&gt;. On a headless box it hangs forever holding zero network connections.&lt;/p&gt;

&lt;p&gt;Skip it. Pull the JSON manifest and fetch directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://rootfs.fex-emu.gg/RootFS_links.json | &lt;span class="se"&gt;\&lt;/span&gt;
  python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import json,sys; d=json.load(sys.stdin); &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  [print(k, v['URL']) for k,v in d['v1'].items() if 'Ubuntu_24' in v['URL']]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I grabbed the SquashFS image (~500MB), extracted it to a directory (no FUSE needed), pointed &lt;code&gt;~/.fex-emu/Config.json&lt;/code&gt; at it:&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;"Config"&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;"RootFS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ubuntu_24_04"&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;Started &lt;code&gt;FEXServer -p 3600&lt;/code&gt; (persistent for an hour), then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;FEXInterpreter /opt/antigravity/antigravity &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First real progress:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FATAL:sandbox/linux/suid/client/setuid_sandbox_host.cc:166]
The SUID sandbox helper binary was found, but is not configured correctly.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fixable. &lt;code&gt;chmod 4755&lt;/code&gt; + &lt;code&gt;chown root:root&lt;/code&gt; on &lt;code&gt;chrome-sandbox&lt;/code&gt;. Also disable AppArmor restricted userns (Ubuntu 24.04 added this):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chown &lt;/span&gt;root:root /opt/antigravity/chrome-sandbox
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;4755 /opt/antigravity/chrome-sandbox
&lt;span class="nb"&gt;sudo &lt;/span&gt;sysctl &lt;span class="nt"&gt;-w&lt;/span&gt; kernel.apparmor_restrict_unprivileged_userns&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try again under xvfb with &lt;code&gt;--no-sandbox&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xvfb-run &lt;span class="nt"&gt;-a&lt;/span&gt; FEXInterpreter /opt/antigravity/antigravity &lt;span class="nt"&gt;--no-sandbox&lt;/span&gt; &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Trace/breakpoint trap (core dumped)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;V8 JIT crashed. Electron's V8 generates x86_64 machine code at runtime and runs it. FEX must re-translate that emitted code on the fly. It works for many binaries; for Electron with modern V8 it does not. SIGTRAP every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict: dead.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Round 3: there is no round 3
&lt;/h2&gt;

&lt;p&gt;Options I considered and rejected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;box64&lt;/strong&gt; — same userspace translation class, same V8 problems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full-system KVM x86_64 VM&lt;/strong&gt; — nested virt on arm64 Ampere is brutally slow, needs ~15GB disk, hours of setup, and you still need a display server inside.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wait for an arm64 Antigravity build&lt;/strong&gt; — not on the roadmap.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The actual answer
&lt;/h2&gt;

&lt;p&gt;Antigravity is an IDE. IDEs have Remote-SSH. The correct topology:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Local laptop: Antigravity x86_64]  --SSH--&amp;gt;  [arm64 VM: your code]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The IDE runs on hardware that natively supports it. The VM stays headless and serves files + runs your stack. No emulation, no RDP, no zenity, no V8 JIT translation.&lt;/p&gt;

&lt;p&gt;I should have done this in the first 30 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it cost
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;~2 hours&lt;/li&gt;
&lt;li&gt;~3GB of packages installed and then purged&lt;/li&gt;
&lt;li&gt;168MB tarball + 836MB EroFS + 497MB SquashFS rootfs (also purged)&lt;/li&gt;
&lt;li&gt;A renewed appreciation for native binaries&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lessons
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check arch before download.&lt;/strong&gt; &lt;code&gt;arch64 + linux-x64 .tar.gz = no.&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headless VM + Electron app = pick your pain.&lt;/strong&gt; RDP into a heavy desktop, or admit the IDE belongs on your laptop.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;qemu-user is for static utilities, not Chromium.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FEX-Emu is impressive but V8 self-modifying code is its kryptonite.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;FEXRootFSFetcher&lt;/code&gt; is GUI-only; fetch the rootfs JSON directly on headless machines.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to try anyway, the squashfs rootfs URL is in &lt;code&gt;https://rootfs.fex-emu.gg/RootFS_links.json&lt;/code&gt;. Don't say I didn't warn you.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>arm</category>
      <category>linux</category>
      <category>tooling</category>
    </item>
    <item>
      <title>I shipped my first agentic workflow on GitHub Actions — 4 AI reviewers on every PR</title>
      <dc:creator>pponali</dc:creator>
      <pubDate>Mon, 18 May 2026 07:29:54 +0000</pubDate>
      <link>https://dev.to/pponali/i-shipped-my-first-agentic-workflow-on-github-actions-4-ai-reviewers-on-every-pr-321b</link>
      <guid>https://dev.to/pponali/i-shipped-my-first-agentic-workflow-on-github-actions-4-ai-reviewers-on-every-pr-321b</guid>
      <description>&lt;p&gt;I run &lt;a href="https://play.google.com/store" rel="noopener noreferrer"&gt;Khetisahayak&lt;/a&gt; — a small agritech app — solo. Every PR I open has nobody to review it but me, and I review my own code about as well as anyone reviews their own code. So I built a GitHub Actions workflow that drops &lt;strong&gt;four AI reviewers and a test runner&lt;/strong&gt; onto every PR. This is the first real "agentic" workflow I've shipped, and it now greets every commit I push with five separate opinions.&lt;/p&gt;

&lt;p&gt;This post walks through exactly how it works — &lt;strong&gt;trigger&lt;/strong&gt;, &lt;strong&gt;sequence&lt;/strong&gt;, and &lt;strong&gt;flow&lt;/strong&gt; — plus what I'd change.&lt;/p&gt;

&lt;p&gt;Here's what one PR actually looks like in production (this is the fallback path when the Gemini API rate-limits us — more on that at the end):&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%2Fth02ak18kg3xksw2t2gt.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%2Fth02ak18kg3xksw2t2gt.jpeg" alt="Four review-agent comments on PR #43, each showing the 429 Too Many Requests fallback message" width="800" height="1196"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Four bot comments, one per persona — Senior Developer, CSO, Engineering Lead, Software Architect — and a fifth from the test runner. When the AI call succeeds, those "Automated review could not be completed" blocks are replaced with actual line-by-line review feedback.&lt;/p&gt;




&lt;h2&gt;
  
  
  The two files that do everything
&lt;/h2&gt;

&lt;p&gt;The whole system is two files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.github/workflows/code-review-agents.yml&lt;/code&gt; — the GitHub Actions workflow (the orchestration)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.github/scripts/review_agent.py&lt;/code&gt; — a ~120-line Python script that calls Gemini once per reviewer persona&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. No Lambda, no queue, no separate review service. GitHub Actions is the runtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The trigger
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;develop&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;reopened&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fires when a PR targeting &lt;code&gt;main&lt;/code&gt; or &lt;code&gt;develop&lt;/code&gt; is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;opened&lt;/code&gt; — PR created&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;synchronize&lt;/code&gt; — new commits pushed to the PR branch&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;reopened&lt;/code&gt; — closed PR reopened&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; fire on direct pushes to main, draft PRs (until marked ready), PRs targeting other branches, comments, or label changes. That's deliberate — I don't want a reviewer firing every time someone reacts with a 👍.&lt;/p&gt;

&lt;p&gt;Permissions are scoped tight:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;issues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the code, write comments. Nothing else.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The sequence — five jobs, one DAG
&lt;/h2&gt;

&lt;p&gt;The workflow defines five jobs. GitHub Actions runs them according to &lt;code&gt;needs:&lt;/code&gt; declarations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A([PR event]) --&amp;gt; B[L1: peer-review]
    A --&amp;gt; D[L4: security-review]
    A --&amp;gt; E[test-execution]
    B --&amp;gt; C[L2: lead-review]
    C --&amp;gt; F[L3: architect-review]
    B --&amp;gt; G[review-gate]
    C --&amp;gt; G
    F --&amp;gt; G
    D --&amp;gt; G
    E --&amp;gt; G

    style B fill:#dbeafe,stroke:#2563eb
    style C fill:#dbeafe,stroke:#2563eb
    style F fill:#dbeafe,stroke:#2563eb
    style D fill:#fee2e2,stroke:#dc2626
    style E fill:#dcfce7,stroke:#16a34a
    style G fill:#f3f4f6,stroke:#6b7280
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Three things kick off in parallel&lt;/strong&gt; the moment a PR event lands:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;peer-review&lt;/code&gt; (L1) — code style, correctness, DRY&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;security-review&lt;/code&gt; (L4) — OWASP, secrets, injection, auth&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test-execution&lt;/code&gt; — actually runs Jest + Flutter tests in a real Postgres container&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;A sequential chain&lt;/strong&gt; runs alongside:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;lead-review&lt;/code&gt; waits on &lt;code&gt;peer-review&lt;/code&gt; finishing&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;architect-review&lt;/code&gt; waits on &lt;code&gt;lead-review&lt;/code&gt; finishing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The final gate&lt;/strong&gt; waits on all five:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;review-gate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;peer-review&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;lead-review&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;architect-review&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;security-review&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;test-execution&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;if: always()&lt;/code&gt; means the gate runs even if upstream jobs fail. Right now it just echoes each job's result — it doesn't actually block the merge. To make it blocking, you add a step that exits non-zero on any &lt;code&gt;failure&lt;/code&gt;, then mark &lt;code&gt;review-gate&lt;/code&gt; as a required check in branch protection. (I have not done this yet. Don't be like me.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observable wall-clock timing&lt;/strong&gt; on a typical PR:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;T=0: peer + security + tests start&lt;/li&gt;
&lt;li&gt;T≈45s: peer finishes → lead starts&lt;/li&gt;
&lt;li&gt;T≈90s: lead finishes → architect starts&lt;/li&gt;
&lt;li&gt;T≈3–8 min: tests finish (usually the slowest)&lt;/li&gt;
&lt;li&gt;T≈4–9 min: review-gate fires&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;End-to-end: roughly &lt;strong&gt;5–10 minutes per PR&lt;/strong&gt;, with &lt;strong&gt;4 AI comments + 1 test report&lt;/strong&gt; appearing on the PR.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The flow inside one review job
&lt;/h2&gt;

&lt;p&gt;Every AI review job is the same 4-step pattern. Here's &lt;code&gt;peer-review&lt;/code&gt; as the template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;peer-review&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;0&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Get PR diff&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;GH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;secrets.GITHUB_TOKEN&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;gh pr diff ${{ github.event.pull_request.number }} &amp;gt; /tmp/pr_diff.txt&lt;/span&gt;
        &lt;span class="s"&gt;head -c 30000 /tmp/pr_diff.txt &amp;gt; /tmp/pr_diff_truncated.txt&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v6&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Peer Review&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;GEMINI_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GEMINI_API_KEY }}&lt;/span&gt;
        &lt;span class="na"&gt;PR_TITLE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.title }}&lt;/span&gt;
        &lt;span class="na"&gt;PR_BODY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.body }}&lt;/span&gt;
        &lt;span class="na"&gt;PR_NUMBER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.number }}&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python3 .github/scripts/review_agent.py peer&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Post Peer Review Comment&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;GH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;secrets.GITHUB_TOKEN&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;gh pr comment ${{ github.event.pull_request.number }} \&lt;/span&gt;
          &lt;span class="s"&gt;--repo ${{ github.repository }} \&lt;/span&gt;
          &lt;span class="s"&gt;--body-file /tmp/review_peer.md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step by step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Checkout&lt;/strong&gt; — full clone (&lt;code&gt;fetch-depth: 0&lt;/code&gt;). Honestly overkill since we use &lt;code&gt;gh pr diff&lt;/code&gt; which hits the GitHub API, not local git. A shallow clone would be fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get the diff&lt;/strong&gt; — &lt;code&gt;gh pr diff &amp;lt;num&amp;gt;&lt;/code&gt; writes the diff to disk, then &lt;code&gt;head -c 30000&lt;/code&gt; truncates to fit the model's context window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the Python agent&lt;/strong&gt; — picks the right persona prompt, posts to Gemini, writes a markdown file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post the comment&lt;/strong&gt; — &lt;code&gt;gh pr comment --body-file&lt;/code&gt; drops the markdown onto the PR.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The four reviewer jobs are nearly identical — only the persona argument changes (&lt;code&gt;peer&lt;/code&gt;, &lt;code&gt;lead&lt;/code&gt;, &lt;code&gt;architect&lt;/code&gt;, &lt;code&gt;security&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  The Python script — one file, four personas
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;PROMPTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;peer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Senior Developer (Peer Reviewer)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;emoji&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;👨‍💻&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;focus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a senior developer doing a peer code review.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Focus on:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1. Code correctness — logic bugs, off-by-one, null checks&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2. Code style — naming, readability, comments&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3. DRY violations&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4. Error handling&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5. Edge cases&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rate: APPROVE, REQUEST_CHANGES, or COMMENT.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lead&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Engineering Lead&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;focus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;architect&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Software Architect&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;focus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;security&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Chief Security Officer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;focus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;focus&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PR #&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PR_NUMBER&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PR_TITLE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Description: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PR_BODY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Diff:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;```
&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;endraw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;diff_text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;25000&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
```&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Respond in markdown. Start with your verdict, then list findings.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&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;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;candidates&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No SDK, no framework. Stdlib &lt;code&gt;urllib&lt;/code&gt;, one POST to Gemini, write the response to &lt;code&gt;/tmp/review_&amp;lt;level&amp;gt;.md&lt;/code&gt;. The whole script is ~120 lines.&lt;/p&gt;

&lt;p&gt;The security persona prompt is the most useful to look at, because it's the one that has actually caught things:&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 security officer reviewing this PR for vulnerabilities.
Check for:
1. OWASP Top 10 — SQL injection, XSS, CSRF, broken auth, SSRF
2. Secrets — hardcoded API keys, tokens, passwords, connection strings
3. Input validation — unsanitized user input, path traversal
4. Authentication/Authorization — missing auth checks, privilege escalation
5. Data exposure — PII leaks in logs, overly permissive CORS
6. Dependency risk — known CVEs in added dependencies
7. Cryptography — weak algorithms, insecure random generation
For each finding, provide: severity (Critical/High/Medium/Low), location, and fix.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It once flagged a &lt;code&gt;JWT_SECRET&lt;/code&gt; I'd inlined into a &lt;code&gt;.env.example&lt;/code&gt; "for convenience." Worth the API spend by itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The flow inside the test-execution job
&lt;/h2&gt;

&lt;p&gt;The test job has a different shape — it actually runs code instead of asking an LLM about code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;test-execution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:14&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
        &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
        &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kheti_sahayak_test&lt;/span&gt;
      &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;5432:5432'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
        &lt;span class="s"&gt;--health-cmd pg_isready&lt;/span&gt;
        &lt;span class="s"&gt;--health-interval 10s&lt;/span&gt;
        &lt;span class="s"&gt;--health-timeout 5s&lt;/span&gt;
        &lt;span class="s"&gt;--health-retries 5&lt;/span&gt;

  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20.x'&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;subosito/flutter-action@v2&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;flutter-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.29.0'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;channel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;stable'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Backend Tests&lt;/span&gt;
      &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kheti_sahayak_backend&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://test:test@localhost:5432/kheti_sahayak_test&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;npm install&lt;/span&gt;
        &lt;span class="s"&gt;npm test -- --coverage --json --outputFile=/tmp/backend_test_results.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real Postgres in a service container&lt;/strong&gt; — no SQLite-in-memory shortcut. The backend tests hit a real DB, the same major version as prod.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;continue-on-error: true&lt;/code&gt; on each suite&lt;/strong&gt; — if Jest fails, Flutter tests still run. The job ends with a markdown report that shows ✅/❌ per suite and gets posted as a comment.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. The gotchas (and the 429 in that screenshot)
&lt;/h2&gt;

&lt;p&gt;Now the part I'd actually change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The sequential L1→L2→L3 chain wastes time.&lt;/strong&gt; Nothing in the lead prompt actually uses peer-review output — each reviewer reads the same diff independently. Running all four review jobs in parallel would cut wall time roughly 3×. The chain made sense when I imagined later reviewers reading earlier reviewers' comments. They don't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;review-gate&lt;/code&gt; doesn't actually gate anything.&lt;/strong&gt; It just echoes results. Branch protection has to do the real enforcement. Easy to miss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Every job re-runs &lt;code&gt;gh pr diff&lt;/code&gt;.&lt;/strong&gt; Cheap but wasteful. A single setup job that uploads the diff as an artifact would be cleaner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. &lt;code&gt;fetch-depth: 0&lt;/code&gt; on every job is overkill.&lt;/strong&gt; &lt;code&gt;gh pr diff&lt;/code&gt; is an API call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. The 429 in the screenshot up top.&lt;/strong&gt; Four jobs hitting Gemini within ~30 seconds of each other, free tier, no backoff. The Python script catches the exception and writes the "Automated review could not be completed" message you see in the screenshot — so the workflow stays green, but the PR ends up with four useless comments. Two fixes I'm considering: add a small retry-with-jittered-backoff in the script, and stagger the jobs (security can wait 60s; it's not on the critical path).&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I built it this way
&lt;/h2&gt;

&lt;p&gt;I wanted &lt;strong&gt;four opinions, not one synthesized one&lt;/strong&gt;. When a real team reviews a PR, the security person notices different things than the architect. Combining the personas into one prompt blurs that. Splitting them into separate jobs costs me a few extra API calls but gives me four distinct comment threads on the PR — which is exactly the shape a human reviewer cluster would have.&lt;/p&gt;

&lt;p&gt;Total cost: roughly &lt;strong&gt;$0.01 per PR&lt;/strong&gt; at Gemini 2.0 Flash prices, plus GitHub Actions minutes (free for me on a personal repo). Cheaper than coffee, more thorough than me reviewing my own PR at 11pm.&lt;/p&gt;




&lt;h2&gt;
  
  
  The files, in full
&lt;/h2&gt;

&lt;p&gt;If you want to drop this into your own repo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Put the workflow at &lt;code&gt;.github/workflows/code-review-agents.yml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Put the script at &lt;code&gt;.github/scripts/review_agent.py&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;GEMINI_API_KEY&lt;/code&gt; to your repo's Actions secrets&lt;/li&gt;
&lt;li&gt;Open a PR&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the whole setup. No external service, no webhook, no infrastructure.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you build something on top of this, I'd love to hear what you change — especially how you handle the rate-limit problem and whether you ever managed to get one reviewer to actually read another's comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>ai</category>
      <category>devops</category>
      <category>automation</category>
    </item>
    <item>
      <title>Stop Wrestling with Merge Conflicts: Automate the Whole Workflow</title>
      <dc:creator>pponali</dc:creator>
      <pubDate>Tue, 12 May 2026 16:08:51 +0000</pubDate>
      <link>https://dev.to/pponali/stop-wrestling-with-merge-conflicts-automate-the-whole-workflow-348m</link>
      <guid>https://dev.to/pponali/stop-wrestling-with-merge-conflicts-automate-the-whole-workflow-348m</guid>
      <description>&lt;p&gt;It's 4:47 PM on a Friday. You've been coding all week on a feature you're proud of. You open a PR, and GitHub greets you with the one message that turns your stomach:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"This branch has conflicts that must be resolved."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You already know what's waiting. The &lt;code&gt;pom.xml&lt;/code&gt; has diverged again. Someone on main bumped a YAML config. There are three Markdown docs with ugly &lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD&lt;/code&gt; markers scattered through them. You're not shipping today.&lt;/p&gt;

&lt;p&gt;Every software developer has lived this moment. And most of us keep solving it the same slow, error-prone, manual way — every single time.&lt;/p&gt;

&lt;p&gt;This post is about why that's a problem, and how the &lt;strong&gt;&lt;code&gt;/resolve-conflicts&lt;/code&gt; Claude Cowork skill&lt;/strong&gt; fixes it — a first-class Claude skill you simply invoke in natural language, and Claude handles the entire workflow end to end.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🧩 &lt;strong&gt;The &lt;code&gt;/resolve-conflicts&lt;/code&gt; skill&lt;/strong&gt; is an installable Claude Cowork skill that automates your entire merge conflict workflow. The source code, shell script, and full documentation are open source on GitHub: &lt;strong&gt;&lt;a href="https://github.com/pponali/claude_skills" rel="noopener noreferrer"&gt;github.com/pponali/claude_skills&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Real Pain Points Behind Merge Conflicts
&lt;/h2&gt;

&lt;p&gt;Merge conflicts feel like a Git problem, but they're really a &lt;em&gt;team coordination&lt;/em&gt; problem made visible. Here's what actually makes them brutal:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They always hit at the worst time.&lt;/strong&gt; You finish a feature after days of work, go to merge, and suddenly you're debugging someone else's schema migration inside your &lt;code&gt;application.yml&lt;/code&gt;. Context-switching mid-sprint to resolve config conflicts is a massive cognitive tax.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The same files conflict over and over.&lt;/strong&gt; In any non-trivial project, you'll find the same suspects in your conflict list week after week: &lt;code&gt;pom.xml&lt;/code&gt;, &lt;code&gt;docker-compose.yml&lt;/code&gt;, &lt;code&gt;*.properties&lt;/code&gt;, localization files, OpenAPI specs. These aren't complex logical conflicts — they're mechanical, repetitive, and boring. Yet they still demand your full attention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manual resolution is error-prone under pressure.&lt;/strong&gt; When you're staring at a wall of &lt;code&gt;=======&lt;/code&gt; markers, it's easy to accidentally keep the wrong version, drop a dependency update, or smash together two configs in a way that silently breaks something. There's no guardrail — just you, your eyes, and your mouse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The feedback loop is slow.&lt;/strong&gt; You resolve conflicts, push, wait for CI, find out you introduced a bug in the config you just "fixed," fix it again, push again. The cycle can eat hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It doesn't scale with team size.&lt;/strong&gt; On a small team, conflicts are annoying. On a team of 20+ engineers all merging into &lt;code&gt;main&lt;/code&gt; frequently, they're a daily tax on every developer's time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Traditional Approach (And Why It Falls Short)
&lt;/h2&gt;

&lt;p&gt;The standard workflow most of us follow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git fetch origin
git merge origin/main
&lt;span class="c"&gt;# ... open every conflicted file in VS Code ...&lt;/span&gt;
&lt;span class="c"&gt;# ... manually choose ours / theirs / both ...&lt;/span&gt;
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Merge branch 'main' into feature/my-thing"&lt;/span&gt;
git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works, but it has serious problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No consistency.&lt;/strong&gt; Different developers make different choices for the same file types. One dev always keeps &lt;code&gt;pom.xml&lt;/code&gt; from main; another always keeps the branch version. The merge history becomes unpredictable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No documentation.&lt;/strong&gt; The merge commit rarely explains &lt;em&gt;why&lt;/em&gt; you resolved conflicts the way you did. Future you (and your teammates) have no idea.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No scalability.&lt;/strong&gt; Every conflict requires a human to sit down and make individual decisions, even for files where the right answer is always the same.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Enter &lt;code&gt;/resolve-conflicts&lt;/code&gt;: A Claude Cowork Skill That Does It All
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;/resolve-conflicts&lt;/code&gt; skill is a &lt;strong&gt;Claude Cowork skill&lt;/strong&gt; — meaning you don't run commands yourself. You just tell Claude what you want in plain English:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;"Merge main into my branch and resolve conflicts"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"My PR shows conflicts — fix them"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Sync my feature branch with origin/main"&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Claude takes it from there. Under the hood it drives &lt;code&gt;.claude/scripts/resolve-conflicts.sh&lt;/code&gt; — a shell script with codified heuristics for every file type in your project. The skill + script together are fully open source:&lt;/p&gt;

&lt;p&gt;📦 &lt;strong&gt;&lt;a href="https://github.com/pponali/claude_skills" rel="noopener noreferrer"&gt;github.com/pponali/claude_skills&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The core idea: &lt;em&gt;most conflicts in most projects follow predictable patterns&lt;/em&gt;. Once those patterns are encoded as a Claude skill, you never have to think about them again — just describe the problem and let Claude handle it.&lt;/p&gt;

&lt;p&gt;Here's what Claude does behind the scenes when you invoke the skill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Step 1 — Claude runs a dry-run first and summarises what it found&lt;/span&gt;
.claude/scripts/resolve-conflicts.sh origin/main &lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="c"&gt;# Step 2 — After your confirmation, Claude runs for real&lt;/span&gt;
.claude/scripts/resolve-conflicts.sh origin/main &lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude handles everything: stashing uncommitted changes, merging, resolving each conflict using the right heuristic, staging, creating an auditable commit, pushing, and optionally checking PR status via the &lt;code&gt;gh&lt;/code&gt; CLI. You just review and confirm.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Resolution Heuristics Work
&lt;/h2&gt;

&lt;p&gt;The script doesn't guess — it applies explicit, per-file-type rules:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File pattern&lt;/th&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Rationale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pom.xml&lt;/code&gt;, &lt;code&gt;*/pom.xml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Keep branch (&lt;code&gt;--ours&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Feature branch usually holds newer or WIP dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*.properties&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keep branch&lt;/td&gt;
&lt;td&gt;Branch has local/feature-specific config values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;*.yaml&lt;/code&gt; / &lt;code&gt;*.yml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Keep branch&lt;/td&gt;
&lt;td&gt;Same reasoning as properties&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker-compose.*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keep branch&lt;/td&gt;
&lt;td&gt;Preserves the current service topology&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keep both (concatenated)&lt;/td&gt;
&lt;td&gt;Docs from both sides are usually additive, not contradictory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*.java&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keep branch&lt;/td&gt;
&lt;td&gt;Active feature work lives on the branch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;*.js&lt;/code&gt;, &lt;code&gt;*.ts&lt;/code&gt;, &lt;code&gt;*.tsx&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Keep branch&lt;/td&gt;
&lt;td&gt;Same as Java&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;anything else&lt;/td&gt;
&lt;td&gt;Keep branch + warning&lt;/td&gt;
&lt;td&gt;Safe default; flagged for manual review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Keep branch" means the &lt;code&gt;HEAD&lt;/code&gt; side of the merge — your feature branch. The strategy is opinionated by design. If your project has different conventions, you update the script once, and every future merge follows the new rules automatically.&lt;/p&gt;

&lt;p&gt;The Markdown strategy is particularly clever: instead of picking one side, it concatenates both. Documentation is almost always additive — two sections from different branches are usually both worth keeping, so the merge result includes both and a human can clean it up later without losing information.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dry-Run Is Your Best Friend
&lt;/h2&gt;

&lt;p&gt;Before running for real, always do a dry run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;.claude/scripts/resolve-conflicts.sh origin/main &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output tells you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which files have conflicts&lt;/li&gt;
&lt;li&gt;Which rule the script will apply to each one&lt;/li&gt;
&lt;li&gt;Any files in the "unknown" bucket (these get &lt;code&gt;--ours&lt;/code&gt; by default and a warning logged)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the moment to catch surprises. If &lt;code&gt;main&lt;/code&gt; bumped a critical version in &lt;code&gt;pom.xml&lt;/code&gt; that your branch must absorb, you'll see it here. You can let the script handle everything else and manually fix just that one file afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify, Don't Assume
&lt;/h2&gt;

&lt;p&gt;After the script runs, always check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Should show your merge commit&lt;/span&gt;
git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;-3&lt;/span&gt;

&lt;span class="c"&gt;# MUST be empty — any output here means unresolved conflicts&lt;/span&gt;
git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; &lt;span class="nt"&gt;--diff-filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;U

&lt;span class="c"&gt;# Quick overview of what changed&lt;/span&gt;
git show HEAD &lt;span class="nt"&gt;--stat&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your project has a fast smoke test — &lt;code&gt;mvn -q -DskipTests verify&lt;/code&gt;, &lt;code&gt;npm run typecheck&lt;/code&gt;, &lt;code&gt;docker compose config&lt;/code&gt; — run it now. Heuristic resolutions are fast but not infallible.&lt;/p&gt;

&lt;p&gt;If you're using the &lt;code&gt;gh&lt;/code&gt; CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh &lt;span class="nb"&gt;pr &lt;/span&gt;view &amp;lt;number&amp;gt; &lt;span class="nt"&gt;--json&lt;/span&gt; mergeable,mergeStateStatus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: GitHub takes 1–2 minutes to recompute mergeability after a push. Don't panic if it shows conflicts for a moment after you push.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to Use the Script
&lt;/h2&gt;

&lt;p&gt;The script is great for mechanical conflicts, but there are cases where you need a human in the loop:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Core business logic in &lt;code&gt;.java&lt;/code&gt; / &lt;code&gt;.ts&lt;/code&gt; / &lt;code&gt;.js&lt;/code&gt; files where both sides have unique, non-overlapping changes.&lt;/strong&gt; The script will keep the branch version and silently drop main's changes. If both sides implemented different parts of the same feature, you need to merge them manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A critical dependency update in &lt;code&gt;pom.xml&lt;/code&gt; that your branch must consume.&lt;/strong&gt; The script keeps your branch version. After it runs, manually edit the file, &lt;code&gt;git add&lt;/code&gt;, and &lt;code&gt;git commit --amend --no-edit&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Files in the "unknown" bucket&lt;/strong&gt; (&lt;code&gt;*.sql&lt;/code&gt;, &lt;code&gt;*.proto&lt;/code&gt;, &lt;code&gt;*.kt&lt;/code&gt;, etc.). The script flags these but defaults to &lt;code&gt;--ours&lt;/code&gt;. Open them, check if main's changes matter, resolve manually, then fold into the merge commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# After manually editing the file:&lt;/span&gt;
git add path/to/file.sql
git commit &lt;span class="nt"&gt;--amend&lt;/span&gt; &lt;span class="nt"&gt;--no-edit&lt;/span&gt;
git push &lt;span class="nt"&gt;--force-with-lease&lt;/span&gt; origin your-branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern that works best: let the script handle the 80% of mechanical conflicts, then manually patch the 20% that need actual reasoning. You get speed &lt;em&gt;and&lt;/em&gt; correctness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Payoff
&lt;/h2&gt;

&lt;p&gt;Once your team adopts this workflow, a few things change:&lt;/p&gt;

&lt;p&gt;Merge commits become consistent and auditable. The commit message documents which strategy was applied to which files. Six months from now, you can look at a merge commit and understand exactly what happened.&lt;/p&gt;

&lt;p&gt;Conflict resolution stops being a senior-developer task. The rules are encoded in a script, not in someone's head. A junior developer can merge safely without needing to ask "should I keep ours or theirs for the YAML?".&lt;/p&gt;

&lt;p&gt;Friday afternoon PRs stop being feared. When conflict resolution is one command instead of a half-hour of careful manual editing, you stop dreading the merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started: Install the Claude Skill
&lt;/h2&gt;

&lt;p&gt;Everything — the skill definition, the shell script, and full documentation — is in the open source repo:&lt;/p&gt;

&lt;p&gt;📦 &lt;strong&gt;&lt;a href="https://github.com/pponali/claude_skills" rel="noopener noreferrer"&gt;github.com/pponali/claude_skills&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Add the script to your project&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; .claude/scripts
curl &lt;span class="nt"&gt;-o&lt;/span&gt; .claude/scripts/resolve-conflicts.sh &lt;span class="se"&gt;\&lt;/span&gt;
  https://raw.githubusercontent.com/pponali/claude_skills/master/.claude/scripts/resolve-conflicts.sh
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x .claude/scripts/resolve-conflicts.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once installed, just open Claude in Cowork mode and describe the problem. Claude will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run pre-flight checks (detects if you're already mid-merge)&lt;/li&gt;
&lt;li&gt;Do a dry run and summarise exactly which files it will touch and how&lt;/li&gt;
&lt;li&gt;Ask for your confirmation before committing anything&lt;/li&gt;
&lt;li&gt;Merge, resolve, commit, push — and report back&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You're always in control. Claude handles the mechanical parts, you handle the judgment calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running the script directly&lt;/strong&gt; (without Claude) is also supported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Dry run first — always&lt;/span&gt;
.claude/scripts/resolve-conflicts.sh origin/main &lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="c"&gt;# Then for real&lt;/span&gt;
.claude/scripts/resolve-conflicts.sh origin/main &lt;span class="nb"&gt;false&lt;/span&gt;

&lt;span class="c"&gt;# Merge a different branch&lt;/span&gt;
.claude/scripts/resolve-conflicts.sh origin/feat/payment &lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Full Script
&lt;/h2&gt;

&lt;p&gt;Drop this into &lt;code&gt;.claude/scripts/resolve-conflicts.sh&lt;/code&gt; in your repo and make it executable (&lt;code&gt;chmod +x&lt;/code&gt;). It's self-contained — no external dependencies beyond &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;git&lt;/code&gt;, and optionally the &lt;code&gt;gh&lt;/code&gt; CLI for PR status checks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# =============================================================================&lt;/span&gt;
&lt;span class="c"&gt;# resolve-conflicts.sh&lt;/span&gt;
&lt;span class="c"&gt;# Automated merge-conflict resolution with per-file-type heuristics.&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# Usage:&lt;/span&gt;
&lt;span class="c"&gt;#   .claude/scripts/resolve-conflicts.sh [target-branch] [dry-run]&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# Arguments:&lt;/span&gt;
&lt;span class="c"&gt;#   target-branch  Branch to merge in. Default: origin/main&lt;/span&gt;
&lt;span class="c"&gt;#   dry-run        true|false. Default: false&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# Examples:&lt;/span&gt;
&lt;span class="c"&gt;#   .claude/scripts/resolve-conflicts.sh                          # merge origin/main, for real&lt;/span&gt;
&lt;span class="c"&gt;#   .claude/scripts/resolve-conflicts.sh origin/main true        # dry-run first&lt;/span&gt;
&lt;span class="c"&gt;#   .claude/scripts/resolve-conflicts.sh origin/feat/payment false&lt;/span&gt;
&lt;span class="c"&gt;# =============================================================================&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;TARGET_BRANCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;/main&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;DRY_RUN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;false&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Colour helpers&lt;/span&gt;
&lt;span class="nv"&gt;RED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'\033[0;31m'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;YELLOW&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'\033[1;33m'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'\033[0;32m'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;CYAN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'\033[0;36m'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;NC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'\033[0m'&lt;/span&gt;
info&lt;span class="o"&gt;()&lt;/span&gt;    &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CYAN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;[resolve-conflicts]&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NC&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
success&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;[resolve-conflicts]&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NC&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
warn&lt;span class="o"&gt;()&lt;/span&gt;    &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;YELLOW&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;[resolve-conflicts] WARN:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NC&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
error&lt;span class="o"&gt;()&lt;/span&gt;   &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RED&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;[resolve-conflicts] ERROR:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NC&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;# Pre-flight&lt;/span&gt;
&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--verify&lt;/span&gt; MERGE_HEAD &amp;amp;&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;error &lt;span class="s2"&gt;"Repo is already mid-merge (MERGE_HEAD exists)."&lt;/span&gt;
  error &lt;span class="s2"&gt;"Run 'git merge --abort' to cancel the previous merge, then retry."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;CURRENT_BRANCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--abbrev-ref&lt;/span&gt; HEAD&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
info &lt;span class="s2"&gt;"Current branch : &lt;/span&gt;&lt;span class="nv"&gt;$CURRENT_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
info &lt;span class="s2"&gt;"Target branch  : &lt;/span&gt;&lt;span class="nv"&gt;$TARGET_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
info &lt;span class="s2"&gt;"Dry run        : &lt;/span&gt;&lt;span class="nv"&gt;$DRY_RUN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

info &lt;span class="s2"&gt;"Fetching origin..."&lt;/span&gt;
git fetch origin &lt;span class="nt"&gt;--quiet&lt;/span&gt;

&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;# Stash any uncommitted changes&lt;/span&gt;
&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="nv"&gt;STASHED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; git diff &lt;span class="nt"&gt;--quiet&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; git diff &lt;span class="nt"&gt;--cached&lt;/span&gt; &lt;span class="nt"&gt;--quiet&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;info &lt;span class="s2"&gt;"Stashing local changes..."&lt;/span&gt;
  git stash push &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"resolve-conflicts auto-stash &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;STASHED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;# Attempt the merge&lt;/span&gt;
&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
info &lt;span class="s2"&gt;"Merging &lt;/span&gt;&lt;span class="nv"&gt;$TARGET_BRANCH&lt;/span&gt;&lt;span class="s2"&gt; into &lt;/span&gt;&lt;span class="nv"&gt;$CURRENT_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;..."&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;git merge &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TARGET_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--no-edit&lt;/span&gt; &lt;span class="nt"&gt;--no-commit&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;success &lt;span class="s2"&gt;"Merge completed with no conflicts. Committing..."&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DRY_RUN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"false"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;git commit &lt;span class="nt"&gt;--no-edit&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Merge &lt;/span&gt;&lt;span class="nv"&gt;$TARGET_BRANCH&lt;/span&gt;&lt;span class="s2"&gt; into &lt;/span&gt;&lt;span class="nv"&gt;$CURRENT_BRANCH&lt;/span&gt;&lt;span class="s2"&gt; (no conflicts)"&lt;/span&gt;
    git push origin &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CURRENT_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    success &lt;span class="s2"&gt;"Pushed."&lt;/span&gt;
  &lt;span class="k"&gt;else
    &lt;/span&gt;git merge &lt;span class="nt"&gt;--abort&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
    &lt;/span&gt;info &lt;span class="s2"&gt;"[DRY RUN] No conflicts — merge would succeed cleanly."&lt;/span&gt;
  &lt;span class="k"&gt;fi&lt;/span&gt;
  &lt;span class="nv"&gt;$STASHED&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git stash pop &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
  exit &lt;/span&gt;0
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;CONFLICTED_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; &lt;span class="nt"&gt;--diff-filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;U&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONFLICTED_FILES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;warn &lt;span class="s2"&gt;"Merge stopped but no conflict markers found. Check git status."&lt;/span&gt;
  git merge &lt;span class="nt"&gt;--abort&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
  &lt;span class="nv"&gt;$STASHED&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git stash pop &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
  exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;info &lt;span class="s2"&gt;"Conflicted files:"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONFLICTED_FILES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; f&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  - &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;# Resolution heuristics&lt;/span&gt;
&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
resolve_file&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="nv"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;

  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in
    &lt;/span&gt;pom.xml|&lt;span class="k"&gt;*&lt;/span&gt;/pom.xml&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nv"&gt;strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ours"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="nv"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"pom.xml — keep branch (feature deps)"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;.properties&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nv"&gt;strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ours"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="nv"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.properties — keep branch (feature config)"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;.yaml|&lt;span class="k"&gt;*&lt;/span&gt;.yml&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nv"&gt;strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ours"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="nv"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.yaml/yml — keep branch (feature config)"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
    docker-compose&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;/docker-compose&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nv"&gt;strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ours"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="nv"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"docker-compose — keep branch (feature topology)"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;.md&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nv"&gt;strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"union"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.md — keep both (documentation is additive)"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;.java&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nv"&gt;strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ours"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="nv"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.java — keep branch (active feature work)"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;.js|&lt;span class="k"&gt;*&lt;/span&gt;.ts|&lt;span class="k"&gt;*&lt;/span&gt;.tsx|&lt;span class="k"&gt;*&lt;/span&gt;.jsx&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nv"&gt;strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ours"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="nv"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.js/ts — keep branch (active feature work)"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nv"&gt;strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ours"&lt;/span&gt;
      &lt;span class="nv"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"unknown type — defaulting to branch; MANUAL REVIEW RECOMMENDED"&lt;/span&gt;
      warn &lt;span class="s2"&gt;"No rule for: &lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt; — keeping branch version. Please verify manually."&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;esac&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DRY_RUN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;info &lt;span class="s2"&gt;"[DRY RUN] &lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt; → &lt;/span&gt;&lt;span class="nv"&gt;$strategy&lt;/span&gt;&lt;span class="s2"&gt;  (&lt;/span&gt;&lt;span class="nv"&gt;$reason&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
    &lt;span class="k"&gt;return
  fi

  &lt;/span&gt;info &lt;span class="s2"&gt;"Resolving: &lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt; → &lt;/span&gt;&lt;span class="nv"&gt;$strategy&lt;/span&gt;&lt;span class="s2"&gt;  (&lt;/span&gt;&lt;span class="nv"&gt;$reason&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$strategy&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ours"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;git checkout &lt;span class="nt"&gt;--ours&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    git add &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$strategy&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"union"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;local &lt;/span&gt;ours theirs
    &lt;span class="nv"&gt;ours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git show :2:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nv"&gt;theirs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git show :3:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ours&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"---"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;!-- merged from &lt;/span&gt;&lt;span class="nv"&gt;$TARGET_BRANCH&lt;/span&gt;&lt;span class="s2"&gt; --&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$theirs&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    git add &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; file&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; resolve_file &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONFLICTED_FILES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c"&gt;# Commit and push&lt;/span&gt;
&lt;span class="c"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DRY_RUN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;info &lt;span class="s2"&gt;"[DRY RUN] Complete. No files were changed."&lt;/span&gt;
  git merge &lt;span class="nt"&gt;--abort&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
  &lt;span class="nv"&gt;$STASHED&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git stash pop &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
  exit &lt;/span&gt;0
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;STILL_CONFLICTED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; &lt;span class="nt"&gt;--diff-filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;U 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STILL_CONFLICTED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;error &lt;span class="s2"&gt;"Some files are still unresolved — resolve manually, 'git add', then 'git commit'."&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STILL_CONFLICTED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;$STASHED&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git stash pop &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
  exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;COMMIT_MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Merge &lt;/span&gt;&lt;span class="nv"&gt;$TARGET_BRANCH&lt;/span&gt;&lt;span class="s2"&gt; into &lt;/span&gt;&lt;span class="nv"&gt;$CURRENT_BRANCH&lt;/span&gt;&lt;span class="s2"&gt; (auto-resolved)

Heuristics applied:
&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONFLICTED_FILES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; f&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  - &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;

Generated by resolve-conflicts.sh"&lt;/span&gt;

git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COMMIT_MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
success &lt;span class="s2"&gt;"Merge commit created."&lt;/span&gt;

git push origin &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CURRENT_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
success &lt;span class="s2"&gt;"Pushed to origin/&lt;/span&gt;&lt;span class="nv"&gt;$CURRENT_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;."&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;$STASHED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;info &lt;span class="s2"&gt;"Restoring stashed changes..."&lt;/span&gt;
  git stash pop &lt;span class="o"&gt;||&lt;/span&gt; warn &lt;span class="s2"&gt;"Stash pop failed — run 'git stash list' to inspect."&lt;/span&gt;
&lt;span class="k"&gt;fi

if &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; gh &amp;amp;&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;info &lt;span class="s2"&gt;"Checking PR status (GitHub may take ~2 min to recompute)..."&lt;/span&gt;
  gh &lt;span class="nb"&gt;pr &lt;/span&gt;view &lt;span class="nt"&gt;--json&lt;/span&gt; mergeable,mergeStateStatus 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
    | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import json,sys; d=json.load(sys.stdin); print(f'  mergeable={d[&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;mergeable&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;]}  state={d[&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;mergeStateStatus&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;]}')"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="k"&gt;fi

&lt;/span&gt;success &lt;span class="s2"&gt;"Done. Run 'git log --oneline -3' to verify the merge commit."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save it, make it executable, and you're ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; .claude/scripts
&lt;span class="c"&gt;# paste the script above into .claude/scripts/resolve-conflicts.sh&lt;/span&gt;
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x .claude/scripts/resolve-conflicts.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Merge conflicts aren't going away. As long as multiple people work on the same codebase, there will be divergence. But the amount of &lt;em&gt;manual, repetitive&lt;/em&gt; work they require is entirely within your control. Codify your team's conventions, automate the mechanics, and save the human judgment for the conflicts that actually need it.&lt;/p&gt;

&lt;p&gt;Your future Friday self will thank you.&lt;/p&gt;

</description>
      <category>git</category>
      <category>devops</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Circuit Breakers Under Stress: Anatomy of a Payment Cascade</title>
      <dc:creator>pponali</dc:creator>
      <pubDate>Sun, 10 May 2026 14:10:46 +0000</pubDate>
      <link>https://dev.to/pponali/circuit-breakers-under-stress-anatomy-of-a-payment-cascade-hn0</link>
      <guid>https://dev.to/pponali/circuit-breakers-under-stress-anatomy-of-a-payment-cascade-hn0</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%2Fh9zz6gvcc6b1m0n03nv1.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%2Fh9zz6gvcc6b1m0n03nv1.png" alt=" " width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A flash sale hit us at 10x baseline RPS. Within four minutes, our Payment Service circuit breaker tripped to &lt;strong&gt;OPEN&lt;/strong&gt;, error rate climbed to 92%, and p99 latency on the payment path went from 200ms to 14.2 seconds. Here's the part nobody tells you on the conference circuit: the circuit breaker didn't fail. It worked exactly as designed. The failure was everywhere else.&lt;/p&gt;

&lt;p&gt;This is a postmortem of what we saw, why Resilience4j's defaults weren't enough, and the four changes that made the next sale boring.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Standard Java microservices stack. Spring Cloud Gateway in front, JWT auth via Keycloak, Resilience4j wrapping every outbound call. Payment Service synchronously calls Stripe. Order Service synchronously calls Payment. PostgreSQL for orders, Redis for circuit breaker state, Kafka for the dead-letter queue.&lt;/p&gt;

&lt;p&gt;Six services. Five circuit breakers. One very stressed thread pool.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 10x RPS actually does
&lt;/h2&gt;

&lt;p&gt;Baseline was around 1,000 RPS. The flash sale pushed us to 10,243. The edge layer absorbed it fine — NGINX did its job, the rate limiter degraded gracefully, the CDN cached anything cacheable. Spring Cloud Gateway routed cleanly.&lt;/p&gt;

&lt;p&gt;The wheels came off at the Payment Service. Stripe's p99 latency under load climbed from a healthy 800ms to 14.2 seconds. That doesn't sound catastrophic until you do the math: every Payment thread now holds for ~14s instead of &amp;lt;1s. With a fixed thread pool, throughput collapses long before the breaker notices.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# What we had — Resilience4j defaults, lightly tuned&lt;/span&gt;
&lt;span class="na"&gt;resilience4j.circuitbreaker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;instances&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paymentService&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;failureRateThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;
      &lt;span class="na"&gt;slidingWindowSize&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
      &lt;span class="na"&gt;slidingWindowType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;COUNT_BASED&lt;/span&gt;
      &lt;span class="na"&gt;waitDurationInOpenState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;permittedNumberOfCallsInHalfOpenState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 50% failure threshold over 100 calls means the breaker waits for 50 failures before tripping. At 10x load with timeouts, that's roughly four minutes of users staring at spinners. By the time the breaker opened, the thread pool was already 98% saturated.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cascade, step by step
&lt;/h2&gt;

&lt;p&gt;The order matters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Flash-sale spike hits the gateway at 10x RPS.&lt;/li&gt;
&lt;li&gt;Order Service synchronously calls Payment for every checkout.&lt;/li&gt;
&lt;li&gt;Stripe's p99 spikes to 14s under provider-side load.&lt;/li&gt;
&lt;li&gt;Payment Service threads block on those timeouts.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;failureRateThreshold=50%&lt;/code&gt; breached → Payment CB transitions to &lt;strong&gt;OPEN&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Subsequent calls fail-fast → fallback handler enqueues "deferred order" responses to Kafka.&lt;/li&gt;
&lt;li&gt;Order Service's own CB drops to &lt;strong&gt;HALF-OPEN&lt;/strong&gt;, probing with limited concurrency.&lt;/li&gt;
&lt;li&gt;Bulkhead isolation prevents the cascade from reaching Inventory, Notifications, or User services.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 8 is the only reason this incident wasn't a full-platform outage. Without per-endpoint bulkheads, a slow Stripe would have eaten every thread in the gateway's pool, and User Service login requests would have queued behind dead Payment calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  The state machine, practically
&lt;/h2&gt;

&lt;p&gt;If you've only read the docs, the circuit breaker looks like a tidy three-state diagram. In production it's noisier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Resilience4j state transitions, simplified&lt;/span&gt;
&lt;span class="nc"&gt;CircuitBreaker&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CircuitBreaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"paymentService"&lt;/span&gt;&lt;span class="o"&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;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getEventPublisher&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onStateTransition&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;warn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CB {} : {} -&amp;gt; {}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
          &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCircuitBreakerName&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
          &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStateTransition&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getFromState&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
          &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStateTransition&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getToState&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
      &lt;span class="n"&gt;meterRegistry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;counter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cb.transition"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
          &lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCircuitBreakerName&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
          &lt;span class="s"&gt;"to"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStateTransition&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getToState&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
      &lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;increment&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That listener saved us during the postmortem. We could replay exactly when each breaker tripped, when probing started, and which trial calls failed. If you don't emit metrics on every state transition, you're flying blind.&lt;/p&gt;

&lt;p&gt;The HALF-OPEN state is the dangerous one. Resilience4j permits a small number of trial calls; if any of them fail, you slam back to OPEN for another &lt;code&gt;waitDuration&lt;/code&gt;. Set the trial pool too low and you'll never recover; set it too high and you'll hammer a still-broken downstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four changes that fixed it
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Tighter, faster breakers
&lt;/h3&gt;

&lt;p&gt;We dropped the threshold and shrunk the window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resilience4j.circuitbreaker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;instances&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paymentService&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;failureRateThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;          &lt;span class="c1"&gt;# was 50&lt;/span&gt;
      &lt;span class="na"&gt;slowCallRateThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;          &lt;span class="c1"&gt;# NEW — slow calls also count&lt;/span&gt;
      &lt;span class="na"&gt;slowCallDurationThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2s&lt;/span&gt;      &lt;span class="c1"&gt;# NEW&lt;/span&gt;
      &lt;span class="na"&gt;slidingWindowSize&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;              &lt;span class="c1"&gt;# was 100&lt;/span&gt;
      &lt;span class="na"&gt;minimumNumberOfCalls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
      &lt;span class="na"&gt;waitDurationInOpenState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;       &lt;span class="c1"&gt;# was 30s&lt;/span&gt;
      &lt;span class="na"&gt;permittedNumberOfCallsInHalfOpenState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two non-obvious knobs matter here. &lt;code&gt;slowCallRateThreshold&lt;/code&gt; lets you trip on latency, not just errors — critical when a downstream is dying slowly rather than 500-ing. And the smaller window means the breaker reacts in seconds, not minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Per-endpoint bulkheads
&lt;/h3&gt;

&lt;p&gt;A single thread pool for "Payment Service" is too coarse. Split by downstream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolBulkhead&lt;/span&gt; &lt;span class="nf"&gt;stripeBulkhead&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;ThreadPoolBulkheadConfig&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolBulkheadConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;custom&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxThreadPoolSize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;coreThreadPoolSize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;queueCapacity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;keepAliveDuration&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofMillis&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolBulkhead&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"stripe"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolBulkhead&lt;/span&gt; &lt;span class="nf"&gt;fraudBulkhead&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Smaller — fraud is allowed to be slow, not allowed to starve payment&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolBulkhead&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"fraud"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;ThreadPoolBulkheadConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;custom&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxThreadPoolSize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;coreThreadPoolSize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a slow fraud engine can't drain Stripe's threads, and vice versa. Bulkhead-per-dependency is more YAML, but it's the only way to guarantee isolation when one downstream misbehaves.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Async outbox + Kafka retry
&lt;/h3&gt;

&lt;p&gt;The synchronous &lt;code&gt;Order → Payment → Stripe&lt;/code&gt; chain was the real sin. We moved Payment to an outbox pattern: orders write a payment intent to Postgres in the same transaction, a relay publishes to Kafka, and a worker calls Stripe asynchronously. The user gets an immediate "order placed" response; the charge happens within seconds, with retries handled by the consumer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="nf"&gt;placeOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderRepo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;outboxRepo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OutboxEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"payment.charge.requested"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;objectMapper&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeValueAsString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;payment&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// returns in &amp;lt;50ms regardless of Stripe latency&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decoupling time-of-order from time-of-charge means a 14-second Stripe doesn't translate to a 14-second user experience. It also gives us natural retry and dead-lettering through Kafka, instead of bolting retry logic onto every caller.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. HPA on RPS and queue depth
&lt;/h3&gt;

&lt;p&gt;The Payment Service was scaled on CPU, which is useless when threads are blocked on I/O. We swapped to a custom Prometheus metric — RPS plus Kafka consumer lag — and let the HPA add pods when the queue grew faster than it drained. CPU never crossed 40% during the incident; if we'd been watching the right signal, we'd have scaled out three minutes earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell past me
&lt;/h2&gt;

&lt;p&gt;The circuit breaker is a fire alarm, not a fire suppression system. By the time it trips, you've already had a fire for a while. The real defenses are the things that stop the fire from starting: bulkhead isolation per downstream, slow-call detection, async boundaries on anything you don't fully control, and autoscaling on signals that actually correlate with load.&lt;/p&gt;

&lt;p&gt;Resilience4j is excellent. The defaults are not your friend in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;If you take three things from this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trip on latency, not just errors.&lt;/strong&gt; &lt;code&gt;slowCallRateThreshold&lt;/code&gt; is the most underused knob in Resilience4j.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One bulkhead per downstream, always.&lt;/strong&gt; Coarse pools will betray you the moment two dependencies fail differently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synchronous chains across third-party APIs are tech debt.&lt;/strong&gt; An outbox + queue is more code, but it's the difference between a postmortem and an incident report.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next flash sale ran 12x baseline. Payment p99 stayed under 600ms. Nobody paged.&lt;/p&gt;

</description>
      <category>java</category>
      <category>microservices</category>
      <category>devops</category>
      <category>architecture</category>
    </item>
    <item>
      <title>After the Skill Vault: 3 More Hidden Token Sinks in Claude Code</title>
      <dc:creator>pponali</dc:creator>
      <pubDate>Fri, 08 May 2026 00:48:13 +0000</pubDate>
      <link>https://dev.to/pponali/after-the-skill-vault-3-more-hidden-token-sinks-in-claude-code-32ek</link>
      <guid>https://dev.to/pponali/after-the-skill-vault-3-more-hidden-token-sinks-in-claude-code-32ek</guid>
      <description>&lt;p&gt;If you read my earlier post on the &lt;a href="https://dev.to/pponali/how-i-cut-claude-code-token-consumption-by-96-with-the-skill-vault-pattern-9d1"&gt;Skill Vault pattern&lt;/a&gt;, you know I cut Claude Code's per-session overhead by 96%. After living with it for a few weeks, I went looking for what was &lt;em&gt;still&lt;/em&gt; eating tokens — and found three more sinks worth killing.&lt;/p&gt;

&lt;p&gt;This is the follow-up: smaller wins individually, but together they shaved another &lt;strong&gt;~5,000 tokens off every single session&lt;/strong&gt; with zero capability lost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Tokens Were Still Hiding
&lt;/h2&gt;

&lt;p&gt;After the vault, my baseline was around 51K tokens per session. I dug into the system prompt to see what remained:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Skill list (still): &lt;strong&gt;~3K tokens&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Project &lt;code&gt;CLAUDE.md&lt;/code&gt;: &lt;strong&gt;~2K tokens&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;claude-mem auto-injected timeline: &lt;strong&gt;~2K tokens&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Plugin hook reminders: &lt;strong&gt;~1K tokens&lt;/strong&gt; (some recurring per turn)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three of these were either unnecessary or way overweight. Here's how I killed each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sink #1: A Bloated Root CLAUDE.md (~1.5K tokens)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; files auto-load into every session that touches the repo. Mine had grown to &lt;strong&gt;326 lines / 8 KB&lt;/strong&gt; with quickstart instructions for every component:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flutter dev commands&lt;/li&gt;
&lt;li&gt;Backend &lt;code&gt;npm&lt;/code&gt; scripts&lt;/li&gt;
&lt;li&gt;React build steps&lt;/li&gt;
&lt;li&gt;ML service Python setup&lt;/li&gt;
&lt;li&gt;A duplicate gstack skill listing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem: I was loading &lt;strong&gt;every component's instructions on every turn&lt;/strong&gt;, even when I was only working in one of them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix: Hierarchical CLAUDE.md Files
&lt;/h3&gt;

&lt;p&gt;Claude Code loads &lt;code&gt;CLAUDE.md&lt;/code&gt; &lt;strong&gt;hierarchically&lt;/strong&gt; — only the ones in your current working tree get pulled in. So instead of one fat root file, I split it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;khetisahayak/
├── CLAUDE.md                    # 55 lines — overview, ports, creds only
├── kheti_sahayak_app/CLAUDE.md  # Flutter details
├── frontend/CLAUDE.md           # React details
├── ml/CLAUDE.md                 # ML service details
└── kheti_sahayak_backend/CLAUDE.md  # already existed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The root now contains only:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Project overview (5 lines)&lt;/li&gt;
&lt;li&gt;Service ports table&lt;/li&gt;
&lt;li&gt;Test credentials&lt;/li&gt;
&lt;li&gt;Cross-cutting auth + DB notes&lt;/li&gt;
&lt;li&gt;Troubleshooting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Component-specific details only load when I'm actually working in that subdir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; root CLAUDE.md trimmed from 326 → 55 lines (8 KB → 1.6 KB). &lt;strong&gt;~1.5K tokens saved per session.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Sink #2: Plugin SessionStart Hooks Injecting "Helpful" Context
&lt;/h2&gt;

&lt;p&gt;I use &lt;a href="https://github.com/thedotmack/claude-mem" rel="noopener noreferrer"&gt;claude-mem&lt;/a&gt; for persistent memory across sessions. Genuinely useful. But it has a &lt;code&gt;SessionStart&lt;/code&gt; hook that auto-injects a &lt;strong&gt;timeline of recent observations&lt;/strong&gt; at the top of every conversation — about 50 entries, ~2K tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;S499 Indeed Auto-Apply — User asked how to automate job applications
S498 Indeed MCP Integration Query — clarifying JobSpy vs Apify
2337 12:33p ✅ systemd/install.sh — Backend Services Enabled
... 47 more lines
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I almost never &lt;em&gt;needed&lt;/em&gt; this auto-recall. When I want past context, I call &lt;code&gt;mem-search&lt;/code&gt; explicitly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix: Disable the Auto-Inject, Keep the Memory
&lt;/h3&gt;

&lt;p&gt;The hook config lives at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.claude/plugins/cache/thedotmack/claude-mem/&amp;lt;version&amp;gt;/hooks/hooks.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;SessionStart&lt;/code&gt; array has three hooks. The third is the timeline injection:&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"... node bun-runner.js worker-service.cjs hook claude-code context ..."&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;I removed just that one entry. The other two SessionStart hooks (install + worker-start) and the recording hooks (&lt;code&gt;PostToolUse&lt;/code&gt;, &lt;code&gt;Stop&lt;/code&gt;, &lt;code&gt;SessionEnd&lt;/code&gt;) stay intact, so memory is still being captured. The MCP search server still works — I just have to ask for it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Always back up before editing plugin internals&lt;/span&gt;
&lt;span class="nb"&gt;cp &lt;/span&gt;hooks.json hooks.json.bak
&lt;span class="c"&gt;# Remove the 3rd SessionStart hook (jq or manual edit)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; ~2K tokens saved per session. Memory still works on demand.&lt;/p&gt;

&lt;p&gt;⚠️ Caveat: editing a file in &lt;code&gt;~/.claude/plugins/cache/&lt;/code&gt; will be overwritten on plugin upgrade. For durability, mirror the change in your user-level &lt;code&gt;~/.claude/settings.json&lt;/code&gt; hooks block, or add a small post-upgrade re-patch script.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sink #3: Round 2 of the Skill Vault (~1.4K tokens)
&lt;/h2&gt;

&lt;p&gt;The original vault was a one-time bulk move. After several weeks of actual usage, I saw which skills I'd installed and &lt;strong&gt;never touched&lt;/strong&gt;. The vault was overdue for a second pass.&lt;/p&gt;

&lt;p&gt;Audit script (same as the first post, run again):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in&lt;/span&gt; ~/.claude/skills/&lt;span class="k"&gt;*&lt;/span&gt;/SKILL.md&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$size&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ended up vaulting &lt;strong&gt;27 more skills&lt;/strong&gt; out of 73 actively loaded:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;8 marketing skills&lt;/strong&gt; (&lt;code&gt;mkt-content&lt;/code&gt;, &lt;code&gt;mkt-seo&lt;/code&gt;, &lt;code&gt;mkt-social&lt;/code&gt;, etc.) — I do real marketing in a separate context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6 research skills&lt;/strong&gt; (&lt;code&gt;research&lt;/code&gt;, &lt;code&gt;research-deep&lt;/code&gt;, &lt;code&gt;research-report&lt;/code&gt;, etc.) — episodic, not daily&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5 niche tools&lt;/strong&gt; (&lt;code&gt;obsidian-vault&lt;/code&gt;, &lt;code&gt;make-pdf&lt;/code&gt;, &lt;code&gt;pair-agent&lt;/code&gt;, &lt;code&gt;setup-browser-cookies&lt;/code&gt;, &lt;code&gt;open-gstack-browser&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4 design-heavy&lt;/strong&gt; (&lt;code&gt;design-consultation&lt;/code&gt;, &lt;code&gt;design-html&lt;/code&gt;, &lt;code&gt;design-shotgun&lt;/code&gt;, &lt;code&gt;devex-review&lt;/code&gt;) — restored only when designing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3 plan reviews&lt;/strong&gt; (&lt;code&gt;plan-design-review&lt;/code&gt;, &lt;code&gt;plan-devex-review&lt;/code&gt;, &lt;code&gt;plan-tune&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 interview prep&lt;/strong&gt; (&lt;code&gt;staff-engineer-interview&lt;/code&gt;) — used a few times last quarter, not weekly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bulk move:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;s &lt;span class="k"&gt;in &lt;/span&gt;mkt-content mkt-email mkt-growth mkt-pr mkt-review mkt-seo mkt-social cmo &lt;span class="se"&gt;\&lt;/span&gt;
         research research-add-fields research-add-items research-deep research-report edit-article &lt;span class="se"&gt;\&lt;/span&gt;
         staff-engineer-interview &lt;span class="se"&gt;\&lt;/span&gt;
         obsidian-vault make-pdf pair-agent setup-browser-cookies open-gstack-browser &lt;span class="se"&gt;\&lt;/span&gt;
         plan-design-review plan-devex-review plan-tune &lt;span class="se"&gt;\&lt;/span&gt;
         design-consultation design-html design-shotgun devex-review&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;mv&lt;/span&gt; ~/.claude/skills/&lt;span class="nv"&gt;$s&lt;/span&gt; ~/.claude/skills-vault/ 2&amp;gt;/dev/null
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;skill-vault&lt;/code&gt; index skill from the original post still bridges everything — Claude knows where each skill lives and restores it on demand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; 73 → 46 active skills. &lt;strong&gt;~1.4K tokens saved.&lt;/strong&gt;&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;th&gt;Tokens Saved (per session)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sink #1 — CLAUDE.md trim + per-component split&lt;/td&gt;
&lt;td&gt;~1.5K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sink #2 — claude-mem timeline disabled&lt;/td&gt;
&lt;td&gt;~2K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sink #3 — Round 2 skill vault&lt;/td&gt;
&lt;td&gt;~1.4K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~4.9K&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On top of the original 96% reduction from the Skill Vault, this is another solid bite. But honestly, the dollar value isn't the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters Beyond Cost
&lt;/h2&gt;

&lt;p&gt;Every token in your context is a token Claude has to &lt;strong&gt;attend over&lt;/strong&gt; before generating its response. The bigger your prompt, the more diluted attention becomes on the actual task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Big context ≠ better answers.&lt;/strong&gt; Frequently it's the opposite. Targeted context wins.&lt;/p&gt;

&lt;p&gt;The pattern across all three fixes is the same:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Audit what's auto-loaded vs. what's actually useful.&lt;/strong&gt; You'll be surprised.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move episodic content out of the always-on path&lt;/strong&gt; and into on-demand access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust the model to pull what it needs&lt;/strong&gt; — when it does need the vaulted skill, the subdir's CLAUDE.md, or the memory search, it will reach for it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Less ambient noise. Sharper signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips for Your Own Audit
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inspect, don't guess.&lt;/strong&gt; &lt;code&gt;wc -c&lt;/code&gt; your &lt;code&gt;CLAUDE.md&lt;/code&gt; files. &lt;code&gt;ls ~/.claude/skills/&lt;/code&gt;. Read your plugin &lt;code&gt;hooks.json&lt;/code&gt; files line by line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hierarchy is free.&lt;/strong&gt; Per-directory &lt;code&gt;CLAUDE.md&lt;/code&gt; files cost nothing when you're not in that directory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugin hooks are debt.&lt;/strong&gt; Every &lt;code&gt;SessionStart&lt;/code&gt; or &lt;code&gt;UserPromptSubmit&lt;/code&gt; hook is a tax. Audit them by hand. Some are essential (auth, telemetry); some inject "context" that's just clutter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-vault every few weeks.&lt;/strong&gt; Usage patterns shift. Skills that were daily three months ago may be quarterly now. Yesterday's must-have is tomorrow's vault candidate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for per-turn taxes.&lt;/strong&gt; A &lt;code&gt;UserPromptSubmit&lt;/code&gt; hook costs N tokens &lt;em&gt;every single turn&lt;/em&gt;. Even a small reminder block adds up fast in a long session.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;The Skill Vault pattern is still the heaviest hitter. These three follow-ups are the long tail:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CLAUDE.md&lt;/strong&gt; → split per component, slim the root.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugin hooks&lt;/strong&gt; → audit auto-injected context. Disable what you don't need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skill vault&lt;/strong&gt; → revisit it. Vault more.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together: another &lt;strong&gt;~5K tokens per session, zero capability lost.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When your context is tight, your model is sharp.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Prakash Ponali, a Staff Engineer with 16+ years in enterprise eCommerce. Currently building &lt;a href="https://khetisahayak.com" rel="noopener noreferrer"&gt;Khetisahayak&lt;/a&gt; — a farming helper app for Telugu-speaking farmers in Andhra Pradesh. Find me on &lt;a href="https://www.linkedin.com/in/prakash-ponali-75ab9b17" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How I Cut Claude Code Token Consumption by 96% with the Skill Vault Pattern</title>
      <dc:creator>pponali</dc:creator>
      <pubDate>Sun, 19 Apr 2026 07:45:04 +0000</pubDate>
      <link>https://dev.to/pponali/how-i-cut-claude-code-token-consumption-by-96-with-the-skill-vault-pattern-9d1</link>
      <guid>https://dev.to/pponali/how-i-cut-claude-code-token-consumption-by-96-with-the-skill-vault-pattern-9d1</guid>
      <description>&lt;h2&gt;
  
  
  The Problem: 181 Skills Burning 1.18 Million Tokens Per Session
&lt;/h2&gt;

&lt;p&gt;I'm a power user of &lt;a href="https://claude.ai/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; — Anthropic's CLI for AI-assisted development. Over weeks of installing skill packs from GitHub repos, I accumulated &lt;strong&gt;181 skills&lt;/strong&gt; from multiple sources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;gstack&lt;/strong&gt; agents (QA, design, deploy, monitoring)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;marketingskills&lt;/strong&gt; by Corey Haines (36 CRO/SEO/copywriting skills)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;wondelai/skills&lt;/strong&gt; (42 product/engineering frameworks)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;superpowers&lt;/strong&gt; by obra (14 dev methodology skills)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;claude-mem&lt;/strong&gt; (persistent memory plugin)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep-Research-skills&lt;/strong&gt; (structured research workflows)&lt;/li&gt;
&lt;li&gt;Custom skills I built myself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sounds great, right? More skills = more capability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong.&lt;/strong&gt; Every new conversation, Claude Code injects ALL skill descriptions into the system prompt. With 181 skills, that's approximately &lt;strong&gt;1.18 million tokens of overhead per session&lt;/strong&gt; — before I even type my first message.&lt;/p&gt;

&lt;p&gt;Tokens were burning like a wildfire. Every task was expensive. Context windows were filling up fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Discovery: Measuring the Actual Cost
&lt;/h2&gt;

&lt;p&gt;I ran a simple audit to see exactly what was happening:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Count total SKILL.md bytes across all skills&lt;/span&gt;
&lt;span class="nv"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in&lt;/span&gt; ~/.claude/skills/&lt;span class="k"&gt;*&lt;/span&gt;/SKILL.md&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;total &lt;span class="o"&gt;+&lt;/span&gt; size&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Total bytes: &lt;/span&gt;&lt;span class="nv"&gt;$total&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Approx tokens: &lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;total &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: &lt;strong&gt;4.7MB of SKILL.md files = ~1.18 million tokens.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The top offenders were massive:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ship (gstack)&lt;/td&gt;
&lt;td&gt;130KB&lt;/td&gt;
&lt;td&gt;~32K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;plan-ceo-review&lt;/td&gt;
&lt;td&gt;112KB&lt;/td&gt;
&lt;td&gt;~28K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;office-hours&lt;/td&gt;
&lt;td&gt;101KB&lt;/td&gt;
&lt;td&gt;~25K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seedance-15-real-estate&lt;/td&gt;
&lt;td&gt;95KB&lt;/td&gt;
&lt;td&gt;~24K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;plan-devex-review&lt;/td&gt;
&lt;td&gt;93KB&lt;/td&gt;
&lt;td&gt;~23K&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;15 Seedance video skills alone consumed &lt;strong&gt;~265K tokens&lt;/strong&gt;. I used them maybe once a month.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: The Skill Vault Pattern
&lt;/h2&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Move rarely-used skills to a vault directory&lt;/strong&gt; (out of auto-load)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep a lightweight index skill&lt;/strong&gt; that tells Claude what's available&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude restores skills on-demand&lt;/strong&gt; when your request matches one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optionally move it back&lt;/strong&gt; after the task&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 1: Create the Vault
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.claude/skills-vault
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Audit Your Skills by Usage Frequency
&lt;/h3&gt;

&lt;p&gt;Sort all skills by file size to find the biggest offenders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in&lt;/span&gt; ~/.claude/skills/&lt;span class="k"&gt;*&lt;/span&gt;/SKILL.md&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$size&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then categorize them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tier 1 (Daily use):&lt;/strong&gt; Keep active — your core dev workflow skills&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 2 (Project-specific):&lt;/strong&gt; Keep active — skills tied to current projects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 3 (Occasional):&lt;/strong&gt; Vault — heavy skills used weekly/monthly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 4 (Rare):&lt;/strong&gt; Vault — frameworks and reference skills&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Move Skills to the Vault
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Move video generation skills (rarely used)&lt;/span&gt;
&lt;span class="nb"&gt;mv&lt;/span&gt; ~/.claude/skills/seedance-&lt;span class="k"&gt;*&lt;/span&gt; ~/.claude/skills-vault/

&lt;span class="c"&gt;# Move heavy plan review skills&lt;/span&gt;
&lt;span class="nb"&gt;mv&lt;/span&gt; ~/.claude/skills/&lt;span class="o"&gt;{&lt;/span&gt;plan-ceo-review,plan-eng-review,plan-design-review&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
   ~/.claude/skills-vault/

&lt;span class="c"&gt;# Move framework/reference skills&lt;/span&gt;
&lt;span class="nb"&gt;mv&lt;/span&gt; ~/.claude/skills/&lt;span class="o"&gt;{&lt;/span&gt;clean-architecture,domain-driven-design,system-design&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
   ~/.claude/skills-vault/

&lt;span class="c"&gt;# Move marketing skills you don't use daily&lt;/span&gt;
&lt;span class="nb"&gt;mv&lt;/span&gt; ~/.claude/skills/&lt;span class="o"&gt;{&lt;/span&gt;copywriting,seo-audit,paid-ads,cold-email&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
   ~/.claude/skills-vault/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Create the Index Skill (The Key Ingredient)
&lt;/h3&gt;

&lt;p&gt;This is what makes the pattern work. Create a lightweight skill that acts as a lookup table for Claude:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.claude/skills/skill-vault
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;~/.claude/skills/skill-vault/SKILL.md&lt;/code&gt;:&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="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;skill-vault&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Index of vaulted skills at ~/.claude/skills-vault/.&lt;/span&gt;
  &lt;span class="s"&gt;When the user's request matches a vaulted skill, restore it with&lt;/span&gt;
  &lt;span class="s"&gt;mv ~/.claude/skills-vault/&amp;lt;name&amp;gt; ~/.claude/skills/ then use it.&lt;/span&gt;
  &lt;span class="s"&gt;Use when user asks about design, SEO, CRO, copywriting,&lt;/span&gt;
  &lt;span class="s"&gt;architecture patterns, security audit, deployment, video&lt;/span&gt;
  &lt;span class="s"&gt;generation, or any topic not covered by active skills.&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gu"&gt;## Skill Vault&lt;/span&gt;

Skills stored in &lt;span class="sb"&gt;`~/.claude/skills-vault/`&lt;/span&gt; to save tokens.
When a user request matches one, restore it:

mv ~/.claude/skills-vault/&lt;span class="nt"&gt;&amp;lt;skill-name&amp;gt;&lt;/span&gt; ~/.claude/skills/

Then invoke it normally. After the task, optionally move back:

mv ~/.claude/skills/&lt;span class="nt"&gt;&amp;lt;skill-name&amp;gt;&lt;/span&gt; ~/.claude/skills-vault/

&lt;span class="gu"&gt;## Vaulted Skills Index&lt;/span&gt;

&lt;span class="gu"&gt;### Design &amp;amp; UI&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`design-review`&lt;/span&gt; — Visual QA, spacing/hierarchy fixes
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`design-html`&lt;/span&gt; — Production HTML/CSS from mockups
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`design-shotgun`&lt;/span&gt; — Generate multiple design variants
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`top-design`&lt;/span&gt; — Awwwards-quality web experiences

&lt;span class="gu"&gt;### Engineering Frameworks&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`clean-architecture`&lt;/span&gt; — Dependency rule, ports/adapters
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`domain-driven-design`&lt;/span&gt; — Bounded contexts, aggregates
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`system-design`&lt;/span&gt; — Distributed systems, scaling
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`refactoring-patterns`&lt;/span&gt; — Extract method, code smells

&lt;span class="gu"&gt;### DevOps &amp;amp; Monitoring&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`land-and-deploy`&lt;/span&gt; — Merge, CI, verify production
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`canary`&lt;/span&gt; — Post-deploy monitoring
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`cso`&lt;/span&gt; — Security audit (OWASP, STRIDE)

&lt;span class="gu"&gt;### Marketing&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`copywriting`&lt;/span&gt; — Marketing copy for any page
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`seo-audit`&lt;/span&gt; — Technical SEO audit
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`paid-ads`&lt;/span&gt; — Google/Meta/LinkedIn campaigns

(... add all your vaulted skills here ...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This index costs only &lt;strong&gt;~1.5K tokens&lt;/strong&gt; but gives Claude awareness of all 139 vaulted skills.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Reduction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Active skills&lt;/td&gt;
&lt;td&gt;181&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;76% fewer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokens per session&lt;/td&gt;
&lt;td&gt;~1.18M&lt;/td&gt;
&lt;td&gt;~51K&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;96%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vaulted (on-demand)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;139&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capability lost&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;96% token reduction with zero capability loss.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works at Runtime
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You: "audit my design for visual issues"

Claude thinks:
  1. No active skill matches "design audit"
  2. skill-vault index matches: design-review
  3. Run: mv ~/.claude/skills-vault/design-review ~/.claude/skills/
  4. Now use design-review skill normally
  5. Task complete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude reads the vault index, finds the right skill, restores it with a single &lt;code&gt;mv&lt;/code&gt; command, and proceeds normally. The user experience is seamless.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Final Active Skill Set (43 Skills)
&lt;/h2&gt;

&lt;p&gt;Here's what I kept always-active:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Core Dev Workflow (Superpowers):&lt;/strong&gt;&lt;br&gt;
ship, qa, browse, review, investigate, writing-plans, executing-plans, brainstorming, subagent-driven-development, systematic-debugging, test-driven-development, verification-before-completion&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project-Specific:&lt;/strong&gt;&lt;br&gt;
ecommerce-architect, staff-engineer-interview, claude-api-patterns, claude-code-mastery, mcp-server-development&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Marketing (Khetisahayak):&lt;/strong&gt;&lt;br&gt;
cmo, mkt-content, mkt-seo, mkt-social, mkt-growth, mkt-email, mkt-pr, mkt-review&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Research:&lt;/strong&gt;&lt;br&gt;
research, research-deep, research-report&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Vault Index:&lt;/strong&gt;&lt;br&gt;
skill-vault (the 1.5K token index that knows about 139 other skills)&lt;/p&gt;
&lt;h2&gt;
  
  
  Tips for Your Own Vault
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Audit first.&lt;/strong&gt; Run the size audit script before moving anything. You might be surprised which skills are the heaviest.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep your daily drivers active.&lt;/strong&gt; Don't vault skills you use every session. The restore step adds a small delay.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Group by category.&lt;/strong&gt; Makes it easy to restore a whole category:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;mv&lt;/span&gt; ~/.claude/skills-vault/seedance-&lt;span class="k"&gt;*&lt;/span&gt; ~/.claude/skills/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Update the index&lt;/strong&gt; when you add new skills to the vault.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Restore everything&lt;/strong&gt; if you need full power for a complex session:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;mv&lt;/span&gt; ~/.claude/skills-vault/&lt;span class="k"&gt;*&lt;/span&gt; ~/.claude/skills/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Where to Find Great Skills
&lt;/h2&gt;

&lt;p&gt;Here are the skill repos I installed from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/coreyhaines31/marketingskills" rel="noopener noreferrer"&gt;marketingskills&lt;/a&gt; — 36 marketing skills by Corey Haines&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/wondelai/skills" rel="noopener noreferrer"&gt;wondelai/skills&lt;/a&gt; — 42 product/engineering framework skills
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/obra/superpowers" rel="noopener noreferrer"&gt;obra/superpowers&lt;/a&gt; — Dev methodology (TDD, planning, subagents)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Weizhena/Deep-Research-skills" rel="noopener noreferrer"&gt;Deep-Research-skills&lt;/a&gt; — Structured research workflows&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/thedotmack/claude-mem" rel="noopener noreferrer"&gt;claude-mem&lt;/a&gt; — Persistent memory across sessions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Installing is simple — symlink into &lt;code&gt;~/.claude/skills/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/coreyhaines31/marketingskills.git
&lt;span class="k"&gt;for &lt;/span&gt;skill_dir &lt;span class="k"&gt;in &lt;/span&gt;marketingskills/skills/&lt;span class="k"&gt;*&lt;/span&gt;/&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$skill_dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; ~/.claude/skills/&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$skill_dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then vault what you don't need daily.&lt;/p&gt;

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

&lt;p&gt;The Skill Vault pattern is dead simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;~/.claude/skills/&lt;/code&gt;&lt;/strong&gt; = active skills (loaded every turn)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;~/.claude/skills-vault/&lt;/code&gt;&lt;/strong&gt; = dormant skills (restored on demand)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;skill-vault/SKILL.md&lt;/code&gt;&lt;/strong&gt; = lightweight index that bridges the two&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're running more than 50 skills in Claude Code, you're probably burning hundreds of thousands of unnecessary tokens per session. The vault pattern gives you the full arsenal without the cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;96% fewer tokens. Zero lost capability. One &lt;code&gt;mv&lt;/code&gt; command away.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Prakash Ponali, a Staff Engineer with 16+ years in enterprise eCommerce. Currently building &lt;a href="https://khetisahayak.com" rel="noopener noreferrer"&gt;Khetisahayak&lt;/a&gt; — a farming helper app for Telugu-speaking farmers in Andhra Pradesh. Find me on &lt;a href="https://www.linkedin.com/in/prakash-ponali-75ab9b17" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Web Topic Analysis Report - 2025-07-19</title>
      <dc:creator>pponali</dc:creator>
      <pubDate>Sat, 19 Jul 2025 14:53:45 +0000</pubDate>
      <link>https://dev.to/pponali/web-topic-analysis-report-2025-07-19-29fh</link>
      <guid>https://dev.to/pponali/web-topic-analysis-report-2025-07-19-29fh</guid>
      <description>&lt;h1&gt;
  
  
  Web Topic Analysis Report - 2025-07-19
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;By AI Content Generator on July 19, 2025&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;web_analysis&lt;/code&gt;, &lt;code&gt;tech_trends&lt;/code&gt;, &lt;code&gt;ai_analysis&lt;/code&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Web Topic Analysis Report
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Workflow ID:&lt;/strong&gt; web_topic_analysis_20250719_145337&lt;br&gt;
&lt;strong&gt;Generated:&lt;/strong&gt; 2025-07-19T14:53:37.277540&lt;/p&gt;

&lt;p&gt;No analysis results available.&lt;/p&gt;

</description>
      <category>webanalysis</category>
      <category>techtrends</category>
      <category>aianalysis</category>
    </item>
  </channel>
</rss>
