<?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: Han Vigo</title>
    <description>The latest articles on DEV Community by Han Vigo (@han_vigo_7aecf96f08a23b63).</description>
    <link>https://dev.to/han_vigo_7aecf96f08a23b63</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%2F4171485%2F3911f926-f680-461f-9c82-7246c103e8c8.png</url>
      <title>DEV Community: Han Vigo</title>
      <link>https://dev.to/han_vigo_7aecf96f08a23b63</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/han_vigo_7aecf96f08a23b63"/>
    <language>en</language>
    <item>
      <title>I let httpx's departed reviewers come back for a day. They had notes.</title>
      <dc:creator>Han Vigo</dc:creator>
      <pubDate>Fri, 30 Oct 2026 16:29:17 +0000</pubDate>
      <link>https://dev.to/han_vigo_7aecf96f08a23b63/i-let-httpxs-departed-reviewers-come-back-for-a-day-they-had-notes-1a22</link>
      <guid>https://dev.to/han_vigo_7aecf96f08a23b63/i-let-httpxs-departed-reviewers-come-back-for-a-day-they-had-notes-1a22</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week4-2026-10-26"&gt;Hacktoberfest Open-Source AI Challenge: Week 4&lt;/a&gt; — Ghost in the Machine.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ancestors&lt;/strong&gt; lets a repository's past reviewers look at today's pull requests.&lt;/p&gt;

&lt;p&gt;Every long-lived open-source project has people who reviewed hundreds of PRs and then moved on. &lt;a href="https://github.com/encode/httpx" rel="noopener noreferrer"&gt;&lt;code&gt;encode/httpx&lt;/code&gt;&lt;/a&gt; is a good example. Of its 2,690 line comments, 675 were written by florimondmanca, whose last one is from February 2023; 255 by sethmlarson, all in 2019; 124 by yeraydiazdiaz, none after mid-2020. Three of the four most prolific reviewers in the project's history have not left a review comment in years. Their judgement did not leave with them. It is still there, in threads nobody reads any more.&lt;/p&gt;

