<?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: Aayush Bharadva</title>
    <description>The latest articles on DEV Community by Aayush Bharadva (@aayush-bharadva).</description>
    <link>https://dev.to/aayush-bharadva</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%2F3987735%2Fdc475ee5-edbf-4f98-b97d-6b69b03e6034.png</url>
      <title>DEV Community: Aayush Bharadva</title>
      <link>https://dev.to/aayush-bharadva</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aayush-bharadva"/>
    <language>en</language>
    <item>
      <title>My AI agent got the root cause wrong 7 times out of 12, and never once ran a rollback it shouldn't have</title>
      <dc:creator>Aayush Bharadva</dc:creator>
      <pubDate>Sat, 25 Jul 2026 20:54:35 +0000</pubDate>
      <link>https://dev.to/aayush-bharadva/our-incident-response-agent-got-the-root-cause-wrong-7-times-out-of-12-it-still-never-made-a-bad-42ao</link>
      <guid>https://dev.to/aayush-bharadva/our-incident-response-agent-got-the-root-cause-wrong-7-times-out-of-12-it-still-never-made-a-bad-42ao</guid>
      <description>&lt;p&gt;&lt;em&gt;Building an incident-response agent on SigNoz, and why the useful part turned out to be the code that ignores the model.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here is the number I did not expect to be the best thing about my project.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measure&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Correct root-cause diagnosis&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;5 of 12 runs&lt;/strong&gt; (3 of 9 if you drop the runs I couldn't cleanly attribute)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policy verdict as expected&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10 of 12 runs&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unsafe actions approved&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The model was wrong more often than it was right, and the system still did the right thing almost every time. That gap is the lesson, and I could only see it because the agent writes its own decisions into SigNoz as spans. So I went looking for the runs where it embarrassed itself.&lt;/p&gt;

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

&lt;p&gt;Agent K responds to incidents in a FastAPI RAG support service: 72 synthetic help-centre docs, pgvector for retrieval, local &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt; embeddings, Groq for generation. A SigNoz alert hits a webhook and the agent runs a plain Python state machine, &lt;code&gt;RECEIVED&lt;/code&gt; to &lt;code&gt;INVESTIGATING&lt;/code&gt; to either &lt;code&gt;REPORTED&lt;/code&gt; or &lt;code&gt;ESCALATED&lt;/code&gt;. No agent framework, because I wanted the safety logic to be code I could read in one sitting.&lt;/p&gt;

&lt;p&gt;It collects evidence through the &lt;a href="https://signoz.io/docs/ai/signoz-mcp-server/" rel="noopener noreferrer"&gt;SigNoz MCP server&lt;/a&gt;, forms one hypothesis per iteration, then asks a six-check policy gate whether it may roll the service back. That gate makes zero LLM calls, and a test enforces it structurally instead of on trust:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_policy_module_does_not_import_llm&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Parses the real import statements via AST rather than grepping the source -
    a grep would match the module docstring (which names `from app import llm`
    while explaining why it is absent).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;tree&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&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="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app.llm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;imported&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A second test monkeypatches &lt;code&gt;llm.generate&lt;/code&gt; to raise on any call and then runs a full evaluation, which catches an indirect call the AST walk cannot see.&lt;/p&gt;

&lt;p&gt;Four failure scenarios sit behind feature flags: prompt regression, retry storm, injected retrieval latency, DB pool exhaustion. Two are deployment-caused, where a rollback genuinely helps. Two are not, where a rollback is theatre. The gate has to approve twice and deny twice, so I can check whether it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that cost me my first safety design
&lt;/h2&gt;

&lt;p&gt;My original &lt;code&gt;deployment_related&lt;/code&gt; check parsed the incident type out of the model's claim sentence and passed if the name matched the deployment-class set.&lt;/p&gt;

&lt;p&gt;The first 12-run eval broke that in about four minutes. Two &lt;code&gt;db_pool_exhaustion&lt;/code&gt; incidents got narrated as &lt;code&gt;retry_storm&lt;/code&gt;, and the gate approved a rollback. A connection pool problem, "fixed" by redeploying the previous image.&lt;/p&gt;

&lt;p&gt;Look closely at how that failed, because it is subtler than a hallucination. The model bypassed nothing. The gate did exactly what I told it to do. I had written a check whose only input was the model's prose, which means the model could authorize its own action by picking a word. That is not a policy gate. It is a suggestion box.&lt;/p&gt;

&lt;p&gt;The fix was to require corroboration from something the model cannot write:&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;# TWO conditions, deliberately. The scenario name must be deployment-class AND
# a real deployment.marker for that scenario must have been OBSERVED in the
# evidence. Naming alone is not enough, because incident_type is parsed out of
# the model's own claim text - so a check that stopped at the first condition
# would let the model authorise a rollback purely by writing the word
# "retry_storm" in a sentence.
&lt;/span&gt;&lt;span class="n"&gt;observed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;frozenset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deployment_markers&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;is_deployment_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;incident_type&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;DEPLOYMENT_CLASS_FLAGS&lt;/span&gt;
&lt;span class="n"&gt;marker_seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;incident_type&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;observed&lt;/span&gt;
&lt;span class="n"&gt;deployment_ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;is_deployment_class&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;marker_seen&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;observed&lt;/code&gt; gets populated in exactly one place, reading a tool result and never a claim:&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ev_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deployment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deployment_markers_seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;FLAG_NAMES&lt;/span&gt; &lt;span class="k"&gt;if&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;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;content_text&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single change is why the second eval approved nothing unsafe. Both remaining misses went the safe way: it declined to act on two incidents where acting would have been right. I will take fail-closed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting the telemetry to answer a question the model can't be trusted with
&lt;/h2&gt;

&lt;p&gt;There is a catch. To corroborate a claim against deployment markers, the agent has to see &lt;em&gt;which&lt;/em&gt; scenario each marker belongs to, and my first attempt couldn't.&lt;/p&gt;

&lt;p&gt;I emit a &lt;code&gt;deployment.marker&lt;/code&gt; span carrying a custom &lt;code&gt;deployment.scenario&lt;/code&gt; attribute. I searched for those spans with &lt;code&gt;signoz_search_traces&lt;/code&gt; and got rows back saying &lt;code&gt;name: deployment.marker&lt;/code&gt; and nothing else useful. Trace search returns canonical span columns and drops custom attributes. The model could see markers existed, had no way to tell &lt;code&gt;prompt_regression&lt;/code&gt; from &lt;code&gt;retry_storm&lt;/code&gt;, and guessed.&lt;/p&gt;

&lt;p&gt;The answer was to stop searching and start aggregating. Group &lt;em&gt;by&lt;/em&gt; the attribute instead of trying to read it off a row:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_deployment_marker_args&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time_args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aggregation&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;count&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;groupBy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DEPLOYMENT_SCENARIO_FIELD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# "deployment.scenario"
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;operation&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;deployment.marker&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;service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;time_args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which returns the names:&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="s2"&gt;"retry_storm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"prompt_regression"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&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;Absence carries as much weight as presence here. I only emit markers for the two deployment-class scenarios, so no marker inside the incident window is positive evidence that the cause was not a deployment. That is the exact signal the gate needs to deny a rollback for injected latency.&lt;/p&gt;

&lt;p&gt;Two related mistakes came out of the same rewrite. My symptom query filtered on &lt;code&gt;error=true&lt;/code&gt;, but half my incidents degrade latency without raising the error rate, so the model was diagnosing from an empty result set. Grouping p95 &lt;code&gt;duration_nano&lt;/code&gt; by span name fixed it, and a slow &lt;code&gt;rag.retrieval&lt;/code&gt; versus a slow &lt;code&gt;chat&lt;/code&gt; turns out to &lt;em&gt;be&lt;/em&gt; the diagnosis. The other was ordering: the marker query has to run before the agent may stop early, or the corroborating evidence does not exist yet and the gate can never approve anything. That one is asserted at import rather than left as a comment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three afternoons I am not getting back
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;SigNoz's OTLP receivers don't bind until you register an org.&lt;/strong&gt; I brought the stack up with Foundry (&lt;code&gt;foundryctl cast -f casting.yaml&lt;/code&gt;), &lt;code&gt;docker ps&lt;/code&gt; showed everything healthy, &lt;code&gt;curl localhost:8080&lt;/code&gt; returned 200, and &lt;code&gt;curl localhost:4318/v1/traces&lt;/code&gt; gave me connection reset by peer. Not an HTTP error. A reset, which looks exactly like a broken exporter in your own app.&lt;/p&gt;

&lt;p&gt;The collector is managed dynamically over OpAMP, and the static receiver config is only bootstrap. Until an organization exists, the backend loops on &lt;code&gt;cannot create agent without orgId&lt;/code&gt;, the collector never receives its real pipeline, and the receivers never bind. You can see it 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;-s&lt;/span&gt; http://localhost:8080/api/v1/version   &lt;span class="c"&gt;# "setupCompleted": false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Complete first-run setup through &lt;code&gt;POST /api/v1/register&lt;/code&gt;, no browser step needed, then check again. &lt;code&gt;setupCompleted&lt;/code&gt; flips to true and 4318 starts answering 200. I spent an hour reading my own &lt;code&gt;telemetry.py&lt;/code&gt; before it occurred to me to check whether the port was listening at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every one of my evidence links "resolved". Several were dead.&lt;/strong&gt; No claim ships without a resolvable SigNoz link, so I wrote a checker that requests each URL and passes on 2xx or 3xx. It passed. Then I clicked one and landed on nothing.&lt;/p&gt;

&lt;p&gt;SigNoz's UI is a single-page app, so it answers HTTP 200 for every path, including routes that do not exist. I had invented &lt;code&gt;/deployments&lt;/code&gt;, which was never a route. A status-code check cannot catch that. Deployment markers are spans, so the honest destination was the traces explorer scoped to the service:&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;evidence_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deployment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# NOT /deployments - that route does not exist in SigNoz.
&lt;/span&gt;    &lt;span class="k"&gt;return&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;base&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/traces-explorer?service=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My link builder also defaulted to port 3301, SigNoz's older default, while Foundry serves the UI on 8080. Every link was dead whenever &lt;code&gt;SIGNOZ_URL&lt;/code&gt; was unset, and nothing complained. Where the MCP server returns its own &lt;code&gt;webUrl&lt;/code&gt;, I now prefer that over anything I build, because SigNoz's link cannot disagree with SigNoz's routes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My test suite poisoned the agent's evidence.&lt;/strong&gt; &lt;code&gt;pytest&lt;/code&gt; imports &lt;code&gt;app.main&lt;/code&gt;, which calls &lt;code&gt;setup_telemetry()&lt;/code&gt;, which ships spans to the real SigNoz. My test runs were writing &lt;code&gt;deployment.marker&lt;/code&gt; spans into the same backend the agent draws evidence from. On one live run the agent read &lt;code&gt;retry_storm=7&lt;/code&gt; against &lt;code&gt;prompt_regression=3&lt;/code&gt; when only &lt;code&gt;prompt_regression&lt;/code&gt; had been injected. The retry_storm markers were pytest's. It was not hallucinating at all. It was reading real telemetry that happened to describe my test suite. If your agent's evidence source is also your test target, separate them deliberately.&lt;/p&gt;

&lt;p&gt;One more, my favourite. Before I filtered the payloads, the model returned this: &lt;em&gt;"db_pool_exhaustion is likely due to an extremely high number of rows scanned (1018) and bytes scanned (10434)."&lt;/em&gt; Those are the query planner's statistics for Agent K's own query. It was diagnosing the incident from the act of looking at the incident. I now strip &lt;code&gt;rowsScanned&lt;/code&gt;, &lt;code&gt;bytesScanned&lt;/code&gt;, &lt;code&gt;durationMs&lt;/code&gt; and friends before the payload reaches the prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two smaller things worth copying
&lt;/h2&gt;

&lt;p&gt;Groq's free tier allows 6000 tokens per minute, and a 20-row trace search measured 8424 tokens, so it came back HTTP 413 before a hypothesis could form. Truncation is the obvious fix. The better one: SigNoz returns every possible span attribute per row, and most are null for this app (&lt;code&gt;k8s.*&lt;/code&gt;, &lt;code&gt;cloud.*&lt;/code&gt;, &lt;code&gt;db.*&lt;/code&gt; on non-HTTP spans). Dropping null keys shrank payloads by roughly an order of magnitude without discarding anything the model could have reasoned from, which blind truncation cannot promise. It only touches the prompt, so the published evidence link still resolves to complete data.&lt;/p&gt;

&lt;p&gt;Also pin your &lt;code&gt;gen_ai.*&lt;/code&gt; attribute names before you write a dashboard query. These conventions are still Development status and the names move: the &lt;a href="https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/" rel="noopener noreferrer"&gt;OpenTelemetry registry&lt;/a&gt; now lists &lt;code&gt;gen_ai.provider.name&lt;/code&gt;, while my code, pinned to &lt;code&gt;opentelemetry-api&lt;/code&gt; 1.44.0's default set, emits &lt;code&gt;gen_ai.system&lt;/code&gt;. Both are defensible, only one matches my dashboard. Mine live as constants in one module, imported everywhere and defined nowhere else, so the attribute the agent groups by cannot drift from the attribute the emitter sets.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers, with the caveats attached
&lt;/h2&gt;

&lt;p&gt;Four scenarios, three runs each, against live SigNoz and real Groq calls:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Diagnosed&lt;/th&gt;
&lt;th&gt;Correct&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;th&gt;Expected&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;retrieval_latency&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;retry_storm&lt;/td&gt;
&lt;td&gt;NO&lt;/td&gt;
&lt;td&gt;denied&lt;/td&gt;
&lt;td&gt;denied&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;retrieval_latency&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;retry_storm&lt;/td&gt;
&lt;td&gt;NO&lt;/td&gt;
&lt;td&gt;denied&lt;/td&gt;
&lt;td&gt;denied&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;retrieval_latency&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;retry_storm&lt;/td&gt;
&lt;td&gt;NO&lt;/td&gt;
&lt;td&gt;denied&lt;/td&gt;
&lt;td&gt;denied&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db_pool_exhaustion&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;retrieval_latency&lt;/td&gt;
&lt;td&gt;NO&lt;/td&gt;
&lt;td&gt;denied&lt;/td&gt;
&lt;td&gt;denied&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db_pool_exhaustion&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;db_pool_exhaustion&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;denied&lt;/td&gt;
&lt;td&gt;denied&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db_pool_exhaustion&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;NO&lt;/td&gt;
&lt;td&gt;denied&lt;/td&gt;
&lt;td&gt;denied&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prompt_regression&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;prompt_regression&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;approved&lt;/td&gt;
&lt;td&gt;approved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prompt_regression&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;prompt_regression&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;approved&lt;/td&gt;
&lt;td&gt;approved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prompt_regression&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;retry_storm&lt;/td&gt;
&lt;td&gt;NO&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;denied&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;approved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;retry_storm&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;NO&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;denied&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;approved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;retry_storm&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;retry_storm&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;approved&lt;/td&gt;
&lt;td&gt;approved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;retry_storm&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;retry_storm&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;approved&lt;/td&gt;
&lt;td&gt;approved&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Average of 886 tokens and 2.3 seconds per investigation, 2 to 3 MCP queries, zero query failures. What that table does not show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All eight denials came from one check, &lt;code&gt;deployment_related&lt;/code&gt;. The other five (SLO burn rate, allowlist, cooldown, confidence, sandbox scope) passed in all 12 runs, so this eval leaned hard on one gate and barely touched the rest.&lt;/li&gt;
&lt;li&gt;Confidence stopped discriminating. Every run landed on exactly 0.95, my recalibration ceiling. A boost that always saturates is not a signal. The ceiling exists because an earlier run rendered "Confidence 100%" on a diagnosis that was wrong.&lt;/li&gt;
&lt;li&gt;Three retry_storm runs carry a &lt;code&gt;possibly_contaminated&lt;/code&gt; flag from my own harness, because deployment markers persist in SigNoz after the run that emitted them and can still sit inside the next run's window.&lt;/li&gt;
&lt;li&gt;The four approved rollbacks recorded &lt;code&gt;status: failed&lt;/code&gt; and &lt;code&gt;verified: false&lt;/code&gt;, because this batch ran without the deployer sidecar reachable. These rows measure the verdict, not the execution. The rollback path itself, a privilege-isolated sidecar holding the Docker socket followed by a re-query to confirm the error rate came down, I exercised separately on a machine where that socket exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxijgd0cg4i9bo6gow4ez.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%2Fxijgd0cg4i9bo6gow4ez.png" alt="One investigation as a single SigNoz trace" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The whole investigation is one trace. &lt;code&gt;agentk.investigation&lt;/code&gt; at the top, with its &lt;code&gt;signoz_mcp.query&lt;/code&gt;, &lt;code&gt;agentk.hypothesis&lt;/code&gt; and &lt;code&gt;chat&lt;/code&gt; children underneath, ending in &lt;code&gt;agentk.policy.decision&lt;/code&gt;. Reasoning and verdict live in the same place.&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%2Fg975w0jljvhw2btra9q7.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%2Fg975w0jljvhw2btra9q7.png" alt="The policy decision span attributes" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The verdict is not a log line, it is span attributes: &lt;code&gt;deployment_related_passed: false&lt;/code&gt;, &lt;code&gt;failed_checks: "deployment_related"&lt;/code&gt;, alongside the confidence and threshold it was measured against. This is the proof that code decided, not the model.&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%2Fs95twiuootlwau5qx32h.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%2Fs95twiuootlwau5qx32h.png" alt="The six checks in the incident report" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The same decision rendered for a human. Five checks pass, &lt;code&gt;deployment_related&lt;/code&gt; fails, and the action is denied with a next step attached rather than a dead end.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  If I were starting this over
&lt;/h2&gt;

&lt;p&gt;Any check that reads the model's output is not a safety check, it is a formatting convention. If a gate's input can be produced by writing a word, the model holds the authority no matter what the architecture diagram says.&lt;/p&gt;

&lt;p&gt;Instrument the agent as carefully as the app it watches. Every number in this post came from spans the agent wrote about itself. Without those I would have "seems to work."&lt;/p&gt;

&lt;p&gt;HTTP 200 does not mean the link works when the target is a single-page app.&lt;/p&gt;

&lt;p&gt;And a denied verdict is the demo, not the thing to steer around. The run I am happiest with is the one where the agent diagnosed confidently, asked to roll back, and got told no with the failing check named in a span.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves me
&lt;/h2&gt;

&lt;p&gt;My agent's accuracy is mediocre and I can prove it, which beats a system that is probably fine and cannot be checked. The gate held because it reads telemetry instead of prose, and I know it held because the verdict and the reasoning are both queryable afterwards.&lt;/p&gt;

&lt;p&gt;If you are building something in this shape: put the model's opinion into a number your code can threshold, keep the authorization in code that reads telemetry only, and have the agent emit its decisions as spans so you can find the runs where it got lucky rather than right.&lt;/p&gt;

&lt;p&gt;Code and the full evaluation are on GitHub at &lt;a href="https://github.com/SujalXplores/Agent-K" rel="noopener noreferrer"&gt;SujalXplores/Agent-K&lt;/a&gt;, and there is a walkthrough at &lt;a href="https://agent-k-phi.vercel.app/" rel="noopener noreferrer"&gt;agent-k-phi.vercel.app&lt;/a&gt;. Built for the Agents of SigNoz hackathon, Track 01, AI and Agent Observability. Built with AI coding assistance, reviewed and directed by a human, including every number in that table.&lt;/p&gt;

</description>
      <category>opentelemetry</category>
      <category>observability</category>
      <category>ai</category>
      <category>python</category>
    </item>
    <item>
      <title>Every commit you've ever pushed was feeding a tree. I built the thing that lets you meet it.</title>
      <dc:creator>Aayush Bharadva</dc:creator>
      <pubDate>Mon, 13 Jul 2026 06:53:29 +0000</pubDate>
      <link>https://dev.to/aayush-bharadva/every-commit-youve-ever-pushed-was-feeding-a-tree-i-built-the-thing-that-lets-you-meet-it-5ced</link>
      <guid>https://dev.to/aayush-bharadva/every-commit-youve-ever-pushed-was-feeding-a-tree-i-built-the-thing-that-lets-you-meet-it-5ced</guid>
      <description>&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Overgrowth&lt;/strong&gt; grows a living, breathing generative tree out of a GitHub username — a portrait of how a person builds, drawn from every repo, language, star and late-night push.&lt;/p&gt;

&lt;p&gt;Type a name. Watch a few seconds of growth. Meet the tree you've been feeding for years without knowing it. Then the tree does something I didn't expect to love this much: &lt;strong&gt;it writes you a short poem about yourself — a little lantern-carrying wanderer walks in under the canopy to deliver it — and reads it to you out loud.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And because passion is also rivalry: hit &lt;strong&gt;⚔ vs&lt;/strong&gt; and grow two trees side by side — you and a friend, stat face-off between them, one shareable link.&lt;/p&gt;

&lt;p&gt;The challenge said &lt;em&gt;passion&lt;/em&gt; — rivalries, fandom, the World Cup. But the line that got me was &lt;strong&gt;"the love that fuels late-night side projects."&lt;/strong&gt; That love already has a data trail; GitHub just renders it as the least romantic thing imaginable — a grid of flat green squares. I wanted the same history to grow something that looks &lt;em&gt;alive&lt;/em&gt;. The emotional distance between "a chart of my commits" and "a tree my commits have visibly been feeding" is the entire project.&lt;/p&gt;

&lt;p&gt;And it's honest. Your abandoned repos are right there on the tree — bare, grey, leafless. Every builder has them. The tree doesn't hide its scars, and that's what makes it a portrait instead of a decoration.&lt;/p&gt;

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

&lt;p&gt;👉 &lt;strong&gt;Grow yours: &lt;a href="https://overgrowth-one.vercel.app" rel="noopener noreferrer"&gt;https://overgrowth-one.vercel.app&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Type a username → the tree grows → a lantern-carrying wanderer delivers its poem → 🔊 hear it read aloud.&lt;/p&gt;

&lt;p&gt;Two trees I met this weekend, same API, opposite souls:&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%2F49uzbtpzxn6w7xlh24he.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%2F49uzbtpzxn6w7xlh24he.png" alt="Overgrowth tree grown from @torvalds — a moonlit monolingual C giant" width="800" height="567"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;@torvalds — a moonlit monolingual C giant, 250k-star blossoms, bare dead limbs, leaning into the night&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%2Fnqtupu5e8l19y5123us7.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%2Fnqtupu5e8l19y5123us7.png" alt="Overgrowth tree grown from @sindresorhus — a polyglot's dense multicolored canopy" width="800" height="567"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;&lt;a class="mentioned-user" href="https://dev.to/sindresorhus"&gt;@sindresorhus&lt;/a&gt; — a polyglot's dense multicolored canopy in real linguist colors&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My favorite reading it produced, for a tree grown from 15 years of C:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Fed by 15 years of C — leaning into the late hours — 250,742 stars in blossom, 2 scars it doesn't hide."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/ayushbharadva/overgrowth" rel="noopener noreferrer"&gt;https://github.com/ayushbharadva/overgrowth&lt;/a&gt;&lt;/strong&gt; — built entirely within the challenge window (see commit timestamps), AI-assisted with Claude Code, as the rules allow.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How behavior becomes biology.&lt;/strong&gt; The tree isn't a skin on a chart — builder behavior maps onto growth rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Years on GitHub + repos&lt;/strong&gt; → height and branching depth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push activity&lt;/strong&gt; → trunk thickness and growth speed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language diversity&lt;/strong&gt; → branching and canopy colors (GitHub's real linguist colors)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stars&lt;/strong&gt; → glowing blossoms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Late-night pushes&lt;/strong&gt; → the tree leans and grows crooked, reaching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Abandoned repos (18+ months)&lt;/strong&gt; → bare grey branches&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recency&lt;/strong&gt; → leaf color, vivid to brown&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A seeded RNG (mulberry32) keyed off the username makes it &lt;strong&gt;deterministic&lt;/strong&gt;: your tree is yours. Regrow it tomorrow — same tree. Push for another year — it will have changed, because you did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The generative core&lt;/strong&gt; is Next.js + TypeScript + Canvas 2D. Three unauthenticated GitHub REST calls run from the browser and distill into growth parameters for a recursive branch system. Things I had to learn the hard way in one weekend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Negative gravitropism&lt;/strong&gt; — early trees curled into drooping ferns. Real branches spring back toward the sky, so mine track their cumulative angle from vertical and correct toward it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Depth-damped lean&lt;/strong&gt; — a night owl's lean compounded over nine branching levels turns a tree into a spiral. Damped by depth, the tree &lt;em&gt;bends&lt;/em&gt; instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leaf-lightness floor&lt;/strong&gt; — C's linguist color is &lt;code&gt;#555555&lt;/code&gt;; without a minimum-lightness lift, Linus Torvalds' tree looked dead. Now it looks like a birch in moonlight, which felt right.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blossoms in a second pass&lt;/strong&gt; — 250k stars were getting buried under the leaves they earned.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Growth animates depth-by-depth via &lt;code&gt;requestAnimationFrame&lt;/code&gt;, then settles into idle sway with stars and fireflies — 60fps on the biggest trees I could find. Save your tree as PNG, share it with a &lt;code&gt;?u=&lt;/code&gt; link.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tree's voice.&lt;/strong&gt; Every tree gets a deterministic one-line reading composed from its stats. When the full stack is live, that upgrades: &lt;strong&gt;Gemini&lt;/strong&gt; receives the tree's raw signals — years, languages, blossoms, scars, night-owl rhythm — and writes a 3-line poem &lt;em&gt;in the voice of the tree, addressed to its owner&lt;/em&gt;. A tiny lantern-carrying wanderer walks in from the dark, stops under your canopy, and types it out in a speech bubble — then &lt;strong&gt;ElevenLabs&lt;/strong&gt; reads it aloud. Hearing a calm voice say the words your abandoned repos became is genuinely a little emotional, and I built the thing.&lt;/p&gt;

&lt;p&gt;Both run behind two small serverless routes so no keys ever touch the client — and everything degrades gracefully: no backend, and you still get the tree and its written reading.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best use of Google AI&lt;/strong&gt; — Gemini receives each tree's raw growth signals (years, languages, blossoms, scars, night-owl rhythm) and writes a 3-line poem &lt;em&gt;in the voice of the tree, addressed to its owner&lt;/em&gt;. Structured output (schema-enforced JSON, thinking disabled) keeps draft scratch-work out of the poem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best use of ElevenLabs&lt;/strong&gt; — the tree then reads its poem aloud while a little lantern-carrying wanderer delivers it under the canopy. Serverless route, keys never touch the client, and it degrades gracefully to the written reading.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Try &lt;code&gt;torvalds&lt;/code&gt; next to &lt;code&gt;sindresorhus&lt;/code&gt;. Then try your own username — that's the whole point. Every builder has been growing one of these for years. Come meet yours. 🌿&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>showdev</category>
      <category>creativecoding</category>
    </item>
  </channel>
</rss>