&lt;p&gt;Ancestors ingests that history, distils what the reviewers kept asking for, and then reviews a &lt;strong&gt;new&lt;/strong&gt; diff in their collective voice. Three rules make it honest rather than spooky:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Only the dead may speak.&lt;/strong&gt; A comment counts as an ancestor only if it is more than twelve months old, and the PR under review is never allowed to testify about itself. (My first version got this wrong and happily "discovered" the reviewers' concerns by reading this week's review thread. Fixed, and the fix is the most important line in the repo.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No citation, no verdict.&lt;/strong&gt; Every note must cite a comment id that was actually retrieved for that hunk. If the model cites nothing verifiable, the verdict is downgraded to &lt;em&gt;"the ancestors are silent on this"&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two witnesses or it does not count.&lt;/strong&gt; A "house rule" in the charter survives only if two different comments back it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It runs entirely on my desktop: Gemma 3 (4B) writes, EmbeddingGemma retrieves, Mastra holds the agent, memory, vector store and the charter workflow, and Temporal turns a multi-hour pass over a repo's open PRs into something that survives me killing the worker.&lt;/p&gt;

&lt;p&gt;Día de los Muertos was the obvious frame. The dead are allowed to visit for one day, and they may only speak about what they actually saw.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fef1ebaymn9r0se8lsd3y.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fef1ebaymn9r0se8lsd3y.jpg" alt="The ancestors visited PR #3769: three notes, each citing a 2020 review comment on the same file" width="800" height="1325"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A report as rendered by the little viewer in &lt;code&gt;out/&lt;/code&gt;: every note ends in clickable citations; hunks the ancestors never spoke about are listed as silent.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcs0ihrq6fyrzizis3jsx.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%2Fcs0ihrq6fyrzizis3jsx.png" alt="Temporal timeline: reviewPr attempt 1 (red) dies with the worker, attempt 2 picks the same PR up on a new worker, then the second PR runs" width="797" height="93"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Temporal UI timeline of the run described below: the red segment is the review that died with worker #1; the retry is the same activity on worker #2.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/vigohan95/ancestors" rel="noopener noreferrer"&gt;https://github.com/vigohan95/ancestors&lt;/a&gt; (MIT). Everything below is from real runs on a Ryzen 7 5700G with no GPU.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the ancestors actually said
&lt;/h2&gt;

&lt;p&gt;I pointed it at three open httpx PRs this morning. Every voice quoted below is from 2020–2024; the cutoff means nothing from the last twelve months can speak, and the PR's own thread is excluded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PR #3769 — "Ensure mounted transports are closed even if main transport raises."&lt;/strong&gt; The PR wraps transport shutdown in &lt;code&gt;Client.close()&lt;/code&gt; / &lt;code&gt;aclose()&lt;/code&gt; in &lt;code&gt;try/finally&lt;/code&gt;. For three of the four hunks the ancestors came back with the same two voices from August 2020, when this exact method was last redesigned:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🕯️ &lt;code&gt;httpx/_client.py&lt;/code&gt; −1267,10 +1267,12 — &lt;em&gt;The ancestors are concerned about exception handling and ensuring all transports are closed, even in error scenarios. Comment [c477206186] suggests passing exception information and closing proxies, while [c472953172] recommends handling redundant close calls. The current code uses a &lt;code&gt;try...finally&lt;/code&gt; block to guarantee transport closure, but the absence of exception handling is noted.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[c477206186] &lt;strong&gt;florimondmanca&lt;/strong&gt;, 2020-08-26, PR #1218 on &lt;code&gt;httpx/_client.py&lt;/code&gt;: “…await self.&lt;em&gt;transport.&lt;/em&gt;&lt;em&gt;aexit&lt;/em&gt;_(exc_type, exc_value, traceback) for proxy in self._proxies.values()…”&lt;/li&gt;
&lt;li&gt;[c472953172] &lt;strong&gt;lovelydinosaur&lt;/strong&gt;, 2020-08-19, PR #1197 on &lt;code&gt;httpx/_client.py&lt;/code&gt;: “We should put &lt;code&gt;self._is_closed = True&lt;/code&gt; as the very first line, rather than the…”&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a real thread. In PR #1197 and #1218 the maintainers argued about exactly this close-ordering problem — pass the exception down, set &lt;code&gt;_is_closed&lt;/code&gt; first, close every proxy even when one raises. The 2026 PR re-opens the same ground without knowing, and the ancestors hand the reviewer the two comments to read before deciding. (4 hunks, 106 s, 3 notes, 1 silent.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PR #3767 — "Include header name in encoding error message."&lt;/strong&gt; For the hunk in &lt;code&gt;httpx/_models.py&lt;/code&gt; that re-encodes header values, the ancestors pulled obataku's 2020 question about why the code does not just use &lt;code&gt;normalize_header_value&lt;/code&gt;, and alexmojaki's 2024 aside about &lt;code&gt;self.text&lt;/code&gt; for non-UTF-8 charsets. Useful context for a reviewer; probably not a blocker. The other three hunks (tests, an unrelated signature) came back &lt;em&gt;silent&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PR #3777 — "Add real async iterator for ByteStreams."&lt;/strong&gt; Both hunks: &lt;em&gt;silent&lt;/em&gt;. The nearest voice (agronholm, 2025-07, on &lt;code&gt;__aiter__()&lt;/code&gt; returning any &lt;code&gt;AsyncIterator&lt;/code&gt;) is from this summer, so it is not old enough to be an ancestor, and nothing older applied. I like that it says so. A hosted model would have produced three confident paragraphs here.&lt;/p&gt;

&lt;p&gt;The current weakness is visible too: when the ancestors conclude "this is acceptable", Gemma 4B still labels it a &lt;em&gt;note&lt;/em&gt; instead of &lt;em&gt;honoured&lt;/em&gt;. The verdict schema has an &lt;code&gt;ok&lt;/code&gt; state; the small model under-uses it. That is a prompt problem I know how to fix and a model-size problem I can fix with one environment variable.&lt;/p&gt;
&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. The voices
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;gh api repos/encode/httpx/pulls/comments&lt;/code&gt; returns every line-anchored review comment since 2019: 2,690 of them across 168 files, median 119 characters, 1,182 from 2019 alone. Each substantive comment (≥25 characters; 2,442 of them) is embedded together with &lt;em&gt;where&lt;/em&gt; it was said — file path, reviewer, the last six lines of the hunk under review — using &lt;strong&gt;EmbeddingGemma&lt;/strong&gt; through Ollama, and stored in a LibSQL vector index via &lt;code&gt;@mastra/libsql&lt;/code&gt;. A blob-less clone supplies the 1,482-commit log, so the agent can also tell you who last touched a file and why.&lt;/p&gt;

&lt;p&gt;Ingest, end to end, on CPU: &lt;strong&gt;448 seconds&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. The charter — a Mastra workflow
&lt;/h3&gt;

&lt;p&gt;Before reviewing anything, a three-step &lt;code&gt;createWorkflow&lt;/code&gt; (&lt;code&gt;sample-voices → distil-charter → save-charter&lt;/code&gt;) samples comments stratified by year (so 2019's flood does not drown 2024), hands them to Gemma, and asks for the expectations the reviewers kept returning to, each with the comment ids that support it. Then the code applies the two-witness rule and throws away everything else.&lt;/p&gt;

&lt;p&gt;Here is the entire charter for httpx after the two-witness rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Distilled by Gemma from 44 comments spread across every year of review history; a rule only survives if at least two different comments back it (7 single-witness candidates dropped).&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prefer explicit parameters over &lt;code&gt;**kwargs&lt;/code&gt; in public signatures. ([c399643358] [c552568019])&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;One rule. Gemma proposed eight; seven were one reviewer talking about one line, dressed up as tradition. I would rather hand the review agent one true sentence than eight plausible ones, and the sampled comments still reach the agent through retrieval, where they are attributed to a person and a date instead of to "the project".&lt;/p&gt;
&lt;h3&gt;
  
  
  3. The review — a Mastra agent on a very short leash
&lt;/h3&gt;

&lt;p&gt;The agent never gets "review this PR". For each of the most substantive hunks it gets: the hunk, the last five commits to that file &lt;em&gt;before the cutoff&lt;/em&gt;, the charter, and the six closest ancestor comments (comments on the &lt;em&gt;same file&lt;/em&gt; get a boost — the ancestors literally stood here). It has to answer in a strict JSON shape: &lt;code&gt;note&lt;/code&gt;, &lt;code&gt;ok&lt;/code&gt;, or &lt;code&gt;silent&lt;/code&gt;, plus citations.&lt;/p&gt;

&lt;p&gt;The line that keeps it honest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cited&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;citations&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\[\]]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;silent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;cited&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;silent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;allowed&lt;/code&gt; is the set of comments that were actually retrieved for this hunk. A verdict that cites nothing from that set is silence, whatever the model wrote. This is what makes a 4B model usable: the retrieval does the remembering, the model does the reading, and it cannot pass off invention as tradition.&lt;/p&gt;

&lt;p&gt;Per hunk on this CPU: 25–40 seconds. A four-hunk PR: about two minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The haunting — Temporal
&lt;/h3&gt;

&lt;p&gt;Reviewing one PR takes minutes; reviewing a repository's open PRs takes an hour. That is exactly the kind of job that dies when a laptop sleeps. The &lt;code&gt;hauntRepo&lt;/code&gt; workflow lists open PRs and calls a &lt;code&gt;reviewPr&lt;/code&gt; activity per PR; the activity heartbeats after every hunk, the worker runs one activity at a time (&lt;code&gt;maxConcurrentActivityTaskExecutions: 1&lt;/code&gt;, because there is one local model), and the retry policy does the rest.&lt;/p&gt;

&lt;p&gt;Here is the run I kept (workflow &lt;code&gt;haunt-encode-httpx-1793376447857&lt;/code&gt;, two most recent open PRs):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;time (UTC)&lt;/th&gt;
&lt;th&gt;what happened&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;16:07:27&lt;/td&gt;
&lt;td&gt;workflow starts; &lt;code&gt;listOpenPrs&lt;/code&gt; returns #3796 and #3785&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16:07:28&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;reviewPr(3796)&lt;/code&gt; attempt 1 starts on worker #1; hunks 1–2 reviewed, heartbeat after each&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16:07:51&lt;/td&gt;
&lt;td&gt;I kill worker #1's node process (&lt;code&gt;taskkill /F /T&lt;/code&gt;) mid-hunk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16:09:40&lt;/td&gt;
&lt;td&gt;heartbeat timeout (2 min) fires; Temporal schedules attempt 2; worker #2, started at 16:08:08, picks it up and reviews all 6 hunks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16:11:20&lt;/td&gt;
&lt;td&gt;attempt 2 completes (98 s of model time); &lt;code&gt;reviewPr(3785)&lt;/code&gt; runs on worker #2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16:11:39&lt;/td&gt;
&lt;td&gt;workflow completes: 2 PRs reviewed, 3 notes, nothing lost, nothing reviewed twice by the same worker&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The activity itself is idempotent (it rewrites the report file), so the retry policy is the whole crash story: no checkpoint code, no queue, no cron. The first version of this demo "worked" because my kill command only killed the &lt;code&gt;npx&lt;/code&gt; wrapper and the orphaned node process quietly finished the job; the event history is what caught me — the retry attempt never appeared. Durable execution is also a lie detector.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why open innovation matters for this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Review comments on a private repo are the last thing you want to ship to a hosted model.&lt;/strong&gt; Everything here talks to &lt;code&gt;localhost:11434&lt;/code&gt;. Unplug the network after ingest and the review still works (&lt;code&gt;--diff file.patch&lt;/code&gt; skips GitHub entirely).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It has to be cheap enough to run on every PR, forever.&lt;/strong&gt; The whole httpx history embeds in seven minutes on a five-year-old desktop CPU; a review costs electricity, not tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swappable.&lt;/strong&gt; &lt;code&gt;ANCESTORS_MODEL=gemma3:12b&lt;/code&gt; and the ancestors get more articulate; no code change. The project's values live in the retrieval, the cutoff and the charter, not in a model vendor's weights.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where open beat closed for me:&lt;/strong&gt; I could make the model &lt;em&gt;shut up&lt;/em&gt;. With a hosted API I would be prompting against a system tuned to always say something helpful. Here I own the loop: no citation, no verdict; too young, not an ancestor.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Review comments ingested&lt;/td&gt;
&lt;td&gt;2,690 (2019–2026), 168 files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embedded (≥25 chars)&lt;/td&gt;
&lt;td&gt;2,442 with EmbeddingGemma, 768 dims&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commits indexed&lt;/td&gt;
&lt;td&gt;1,482&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ingest wall time (CPU)&lt;/td&gt;
&lt;td&gt;448 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Charter&lt;/td&gt;
&lt;td&gt;1 Gemma call, ~120 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review PR #3769&lt;/td&gt;
&lt;td&gt;4 hunks, 106 s, 3 notes / 1 silent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review PR #3767&lt;/td&gt;
&lt;td&gt;4 hunks, 106 s, 1 note / 3 silent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review PR #3777&lt;/td&gt;
&lt;td&gt;2 hunks, 55 s, 2 silent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardware&lt;/td&gt;
&lt;td&gt;Ryzen 7 5700G, 31 GB RAM, no discrete GPU&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt; — Gemma 3 4B generates every verdict and the charter; EmbeddingGemma does all retrieval. Both run locally through Ollama; nothing leaves the machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Mastra&lt;/strong&gt; — &lt;code&gt;Agent&lt;/code&gt; + &lt;code&gt;Memory&lt;/code&gt; on &lt;code&gt;LibSQLStore&lt;/code&gt;, &lt;code&gt;LibSQLVector&lt;/code&gt; for the ancestor index, and a three-step &lt;code&gt;createWorkflow&lt;/code&gt; for the charter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Temporal&lt;/strong&gt; — the &lt;code&gt;hauntRepo&lt;/code&gt; workflow with heartbeating &lt;code&gt;reviewPr&lt;/code&gt; activities; crash-and-resume demonstrated above.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Team
&lt;/h2&gt;

&lt;p&gt;Solo: @vigohan95.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I used Claude Code as a pair programmer while building this. The design, the mistakes (see rule 1), every run and every number above are mine and real.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The 12 Ingress-NGINX annotations with no Gateway API equivalent (and what to do about each)</title>
      <dc:creator>Han Vigo</dc:creator>
      <pubDate>Thu, 08 Oct 2026 13:57:41 +0000</pubDate>
      <link>https://dev.to/han_vigo_7aecf96f08a23b63/the-12-ingress-nginx-annotations-with-no-gateway-api-equivalent-and-what-to-do-about-each-57a3</link>
      <guid>https://dev.to/han_vigo_7aecf96f08a23b63/the-12-ingress-nginx-annotations-with-no-gateway-api-equivalent-and-what-to-do-about-each-57a3</guid>
      <description>&lt;p&gt;Ingress-NGINX was retired in March 2026: no more releases, no more security patches, repository archived. By the Kubernetes project's own account, about half of cloud-native environments were still running it. Most migration guides stop at "run &lt;code&gt;ingress2gateway&lt;/code&gt;". That tool does the routing part well. The migration is everything it &lt;em&gt;cannot&lt;/em&gt; convert.&lt;/p&gt;

&lt;p&gt;I went through every Ingress-NGINX annotation and checked, against the October 2026 documentation of each Gateway API implementation, what happens to it. Roughly 80% convert or map to a policy. The remaining 20% have &lt;strong&gt;no Gateway API equivalent&lt;/strong&gt;, and each of them needs a decision. This article is that 20%.&lt;/p&gt;

&lt;p&gt;Everything below was verified against the official docs of Gateway API v1.6, NGINX Gateway Fabric (NGF) 2.7 and Envoy Gateway 1.9 on 7 October 2026. Where a vendor's documentation does not describe a capability, I say "not documented" rather than guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;configuration-snippet&lt;/code&gt; / &lt;code&gt;server-snippet&lt;/code&gt; / &lt;code&gt;location-snippet&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Gone by design. Gateway API deliberately has no "paste raw config here" escape hatch, and the implementations do not add one. Before migrating, inventory what your snippets actually do; in most fleets it is one of five things: header manipulation, redirects, rate limits, auth, or custom error pages. Each of those has a policy below. Anything else moves into the application or a sidecar.&lt;/p&gt;

&lt;p&gt;Inventory in one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get ingress &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; json | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.items[] | select(.metadata.annotations | keys[]? | test("snippet")) | "\(.metadata.namespace)/\(.metadata.name)"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. &lt;code&gt;limit-rps&lt;/code&gt; / &lt;code&gt;limit-rpm&lt;/code&gt; / &lt;code&gt;limit-connections&lt;/code&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Envoy Gateway:&lt;/strong&gt; &lt;code&gt;BackendTrafficPolicy.spec.rateLimit&lt;/code&gt; with &lt;code&gt;type: Local&lt;/code&gt; (per-proxy) or &lt;code&gt;type: Global&lt;/code&gt; (shared counter via the rate-limit service). Global needs the Redis-backed rate limit deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NGF:&lt;/strong&gt; no documented rate limiting in the open-source edition as of 2.7.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;auth-url&lt;/code&gt; / &lt;code&gt;auth-signin&lt;/code&gt; (external auth)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Envoy Gateway:&lt;/strong&gt; &lt;code&gt;SecurityPolicy.spec.extAuth&lt;/code&gt; with an HTTP or gRPC auth service; &lt;code&gt;headersToBackend&lt;/code&gt; forwards identity headers. Attach to the HTTPRoute or the Gateway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NGF:&lt;/strong&gt; not documented. Put an auth proxy (oauth2-proxy) in front of the backend Service and route to it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;affinity: cookie&lt;/code&gt; (sticky sessions)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NGF:&lt;/strong&gt; only via the experimental &lt;code&gt;sessionPersistence&lt;/code&gt; field on the route, and it requires NGINX Plus.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Envoy Gateway:&lt;/strong&gt; &lt;code&gt;BackendTrafficPolicy.spec.loadBalancer.consistentHash&lt;/code&gt; with &lt;code&gt;type: Cookie&lt;/code&gt;; set the cookie name and TTL there.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. &lt;code&gt;custom-http-errors&lt;/code&gt; / &lt;code&gt;default-backend&lt;/code&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Envoy Gateway:&lt;/strong&gt; &lt;code&gt;BackendTrafficPolicy.spec.responseOverride&lt;/code&gt; matches status codes and returns an inline body or a ConfigMap-backed one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NGF:&lt;/strong&gt; not documented.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. &lt;code&gt;proxy-body-size&lt;/code&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Envoy Gateway:&lt;/strong&gt; &lt;code&gt;BackendTrafficPolicy.spec.requestBuffer.limit&lt;/code&gt; (exceeding it returns 413).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NGF:&lt;/strong&gt; &lt;code&gt;ClientSettingsPolicy.spec.body.maxSize&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. &lt;code&gt;proxy-read-timeout&lt;/code&gt; / &lt;code&gt;proxy-send-timeout&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;HTTPRoute has a standard &lt;code&gt;timeouts&lt;/code&gt; field (&lt;code&gt;request&lt;/code&gt; and &lt;code&gt;backendRequest&lt;/code&gt;). The catch is who implements it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Envoy Gateway:&lt;/strong&gt; yes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NGF 2.7:&lt;/strong&gt; does &lt;strong&gt;not&lt;/strong&gt; implement HTTPRoute &lt;code&gt;timeouts&lt;/code&gt; or &lt;code&gt;retry&lt;/code&gt;. Use &lt;code&gt;ClientSettingsPolicy&lt;/code&gt; / &lt;code&gt;UpstreamSettingsPolicy&lt;/code&gt; where they cover your case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Load Balancer Controller and GKE:&lt;/strong&gt; no HTTPRoute &lt;code&gt;timeouts&lt;/code&gt; either; timeouts are configured on the load balancer resources.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. &lt;code&gt;rewrite-target&lt;/code&gt; with capture groups
&lt;/h2&gt;

&lt;p&gt;Simple prefix rewrites convert (&lt;code&gt;URLRewrite&lt;/code&gt; filter with &lt;code&gt;ReplacePrefixMatch&lt;/code&gt;). Regex rewrites with &lt;code&gt;$1&lt;/code&gt;/&lt;code&gt;$2&lt;/code&gt; captures do not; Gateway API has no regex rewrite. Options: restructure the paths, or use the implementation's extension (Envoy Gateway &lt;code&gt;HTTPRouteFilter&lt;/code&gt; with &lt;code&gt;regex&lt;/code&gt; rewrite).&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Regex paths shared across hosts
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;use-regex: "true"&lt;/code&gt; combined with several hosts on one Ingress is the single most common cause of broken cutovers. Gateway API has &lt;code&gt;RegularExpression&lt;/code&gt; path matching, but the implementations differ in the regex dialect and in how they order overlapping matches. Find these before you start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get ingress &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; json | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.items[] | select(.metadata.annotations["nginx.ingress.kubernetes.io/use-regex"]=="true") | select((.spec.rules|length)&amp;gt;1) | "\(.metadata.namespace)/\(.metadata.name)"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  10. &lt;code&gt;ssl-redirect&lt;/code&gt; / &lt;code&gt;force-ssl-redirect&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Converts: an HTTP listener with a &lt;code&gt;RequestRedirect&lt;/code&gt; filter to &lt;code&gt;https&lt;/code&gt;, status 301. Do not forget the HTTP listener itself, which many people drop when they only migrate the HTTPS one.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. &lt;code&gt;whitelist-source-range&lt;/code&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Envoy Gateway:&lt;/strong&gt; &lt;code&gt;SecurityPolicy.spec.authorization&lt;/code&gt; with client CIDR rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NGF:&lt;/strong&gt; not documented at the route level.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  12. L4 (&lt;code&gt;tcp-services&lt;/code&gt; / &lt;code&gt;udp-services&lt;/code&gt; ConfigMaps)
&lt;/h2&gt;

&lt;p&gt;Not annotations, but they ride along with Ingress-NGINX and are forgotten. Gateway API has &lt;code&gt;TCPRoute&lt;/code&gt; and &lt;code&gt;UDPRoute&lt;/code&gt; (experimental channel). Check that your implementation ships the experimental CRDs before you depend on them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things that bit me
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AWS WAF&lt;/strong&gt; attaches through &lt;code&gt;LoadBalancerConfiguration.wafV2&lt;/code&gt; on the Gateway, not through an annotation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-namespace Gateways&lt;/strong&gt; need a &lt;code&gt;ReferenceGrant&lt;/code&gt;, and the failure mode is silent: the route simply never attaches. &lt;code&gt;kubectl get httproute -o yaml&lt;/code&gt; shows &lt;code&gt;Accepted=False&lt;/code&gt; in the conditions, but nothing else complains.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I did with all this
&lt;/h2&gt;

&lt;p&gt;I turned the full table (9 implementations, including GKE, AWS LBC and Azure AGC), the inventory scripts, twelve before/after manifest pairs validated against the real CRD schemas, and the zero-downtime cutover and rollback runbooks into a book: &lt;a href="https://leanpub.com/gateway-api-migration-playbook" rel="noopener noreferrer"&gt;Off Ingress-NGINX: The Gateway API Migration Playbook&lt;/a&gt;. The list above is the part most teams need first; questions in the comments are welcome.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: the book was drafted with AI assistance under my direction and then technically edited claim by claim against the official documentation; the editing log ships with it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>networking</category>
      <category>cloudnative</category>
    </item>
  </channel>
</rss>
