<?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: Ashish Mishra</title>
    <description>The latest articles on DEV Community by Ashish Mishra (@ashish_mishra_8491c3b9912).</description>
    <link>https://dev.to/ashish_mishra_8491c3b9912</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%2F4038717%2F5291905e-58b8-4e5a-b027-f125e893d02b.png</url>
      <title>DEV Community: Ashish Mishra</title>
      <link>https://dev.to/ashish_mishra_8491c3b9912</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ashish_mishra_8491c3b9912"/>
    <language>en</language>
    <item>
      <title>Building a Disposable Notion Agent on Cheap Models</title>
      <dc:creator>Ashish Mishra</dc:creator>
      <pubDate>Thu, 20 Aug 2026 03:47:10 +0000</pubDate>
      <link>https://dev.to/ashish_mishra_8491c3b9912/building-a-disposable-notion-agent-on-cheap-models-m5b</link>
      <guid>https://dev.to/ashish_mishra_8491c3b9912/building-a-disposable-notion-agent-on-cheap-models-m5b</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; We built a one-shot HTTP worker that talks to Notion through MCP. Version one worked. Version two got cheaper and more readable, then failed in a new way. The harness was fine. The tool surface, the model, and the prompt were not the same problem, and we kept treating them as one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We keep seeing the same pitch: put an agent in the cloud, give it tools, let it live in Slack, let it remember you. That is a product. It is not the product we needed.&lt;/p&gt;

&lt;p&gt;We needed something dumber and more useful. Another service should be able to say "read this Notion page, write a summary somewhere, stop." No chat history. No personality that accretes over weeks. No always-on process. If nobody is calling it, it should cost nothing.&lt;/p&gt;

&lt;p&gt;We started calling that shape a &lt;strong&gt;one-shot agent&lt;/strong&gt;. One HTTP request. Tools for that request. A JSON result. Then the instance can go away.&lt;/p&gt;

&lt;p&gt;This is the path we actually walked: first working version, what it got wrong, the Markdown fork, and the cheaper tricks that mattered more than swapping frameworks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The job was never "build a chatbot"
&lt;/h2&gt;

&lt;p&gt;The first real task was almost boring. Once a week, pull a skill write-up from Notion, extract what mattered, and append it to a digest page. Callers would name pages in English. They would not paste Notion ids. If a name was ambiguous, the agent should refuse to write rather than guess.&lt;/p&gt;

&lt;p&gt;If that loop is wrong, people stop trusting write-back. If it is expensive, nobody schedules it. If it needs a human to babysit a terminal, it is not a system.&lt;/p&gt;

&lt;p&gt;So the constraints were social as much as technical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An external caller owns the schedule. The agent does not.&lt;/li&gt;
&lt;li&gt;The agent must be allowed to use tools, not just talk about them.&lt;/li&gt;
&lt;li&gt;Secrets stay in the environment, never in the request body.&lt;/li&gt;
&lt;li&gt;Idle time should be free.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Question you will probably ask:&lt;/strong&gt; why not a cron script that hits the Notion API directly?&lt;/p&gt;

&lt;p&gt;Because the &lt;em&gt;task&lt;/em&gt; changes every call. This week it is a weekly digest. Next week it is "list in-progress rows and do not write." We did not want a new Python file per job. We wanted one worker that takes a prompt plus a list of tool servers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common pitfall:&lt;/strong&gt; starting from a coding agent (shell, files, memory, a learning loop) and trying to shrink it into a request/response worker. You spend months deleting features you never wanted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape we committed to
&lt;/h2&gt;

&lt;p&gt;The loop is short on purpose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;caller
  |  POST /run  { system prompt, user prompt, which tools }
  v
one-shot agent  (starts if needed, dies when idle)
  |  model + tool loop
  v
MCP server for Notion
  |  integration token
  v
Notion API
  |
  +--&amp;gt; JSON back: result, tools used, tokens, cost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few choices fell out of that picture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTP, synchronous.&lt;/strong&gt; The caller waits. A weekly digest can wait two minutes. We did not want a job queue for v1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The agent is not the Notion client.&lt;/strong&gt; Notion (and later Slack, GitHub, whatever) lives behind &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;MCP&lt;/a&gt;: a small server that exposes tools. The agent image stays thin. The Notion token never enters the agent process as "the caller's header."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Search first.&lt;/strong&gt; Callers say "Weekly Skill," not a 32-character id. The model searches, picks a title match, and stops if it cannot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A cheap default model behind an OpenRouter-style gateway.&lt;/strong&gt; We did not want a Claude bill on every internal trigger. The first default was a very cheap DeepSeek flash model, on the order of a few cents per million tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision:&lt;/strong&gt; keep the harness thin (validate request, attach tools, run the loop, return JSON). Put intelligence in the model, the tool server, and the prompt. If quality is bad, we change those three before we change the loop.&lt;/p&gt;

&lt;p&gt;That last sentence sounds obvious. We still almost violated it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we looked at and walked past
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A hosted Notion MCP
&lt;/h3&gt;

&lt;p&gt;Notion's hosted connector is built for Claude Desktop and similar clients. It wants OAuth. We wanted an internal integration token in a secret store and a server we could run next to the agent. Self-hosting the official Notion MCP was the boring path that matched that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Identity for every caller
&lt;/h3&gt;

&lt;p&gt;The "correct" cloud version is: every caller has a service account, mints a short-lived token, the platform checks it. That is great when all callers live in your cloud. It is miserable when the next caller is a script on a laptop or a third-party job.&lt;/p&gt;

&lt;p&gt;We shipped a shared API key on the agent instead. Cloud ingress is open. The app checks the header and fails closed. Onboarding is "here is a key." Revoke is "rotate the key."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tradeoff:&lt;/strong&gt; you lose per-caller identity until you grow a key set. We accepted that for v1.&lt;/p&gt;

&lt;h3&gt;
  
  
  A heavier harness
&lt;/h3&gt;

&lt;p&gt;When quality wobbled, the tempting move was "use a real agent": something with memory, skills, a terminal, a personality that improves. We looked hard at that family (the persistent, self-improving runtimes people mean when they say they want Hermes-class agents).&lt;/p&gt;

&lt;p&gt;They are good at being &lt;em&gt;companions&lt;/em&gt;. They are the wrong shape for "POST, work, return, stop." They want state on disk. They want to stay up. They would make our Cloud Run bill and our threat model worse for a problem we did not have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision:&lt;/strong&gt; do not swap the loop because the model cannot name a tool. A thin loop plus OpenRouter already &lt;em&gt;is&lt;/em&gt; the harness. Quality lives one layer up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Baking the weekly-skill job into the system prompt
&lt;/h3&gt;

&lt;p&gt;This one is ideological, and it showed up in a real request.&lt;/p&gt;

&lt;p&gt;We had a generic worker. Someone sent a specific Notion page and a specific question ("summarize in-progress tasks"). The easy fix is to stuff that page name, that Status property, that persona into the system prompt.&lt;/p&gt;

&lt;p&gt;That prompt then becomes unusable for the next job. The agent is no longer one-shot and generic. It is a weekly-skill bot wearing a generic coat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pattern we now follow:&lt;/strong&gt; two layers.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;How to drive the tools&lt;/strong&gt; (stable). Search by name. Read before writing. Do not invent ids. If markdown shows a database stub, query the database. Do not write unless asked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What to do this run&lt;/strong&gt; (the caller). The actual task lives in the user prompt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Claude Desktop users barely write layer 1, because a strong model infers it from the tool list. A cheap model will not. Layer 1 has to be explicit, and it still must not contain this week's page title.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version one: named tools and a pile of JSON
&lt;/h2&gt;

&lt;p&gt;The first MVP was honest and a little ugly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Official Notion MCP, with a flat list of named tools (&lt;code&gt;search&lt;/code&gt;, &lt;code&gt;retrieve page&lt;/code&gt;, &lt;code&gt;append blocks&lt;/code&gt;, and friends).&lt;/li&gt;
&lt;li&gt;The model could see those names and call them.&lt;/li&gt;
&lt;li&gt;Reads came back as Notion's raw JSON block trees.&lt;/li&gt;
&lt;li&gt;Default model: the cheapest flash-class ID we could point at.&lt;/li&gt;
&lt;li&gt;Guardrails: 12 model steps, 20 tool calls, two minutes on the clock.&lt;/li&gt;
&lt;li&gt;Infra: two small containers that scale to zero. Notion's API is free for internal integrations. The bill that matters is tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It worked. Search found pages by title. A digest got appended. A successful run we logged was on the order of &lt;strong&gt;40,000 input tokens&lt;/strong&gt;, a few hundred output tokens, and about &lt;strong&gt;a quarter of a cent&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For a weekly job, that is fine. For something you want to call all day, you start staring at the 40k.&lt;/p&gt;

&lt;h2&gt;
  
  
  What version one actually got wrong
&lt;/h2&gt;

&lt;p&gt;The failure was not "it does not work." The failure was "it works in a way that does not travel."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSON is a terrible reading format for a summarizer.&lt;/strong&gt; Claude's own Notion connector feels good because it reads Markdown. We were stuffing block trees into a cheap model and asking it to sound like a colleague. Input tokens were the tax. Quality was the interest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cheap model was good enough for named tools.&lt;/strong&gt; That hid a landmine. When every tool has a clear name, flash-class models can finish in a handful of steps. We thought we had "the model problem" solved. We had only solved it for a flat tool list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploy was a human ritual.&lt;/strong&gt; Build three images on a laptop, remember &lt;code&gt;linux/amd64&lt;/code&gt; on Apple Silicon, push, hope. One arm64 image and Cloud Run will not start. That is not an agent problem. It still blocks the agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We almost taught callers the wrong lesson.&lt;/strong&gt; Version one accepted a vague system prompt. Official tools are discoverable. Callers copied that prompt to the next server. Then nothing worked, and it looked like the new server was broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question you will probably ask:&lt;/strong&gt; why not raise the step cap when a run dies?&lt;/p&gt;

&lt;p&gt;Because a confused loop is a money printer. Twelve steps is a fuse. If the model is guessing tool names, more steps buys more guesses. Fix the prompt and the model first. Raise the cap last, and only for jobs you have already seen succeed in fewer rounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Markdown experiment (this is version two's raw material)
&lt;/h2&gt;

&lt;p&gt;We wanted reads that look like documents, not like API dumps. We source-reviewed two community MCP servers that return Markdown. Both were MIT, both spoke Streamable HTTP, both kept the Notion token in the environment.&lt;/p&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;Awkoy-style (two meta-tools)&lt;/th&gt;
&lt;th&gt;Flat Markdown server (~40 named tools)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;How the model calls Notion&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;execute(operation, payload)&lt;/code&gt; plus a schema helper&lt;/td&gt;
&lt;td&gt;Direct tool names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema size in the prompt&lt;/td&gt;
&lt;td&gt;Tiny (hundreds of tokens)&lt;/td&gt;
&lt;td&gt;Large&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notion 429 handling&lt;/td&gt;
&lt;td&gt;Built-in pacing and retry&lt;/td&gt;
&lt;td&gt;You own it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Risk&lt;/td&gt;
&lt;td&gt;The model must &lt;em&gt;name&lt;/em&gt; the operation&lt;/td&gt;
&lt;td&gt;Tool-list overload&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Awkoy's trick is the interesting one. Instead of 40 tools, it exposes two. Every real action is an operation name inside a payload. The tool schema that lands in the prompt shrinks from something like &lt;strong&gt;17,000 tokens&lt;/strong&gt; to something like &lt;strong&gt;400&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is a real cost win &lt;em&gt;if&lt;/em&gt; the model can dispatch. There is almost no public evidence of people driving that two-tool surface from a flash-class model. Desktop Claude does it because Claude can. We were about to find out what DeepSeek flash does instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision:&lt;/strong&gt; do not replace version one. Run both. Callers pick a catalog name. Official named tools stay the simple path. Markdown stays the cheap-token path. Another scale-to-zero container is roughly free when idle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version two: same worker, different tools, a more expensive cheap model
&lt;/h2&gt;

&lt;p&gt;We pointed the same &lt;code&gt;/run&lt;/code&gt; body at the Markdown server: same generic prompt, same flash model, &lt;code&gt;use: markdown_notion&lt;/code&gt; instead of &lt;code&gt;use: official_notion&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two things happened, in order.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The run hit the step fuse
&lt;/h3&gt;

&lt;p&gt;The official server exposes search as a tool the model can see. The Markdown server does not. It exposes two verbs. The model has to say &lt;code&gt;search_pages&lt;/code&gt; &lt;em&gt;inside&lt;/em&gt; &lt;code&gt;execute&lt;/code&gt;. Our prompt never said that. Flash guessed, asked for schemas, retried, and burned 12 steps.&lt;/p&gt;

&lt;p&gt;A clean Markdown run is 3 to 5 steps. A confused one is 12 and an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt; named tools + vague prompt + flash = success.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;After:&lt;/strong&gt; two meta-tools + the same prompt + flash = &lt;code&gt;step_limit_exceeded&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The worker did not get worse. The instructions no longer matched the tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. When it did not fuse, it lied politely
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;get_page_markdown&lt;/code&gt; renders the page body. An inline database shows up as a stub: a title and an id, not the rows. The model would say "there is a database" and stop. That looks like a product bug. It is a missing rule.&lt;/p&gt;

&lt;p&gt;Rows only come from &lt;code&gt;query_database&lt;/code&gt;. If the user asked about tasks, status, or items, we have to query. We also cannot assume the property is called &lt;code&gt;Status&lt;/code&gt;. Real databases are named &lt;code&gt;State&lt;/code&gt;, &lt;code&gt;Stage&lt;/code&gt;, a checkbox, a board.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common pitfall:&lt;/strong&gt; writing a "working" prompt for one database (&lt;code&gt;Status = In progress&lt;/code&gt;) and putting it in the system prompt. The next caller has different properties. The agent filters into empty and looks broken.&lt;/p&gt;

&lt;p&gt;Version two's actual upgrade was not a new framework. It was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;tool primer&lt;/strong&gt; that names the two verbs, the search-then-read loop, and the database-stub rule.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;stronger cheap model&lt;/strong&gt; for that path (a V3-class DeepSeek, roughly three times the input price of flash, still far under frontier). Flash stays the default for named tools.&lt;/li&gt;
&lt;li&gt;The task still lives in the user prompt. No page names in the primer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Quality jumped because the model could finish the loop, not because we grew a memory subsystem.&lt;/p&gt;

&lt;p&gt;We also stopped shipping images from laptops as the happy path. Merge to main builds and deploys. Terraform still owns env and IAM. The pipeline owns tags. Those two fighting each other is its own footgun (ignore image changes in Terraform, never apply Terraform from the image job).&lt;/p&gt;

&lt;h2&gt;
  
  
  What we would copy if we started tomorrow
&lt;/h2&gt;

&lt;p&gt;These are the optimizations that paid rent. Not a toolkit dump. A short list of bets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Pay for tokens where they leak.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Official JSON reads were the 40k-input problem. Markdown plus a tiny tool schema attacks that directly. Extra round-trips from a weak dispatcher can eat the savings. Measure both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Price the model to the tool surface, not to the brand.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Flash is fine when tools have names. Meta-dispatch wants a model that can emit &lt;code&gt;query_database&lt;/code&gt; on the first try. We would rather spend ~$0.21 per million input tokens and finish in four steps than spend ~$0.07 and fail at twelve. Failed runs are not cheap. They are delayed work plus a retry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Keep idle at zero.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The worker and each MCP server scale to nothing. Notion's API did not add a monthly line item. The LLM bill is the bill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Put a fuse on the loop.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Steps, tool calls, wall clock. Return a structured error, not a 500 with a stack trace. Log a trace id. The caller should be able to paste that id, not a screenshot of Cloud logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Split "how to use tools" from "what to do."&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
This is the one we would tattoo on the repo. It is also how you keep one worker generic while still running a weekly digest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Search by name. Refuse to guess ids.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Callers should not hold Notion's internal ids. If two pages match, stop. Write-back to the wrong page is worse than a failed run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Do not confuse "simpler implementation" with "fewer moving parts in the prompt."&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A two-tool MCP is simpler &lt;em&gt;for the server&lt;/em&gt;. It is harder &lt;em&gt;for a cheap model&lt;/em&gt;. Simplicity moved. We had to put it back in English.&lt;/p&gt;

&lt;p&gt;Rough numbers we actually use as a gut check (prices move; re-check before you budget):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Order of magnitude&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Official MCP success, flash&lt;/td&gt;
&lt;td&gt;~40k input, ~$0.002 to $0.01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Markdown tool schema vs official tool list&lt;/td&gt;
&lt;td&gt;~400 tokens vs ~17k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flash vs V3-class DeepSeek (input / 1M)&lt;/td&gt;
&lt;td&gt;~$0.07 vs ~$0.21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle Cloud Run&lt;/td&gt;
&lt;td&gt;~$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notion internal integration&lt;/td&gt;
&lt;td&gt;$0 API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What we are trying next
&lt;/h2&gt;

&lt;p&gt;None of this is a promise. It is where the pain is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More tools on the same worker.&lt;/strong&gt; Slack, GitHub, meeting transcripts. The catalog should grow without a new harness. If we cannot add a server without rewriting &lt;code&gt;/run&lt;/code&gt;, the one-shot idea failed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A harness that is actually seamless.&lt;/strong&gt; Today the "harness" is still a pile of conventions: which catalog name, which model, which primer. We want a structure where a caller says the job and the worker picks a sane tool server and a sane model. Disposable should feel like calling a function, not like configuring an IDE.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A way to learn without becoming a companion.&lt;/strong&gt; Persistent agents learn by writing skills to disk and living forever. We still want one-shot. The interesting version is: after a good run, store a &lt;em&gt;primer delta&lt;/em&gt; or a &lt;em&gt;eval fixture&lt;/em&gt; somewhere external. The next cold start can load it. The instance still dies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it on real internal work.&lt;/strong&gt; Weekly digest was the wedge. The point is other systems posting jobs: standups, research write-backs, "what changed in this database." If we only ever curl it ourselves, we built a demo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compare answers without a human reading every one.&lt;/strong&gt; Judging the final response is the slow part now. We need a fixture shape: same prompt, same Notion snapshot (or a recorded tool trace), two models or two primers, a score that is not "the author liked it." Until that exists, we will keep arguing from anecdotes and burning evenings on single traces.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you are in the same spot
&lt;/h2&gt;

&lt;p&gt;Start with a named-tool MCP and a cheap model. Prove search, read, write, and "do not guess." Then, and only then, switch the read path to Markdown and spend a little more on the model that has to dispatch.&lt;/p&gt;

&lt;p&gt;Keep the worker boring. Make the prompt boring in the right way: how tools work, not what this week's page is called.&lt;/p&gt;

&lt;p&gt;And if a run starts saying "there is a database," believe the stub. Query the rows. The page was never the table.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>mcp</category>
      <category>llm</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Automated First-Pass PR Reviews - Build, Buy, or Use Your Coding Agent’s Cloud?</title>
      <dc:creator>Ashish Mishra</dc:creator>
      <pubDate>Fri, 24 Jul 2026 02:52:25 +0000</pubDate>
      <link>https://dev.to/ashish_mishra_8491c3b9912/automated-first-pass-pr-reviews-build-buy-or-use-your-coding-agents-cloud-hk3</link>
      <guid>https://dev.to/ashish_mishra_8491c3b9912/automated-first-pass-pr-reviews-build-buy-or-use-your-coding-agents-cloud-hk3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I wanted every new PR to get an automatic &lt;strong&gt;first-turn brief&lt;/strong&gt; — summary, risks, security/performance smells — before I deep-dive. After comparing SaaS review bots, building a custom agent, and using Cursor’s cloud agents from a GitHub Action, I chose the Action + cloud-agent path because I already pay for Cursor Pro+, I wanted the pipeline in my repo, and I cared more about a trustworthy first pass than a fully productized review platform.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem I was actually solving
&lt;/h2&gt;

&lt;p&gt;Not “replace human review.” Not “ship a full AppSec program.”&lt;/p&gt;

&lt;p&gt;I wanted a &lt;strong&gt;first turn&lt;/strong&gt;: when a PR opens, something reads the change and leaves a structured brief so I’m not starting cold. Ideal shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is this PR trying to do?&lt;/li&gt;
&lt;li&gt;Bugs / logic risks worth noticing early&lt;/li&gt;
&lt;li&gt;Security smells (secrets, auth, injection-shaped code)&lt;/li&gt;
&lt;li&gt;Performance smells (hot paths, N+1s, wasteful work)&lt;/li&gt;
&lt;li&gt;A clear signal: looks fine / needs changes / needs discussion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Later I also want staging previews — but that’s usually a &lt;strong&gt;separate&lt;/strong&gt; pipeline. This piece is about the &lt;strong&gt;analysis brief&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I already had: GitHub repos + a &lt;strong&gt;Cursor Pro+&lt;/strong&gt; subscription. I didn’t have a settled opinion on build vs buy vs “use the agent product I already pay for.”&lt;/p&gt;

&lt;h2&gt;
  
  
  What “good enough” first-pass analysis can (and can’t) do
&lt;/h2&gt;

&lt;p&gt;Almost every AI review path — bot, DIY agent, or cloud coding agent — is strong at &lt;strong&gt;reasoning over the diff and nearby code&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Usually catches&lt;/th&gt;
&lt;th&gt;Usually needs extra tools&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Logic bugs, edge cases, off-by-ones&lt;/td&gt;
&lt;td&gt;Live CVE / dependency vulns (&lt;code&gt;npm audit&lt;/code&gt;, Snyk, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code smells, complexity, inconsistency&lt;/td&gt;
&lt;td&gt;Binary / artifact scanning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security &lt;em&gt;patterns&lt;/em&gt; (secrets in code, missing checks)&lt;/td&gt;
&lt;td&gt;Formal coverage gates (unless tests are run and parsed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance anti-patterns in visible code&lt;/td&gt;
&lt;td&gt;Org-wide policy engines and compliance attestations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the industry split is less “AI vs not AI” and more:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic CI&lt;/strong&gt; — lint, tests, build, secret scan, dependency audit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI first-pass&lt;/strong&gt; — narrative brief + judgment calls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human&lt;/strong&gt; — merge decision&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preview/staging&lt;/strong&gt; — clickable environment (often orthogonal)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Treat the AI layer as a sharp intern’s notes, not the only gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Industry pattern underneath every option
&lt;/h2&gt;

&lt;p&gt;Whatever logo is on the bot, the system is always:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Listen  →  something wants a review (PR open, /review, Slack)
Think   →  model + tools gather context and reason
Speak   →  comment on the PR (or Slack, or both)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you build it yourself, you own all three.&lt;br&gt;&lt;br&gt;
If you buy a product, they own Think (and often Speak).&lt;br&gt;&lt;br&gt;
If you use a coding agent’s cloud runtime, you often own Listen/Speak and rent Think.&lt;/p&gt;

&lt;h2&gt;
  
  
  The main roads people take
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Buy a PR review product (e.g. CodeRabbit and peers)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What you get:&lt;/strong&gt; Install a GitHub App, get inline/threaded comments, summaries, sometimes learning from your repo style, dashboards, team defaults.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strengths&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fastest path to “PRs get reviewed comments”&lt;/li&gt;
&lt;li&gt;Productized UX (inline nits, severity, ignore rules)&lt;/li&gt;
&lt;li&gt;Less prompt-engineering on day one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tradeoffs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Another vendor in the trust/security review&lt;/li&gt;
&lt;li&gt;Opinionated comment style; tuning is product settings, not “your agent”&lt;/li&gt;
&lt;li&gt;Overlap with tools you may already pay for (IDE agents, CI scanners)&lt;/li&gt;
&lt;li&gt;Cost scales with seats/repos/PR volume in &lt;em&gt;their&lt;/em&gt; pricing, not your existing AI plan&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best when:&lt;/strong&gt; You want a dedicated review product and don’t care that Think lives outside your coding agent.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Build your own Listen → Think → Speak system
&lt;/h3&gt;

&lt;p&gt;Classic DIY: GitHub webhook or Actions → call a model API with tools (&lt;code&gt;fetch PR&lt;/code&gt;, &lt;code&gt;get file&lt;/code&gt;, maybe &lt;code&gt;gh&lt;/code&gt;) → post a comment. Optional Slack: paste a PR link, get a thread reply.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strengths&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full control of prompt, severity bar, and where results go&lt;/li&gt;
&lt;li&gt;Can mix models, scanners, and internal APIs&lt;/li&gt;
&lt;li&gt;Clear architecture; easy to explain to security teams&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tradeoffs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You maintain tool loops, auth, retries, prompt drift&lt;/li&gt;
&lt;li&gt;Large diffs need careful context management&lt;/li&gt;
&lt;li&gt;Slack needs an immediate ACK (3s) and async work&lt;/li&gt;
&lt;li&gt;You’re on the hook for reliability (“why didn’t the bot run?”)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best when:&lt;/strong&gt; Review is a core workflow you want to own, or you must stay inside strict data boundaries.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Use your coding agent’s cloud features (e.g. Cursor cloud agents / Automations)
&lt;/h3&gt;

&lt;p&gt;Newer path: the same class of agent you use in the IDE can run in the cloud against a PR — either via &lt;strong&gt;native Automations&lt;/strong&gt; (PR opened → comment) or via a &lt;strong&gt;thin GitHub Action&lt;/strong&gt; that starts a cloud agent through an API and posts the result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strengths&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Think quality close to the agent you already trust day to day&lt;/li&gt;
&lt;li&gt;Can reuse one subscription’s cloud usage (for me: Pro+)&lt;/li&gt;
&lt;li&gt;Automations minimize glue; Actions keep the trigger in &lt;code&gt;.github/workflows&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tradeoffs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Usage is token/spend based — deep reviews can run &lt;strong&gt;10–15+ minutes&lt;/strong&gt; and burn budget faster than IDE chat&lt;/li&gt;
&lt;li&gt;API-started agents may need the Action to &lt;strong&gt;Speak&lt;/strong&gt; (post the comment) even if Think succeeded&lt;/li&gt;
&lt;li&gt;Org repos need GitHub App + Actions permissions sorted&lt;/li&gt;
&lt;li&gt;Not a full substitute for dependency CVE scanners&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best when:&lt;/strong&gt; You already live in that agent daily and want the first-pass voice to match.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Hybrid (what most mature teams quietly do)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Secret scan + &lt;code&gt;npm audit&lt;/code&gt; / SAST in CI (&lt;strong&gt;Speak:&lt;/strong&gt; check failures)&lt;/li&gt;
&lt;li&gt;AI brief on PR open (&lt;strong&gt;Speak:&lt;/strong&gt; one summary comment)&lt;/li&gt;
&lt;li&gt;Preview deploy URL from hosting/CI (&lt;strong&gt;Speak:&lt;/strong&gt; environment link)&lt;/li&gt;
&lt;li&gt;Humans for merge&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best when:&lt;/strong&gt; You want seatbelts &lt;em&gt;and&lt;/em&gt; a narrative first turn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Time to first useful comment&lt;/th&gt;
&lt;th&gt;Ownership&lt;/th&gt;
&lt;th&gt;Cost shape&lt;/th&gt;
&lt;th&gt;Feels like “my agent”?&lt;/th&gt;
&lt;th&gt;First-pass summary quality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SaaS review bot&lt;/td&gt;
&lt;td&gt;Hours&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Per product pricing&lt;/td&gt;
&lt;td&gt;Low–medium&lt;/td&gt;
&lt;td&gt;High UX, fixed personality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DIY agent system&lt;/td&gt;
&lt;td&gt;Days–weeks&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Model API + your time&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;As good as your prompt/tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud coding agent + Automation&lt;/td&gt;
&lt;td&gt;Hours&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Included in agent plan usage&lt;/td&gt;
&lt;td&gt;Medium–high&lt;/td&gt;
&lt;td&gt;High if prompt is good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud coding agent + GitHub Action&lt;/td&gt;
&lt;td&gt;Half day–days&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Plan usage + CI minutes&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;High if prompt is good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hybrid&lt;/td&gt;
&lt;td&gt;Ongoing&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;CI + AI + host&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Best overall risk coverage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I chose — and why
&lt;/h2&gt;

&lt;p&gt;I went with &lt;strong&gt;GitHub Action → Cursor cloud agent → PR comment&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not because SaaS bots are bad. Not because DIY is “more pure.” Because of &lt;em&gt;my&lt;/em&gt; constraints:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;I already pay for Cursor Pro+.&lt;/strong&gt; I wanted the first-pass brain to be the same one I trust in the editor, not a second personality I have to learn to ignore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I wanted a brief, not a product.&lt;/strong&gt; One structured comment before I dive in — summary, bugs, security, performance, verdict — was enough for v1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I wanted the trigger in my repo.&lt;/strong&gt; A workflow file I can read, diff, and tweak beats a dashboard full of vendor toggles (for me). Automations were a strong alternative; I still preferred Option-B-style glue I own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I accepted the honest limits.&lt;/strong&gt; Pattern-level security/perf in the brief; real CVE scanning stays in CI later. Staging deploys stay a separate preview pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build-from-scratch DIY&lt;/strong&gt; was attractive in research, but I didn’t need to own the tool loop on day one when a cloud agent already knows how to explore a repo.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What the decision felt like in practice
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Listen:&lt;/strong&gt; &lt;code&gt;pull_request&lt;/code&gt; opened / ready for review (skip drafts)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Think:&lt;/strong&gt; cloud agent with &lt;code&gt;prUrl&lt;/code&gt; — no analysis on the Actions runner itself&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speak:&lt;/strong&gt; Action posts with &lt;code&gt;gh pr comment --repo …&lt;/code&gt; (because the runner never checked out git)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The surprising lesson: people ask “how does it analyze if Actions didn’t checkout?” — &lt;strong&gt;it doesn’t analyze there.&lt;/strong&gt; The cloud agent clones elsewhere. The Action is a remote control.&lt;/p&gt;

&lt;p&gt;The painful lesson: deep first-pass reviews are &lt;strong&gt;slower and hungrier&lt;/strong&gt; than IDE chat. Budget them. Read your usage dashboard after the first few runs.&lt;/p&gt;

&lt;p&gt;The quality lesson: &lt;strong&gt;the prompt is the product.&lt;/strong&gt; A generic “review this” gets generic notes. A forced structure (summary → risks → security → performance → verdict) is what makes the comment useful &lt;em&gt;before&lt;/em&gt; you dive in.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I’d advise someone still deciding
&lt;/h2&gt;

&lt;p&gt;Ask yourself, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Do I want a productized review UX (inline threads, team policies)?&lt;/strong&gt; → Start with a SaaS bot; add CI scanners beside it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do I already pay for a coding agent with cloud/Automations?&lt;/strong&gt; → Try native Automation or a thin Action before building a custom tool loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do I need Slack-first ChatOps or weird internal tools?&lt;/strong&gt; → DIY Listen/Speak; rent any Think backend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do I need CVE/policy gates?&lt;/strong&gt; → Those are CI jobs either way; don’t make the LLM the only security control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do I need staging?&lt;/strong&gt; → Preview deploys are usually CI/CD; let the AI brief link to the URL later.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My answer to (2) was yes, so I didn’t buy another review logo — I aimed the agent I already have at GitHub and made the first turn automatic.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;first-pass PR brief&lt;/strong&gt; is a different job from full human review or full AppSec.&lt;/li&gt;
&lt;li&gt;Every approach is Listen → Think → Speak; you’re only choosing who owns each box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buy&lt;/strong&gt; for speed and review UX; &lt;strong&gt;build&lt;/strong&gt; for control; &lt;strong&gt;reuse your coding agent’s cloud&lt;/strong&gt; when the subscription and trust are already there.&lt;/li&gt;
&lt;li&gt;Hybrid with deterministic scanners + AI summary + human merge is still the grown-up default.&lt;/li&gt;
&lt;li&gt;If you automate cloud agents from Actions, own the comment step, expect multi-minute runs, and treat prompt design as the real feature.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I didn’t want more noise in the PR timeline. I wanted a calm first paragraph from something that already thinks like I do when I’m in the editor — waiting for me before I dive in.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>automation</category>
      <category>github</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>From "You Have a Bug" to "Here's the Root Cause" - Adding AI Code Analysis to My App Review Pipeline</title>
      <dc:creator>Ashish Mishra</dc:creator>
      <pubDate>Tue, 21 Jul 2026 01:33:50 +0000</pubDate>
      <link>https://dev.to/ashish_mishra_8491c3b9912/from-you-have-a-bug-to-heres-the-root-cause-adding-ai-code-analysis-to-my-app-review-pipeline-51f1</link>
      <guid>https://dev.to/ashish_mishra_8491c3b9912/from-you-have-a-bug-to-heres-the-root-cause-adding-ai-code-analysis-to-my-app-review-pipeline-51f1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I built an app review pipeline that classifies bugs and crashes. It was useful but stopped at "what's wrong." I extended it to answer "where in the code" and "what to change" — using a PydanticAI agent with codebase exploration tools, then made the analysis engine pluggable so it can use Grok Build, Claude Code, or OpenAI Codex as backends. Here's how I got there, what broke along the way, and how the architecture ended up.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where I Started
&lt;/h2&gt;

&lt;p&gt;I have a CLI tool called &lt;strong&gt;AppPulse&lt;/strong&gt; that monitors my app's reviews and crash data. Every morning it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Pulls new reviews from Google Play / App Store&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Classifies each one with an LLM — bug, crash, feature request, performance, praise&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Correlates reviews with crash data from Sentry/Firebase&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sends me a digest&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;apppulse run
&lt;span class="c"&gt;# → "12 new reviews: 3 bugs (1 critical), 2 feature requests, 7 praise"&lt;/span&gt;

apppulse reviews &lt;span class="nt"&gt;--category&lt;/span&gt; bug
&lt;span class="c"&gt;#  ID │ Rating │ Summary                              │ Severity&lt;/span&gt;
&lt;span class="c"&gt;#  42 │  ★☆☆☆☆ │ Photo upload crashes for &amp;gt;10MB images │ critical&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was genuinely useful. I knew &lt;em&gt;what&lt;/em&gt; users were complaining about and which crashes were affecting the most people. But the pipeline stopped at classification. When I saw "Photo upload crashes for &amp;gt;10MB images," I still had to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Open the project in my IDE&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Search for upload-related code&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cross-reference the stacktrace from Sentry&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mentally map the review to the actual code&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Figure out what to change&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For one bug, that's fine. For a batch of 5 critical bugs on a Monday morning, it's a lot of manual work before I've even started fixing anything.&lt;/p&gt;

&lt;p&gt;I wanted the pipeline to go further — to take the classified review, point to the specific files and lines causing the issue, and hand me a brief I could act on immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Version: A PydanticAI Agent with Codebase Tools
&lt;/h2&gt;

&lt;p&gt;The core idea was simple: give an LLM agent access to my codebase through read-only tools and let it investigate the bug, just like I would.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why PydanticAI
&lt;/h3&gt;

&lt;p&gt;I considered LangGraph, LangChain, and rolling my own agent loop. PydanticAI won for three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Structured output by default.&lt;/strong&gt; I needed the analysis to come back as a validated Pydantic model — not free-form text I'd have to parse. PydanticAI auto-validates the LLM output against the schema and retries with correction instructions if it's wrong.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Minimal dependency footprint.&lt;/strong&gt; AppPulse is a lightweight CLI. I didn't want to drag in LangChain's dependency tree for a single-agent linear workflow.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It already supports the LLM providers I use&lt;/strong&gt; — OpenAI, Anthropic, Ollama. No new API keys.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Output Model
&lt;/h3&gt;

&lt;p&gt;Every analysis produces a validated &lt;code&gt;AnalysisBrief&lt;/code&gt;:&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;class&lt;/span&gt; &lt;span class="nc"&gt;AnalysisBrief&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;issue_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;              &lt;span class="c1"&gt;# bug | crash | feature_request
&lt;/span&gt;    &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;                 &lt;span class="c1"&gt;# one-paragraph assessment
&lt;/span&gt;    &lt;span class="n"&gt;root_cause&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;       &lt;span class="c1"&gt;# for bugs/crashes
&lt;/span&gt;    &lt;span class="n"&gt;feature_rationale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="c1"&gt;# for feature requests
&lt;/span&gt;    &lt;span class="n"&gt;affected_files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AffectedFile&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;proposed_changes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ProposedChange&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;complexity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;              &lt;span class="c1"&gt;# small | medium | large
&lt;/span&gt;    &lt;span class="n"&gt;risks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&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="c1"&gt;# potential side effects
&lt;/span&gt;    &lt;span class="n"&gt;testing_notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;           &lt;span class="c1"&gt;# what tests to write/update
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the contract between the analysis engine and the rest of the pipeline. Whether the analysis runs in-process or via an external coding agent, it always produces the same structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Four Built-in Tools
&lt;/h3&gt;

&lt;p&gt;The agent gets four tools — all read-only, all operating on the local codebase:&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="nd"&gt;@analysis_agent.tool&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tool_search_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RunContext&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AnalysisDeps&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;keyword&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;file_extensions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&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;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;grep/ripgrep search — finds where relevant code lives.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;search_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file_extensions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@analysis_agent.tool&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tool_read_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RunContext&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AnalysisDeps&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;file_path&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;start_line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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="n"&gt;end_line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Read source with line range, capped at 500 lines, path-validated.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;read_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start_line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end_line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@analysis_agent.tool&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tool_list_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RunContext&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AnalysisDeps&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&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="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&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;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Directory listing with sizes.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;list_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@analysis_agent.tool&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tool_find_symbols&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RunContext&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AnalysisDeps&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Regex-based definition search — classes, functions, methods.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;find_symbols&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every tool validates paths against &lt;code&gt;code_path&lt;/code&gt; to prevent directory traversal — no &lt;code&gt;../../etc/passwd&lt;/code&gt; games. The agent can explore but never escape the project root.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem with V1: Too Many API Calls
&lt;/h3&gt;

&lt;p&gt;The first version worked, but the agent was expensive. It would make 40-50 tool calls — listing directories, searching for broad terms, reading entire files — before settling on a hypothesis. At &lt;code&gt;gpt-4o-mini&lt;/code&gt; rates that's still cheap, but it hit the API request limit I'd set and sometimes never produced output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimization 1: The Repo Map
&lt;/h2&gt;

&lt;p&gt;The biggest waste was the agent discovering project structure. It would call &lt;code&gt;list_files&lt;/code&gt; recursively, trying to understand where things lived before it could even start investigating.&lt;/p&gt;

&lt;p&gt;The fix: generate a &lt;strong&gt;repo map&lt;/strong&gt; upfront and include it in the system prompt. The repo map is a compact text overview of every source file with its top-level symbols (classes, functions) extracted via regex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/services/
  ImageUploadService.java — ImageUploadService, compress, validateSize, uploadToS3
  UserService.java — UserService, getProfile, updatePreferences
src/utils/
  ImageUtils.java — ImageUtils, downscale, getExifOrientation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives the agent a "GPS" before it makes any tool calls. Instead of blindly listing directories, it can read the repo map and go straight to &lt;code&gt;ImageUploadService.java&lt;/code&gt; when investigating an upload crash.&lt;/p&gt;

&lt;h3&gt;
  
  
  Caching the Repo Map
&lt;/h3&gt;

&lt;p&gt;Generating the repo map means walking the entire source tree and reading every file for definitions. For a medium project (~500 files), that's 100-500ms. Fine for a one-off, but I didn't want to pay that cost on every analysis.&lt;/p&gt;

&lt;p&gt;The cache strategy uses a git-based fingerprint:&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;_compute_fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Git repos: HEAD commit + dirty-file count (~5ms)
&lt;/span&gt;    &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;git&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;rev-parse&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;HEAD&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;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;git&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;status&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;--porcelain&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;dirty_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&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="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&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;commit&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;dirty_count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()[:&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cache hit: ~0ms. Cache miss (new commit or uncommitted changes): regenerate. The cache self-invalidates on every git operation that changes the working tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimization 2: Progressive Tool Gating
&lt;/h2&gt;

&lt;p&gt;Even with the repo map, the agent would sometimes keep exploring forever — finding tangential files, searching for related patterns, going down rabbit holes. Sound familiar? It's what I do when debugging at 2am.&lt;/p&gt;

&lt;p&gt;The fix is a &lt;strong&gt;funnel&lt;/strong&gt; that progressively restricts the agent's tools:&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;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_prepare_tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RunContext&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AnalysisDeps&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;tool_defs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ToolDefinition&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="n"&gt;tool_call_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;count_tool_calls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tool_call_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;18&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="c1"&gt;# No tools — force the agent to produce output
&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tool_call_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Only read_file — no more searching, refine what you already found
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;td&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;td&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tool_defs&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;td&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&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;EXPLORATION_TOOLS&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tool_defs&lt;/span&gt;        &lt;span class="c1"&gt;# Full access during exploration phase
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Tool Calls&lt;/th&gt;
&lt;th&gt;Available Tools&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Exploration&lt;/td&gt;
&lt;td&gt;0-12&lt;/td&gt;
&lt;td&gt;All 4 tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deep-dive&lt;/td&gt;
&lt;td&gt;12-18&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;read_file&lt;/code&gt; only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forced output&lt;/td&gt;
&lt;td&gt;18+&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This dropped the average analysis from 40+ tool calls to 8-12, with no noticeable quality difference. The agent just gets to the point faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Step: Pluggable Analysis Backends
&lt;/h2&gt;

&lt;p&gt;The in-process PydanticAI agent worked well for straightforward bugs. But for complex, multi-file issues — or when I wanted deeper analysis — I kept wishing I could point a full coding agent like Grok Build or Claude Code at the codebase and just get back a structured brief.&lt;/p&gt;

&lt;p&gt;All of these tools support headless mode:&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;# Grok Build&lt;/span&gt;
grok &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"analyze this bug..."&lt;/span&gt; &lt;span class="nt"&gt;--cwd&lt;/span&gt; /path/to/code &lt;span class="nt"&gt;--tools&lt;/span&gt; &lt;span class="s2"&gt;"read_file,grep,list_dir"&lt;/span&gt; &lt;span class="nt"&gt;--yolo&lt;/span&gt;

&lt;span class="c"&gt;# Claude Code&lt;/span&gt;
claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"analyze this bug..."&lt;/span&gt; &lt;span class="nt"&gt;--cwd&lt;/span&gt; /path/to/code &lt;span class="nt"&gt;--dangerously-skip-permissions&lt;/span&gt;

&lt;span class="c"&gt;# OpenAI Codex&lt;/span&gt;
codex &lt;span class="nt"&gt;--quiet&lt;/span&gt; &lt;span class="nt"&gt;--approval-mode&lt;/span&gt; full-auto &lt;span class="s2"&gt;"analyze this bug..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The challenge: each agent returns its output differently (JSON, plain text, structured conversations), and I still needed a validated &lt;code&gt;AnalysisBrief&lt;/code&gt; at the end.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Two-Stage Pipeline
&lt;/h3&gt;

&lt;p&gt;The solution is a two-stage pipeline for external backends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Stage 1: Coding Agent → Raw Markdown
Stage 2: Cheap LLM (structurer) → Validated AnalysisBrief
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Stage 1&lt;/strong&gt; runs the coding agent with a prompt that asks it to produce a markdown analysis with specific sections (Summary, Root Cause, Affected Files, Proposed Changes, etc.). The agent uses its own tools to explore the codebase — it's much better at this than my four built-in tools because it has full access to grep, file reading, and code understanding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 2&lt;/strong&gt; is a single, cheap &lt;code&gt;gpt-4o-mini&lt;/code&gt; call that extracts the markdown into the &lt;code&gt;AnalysisBrief&lt;/code&gt; Pydantic model. No tools, no exploration — just structured extraction. Costs fractions of a cent.&lt;/p&gt;

&lt;p&gt;The raw markdown from Stage 1 is saved as an inspectable artifact at &lt;code&gt;~/.apppulse/analyses/raw/&lt;/code&gt;. If the structured output looks wrong, I can check exactly what the coding agent found.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Backend Architecture
&lt;/h3&gt;

&lt;p&gt;I wanted to be able to add new backends without touching the core analysis flow. The architecture uses a lazy-loaded registry with a strategy pattern:&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;# backends/__init__.py
&lt;/span&gt;&lt;span class="n"&gt;BACKEND_REGISTRY&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="nb"&gt;str&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="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;builtin&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;apppulse.code_analysis.backends.builtin:BuiltinBackend&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;grok&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;apppulse.code_analysis.backends.grok:GrokBackend&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;claude_code&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;apppulse.code_analysis.backends.claude_code:ClaudeCodeBackend&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;codex&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;apppulse.code_analysis.backends.codex:CodexBackend&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_backend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;BaseAnalysisBackend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;module_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;class_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BACKEND_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;rsplit&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;importlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;import_module&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="p"&gt;)()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Backends are imported lazily — if you never use Grok, &lt;code&gt;grok.py&lt;/code&gt; is never loaded. Adding a new backend is three steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Create a module that subclasses &lt;code&gt;ExternalAgentBackend&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Implement &lt;code&gt;_build_command()&lt;/code&gt; — return the argv list&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add one line to &lt;code&gt;BACKEND_REGISTRY&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's the entire Grok backend:&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;class&lt;/span&gt; &lt;span class="nc"&gt;GrokBackend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ExternalAgentBackend&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;display_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;Grok Build&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;cli_command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_build_command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_turns&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;grok&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;-p&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--cwd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--tools&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;read_file,grep,list_dir&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# read-only
&lt;/span&gt;            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--yolo&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;--output-format&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;json&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;--max-turns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_turns&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;Everything else — prompt assembly, subprocess management, output parsing, raw artifact saving, and the Stage 2 structurer call — is handled by the &lt;code&gt;ExternalAgentBackend&lt;/code&gt; base class. The concrete backend only knows how to build its CLI command.&lt;/p&gt;

&lt;h3&gt;
  
  
  Switching Backends
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;config.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;code_analysis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&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;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grok"&lt;/span&gt;        &lt;span class="c1"&gt;# swap to "builtin", "claude_code", or "codex"&lt;/span&gt;
  &lt;span class="na"&gt;max_turns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;           &lt;span class="c1"&gt;# how many exploration turns for external agents&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The setup wizard auto-detects which CLIs are installed on your PATH and lets you pick:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Available analysis backends:
  1. builtin     — Built-in (PydanticAI) — lightweight, no extra tools needed
  2. grok        — Grok Build — headless mode, read-only tool access
  3. claude_code — Claude Code — deep exploration, 200K context
Choose a backend [1]:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What the Output Looks Like
&lt;/h2&gt;

&lt;p&gt;Whether using the builtin or an external backend, the output is the same validated structure:&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="nv"&gt;$ &lt;/span&gt;apppulse analyze 42

Analyzing review &lt;span class="c"&gt;#42 against MyApp Android codebase...&lt;/span&gt;
  Code path: /Users/me/projects/myapp
  Issue &lt;span class="nb"&gt;type&lt;/span&gt;: bug
  Backend:   grok

  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Launching Grok Build agent...
  ... Agent exploring codebase &lt;span class="o"&gt;(&lt;/span&gt;this may take 30-60s&lt;span class="o"&gt;)&lt;/span&gt;...
  OK Raw analysis saved to ~/.apppulse/analyses/raw/MyApp_bug_20250710_091523.md
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Structuring analysis into brief...
  OK Analysis &lt;span class="nb"&gt;complete&lt;/span&gt;

┌──────────────────────────────────────────────────────────────┐
│  CODE ANALYSIS — Review &lt;span class="c"&gt;#42                                   │&lt;/span&gt;
│  Photo upload crashes &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;10MB images                        │
│  Type: bug │ Complexity: medium                               │
├──────────────────────────────────────────────────────────────┤
│  ROOT CAUSE                                                   │
│  ImageUploadService.compress&lt;span class="o"&gt;()&lt;/span&gt; loads the full bitmap into     │
│  memory before compression. Images &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;10MB exceed the heap      │
│  limit on devices with ≤4GB RAM → OutOfMemoryError.           │
│                                                               │
│  AFFECTED FILES                                               │
│  ImageUploadService.java &lt;span class="o"&gt;(&lt;/span&gt;lines 45-89&lt;span class="o"&gt;)&lt;/span&gt; — compress&lt;span class="o"&gt;()&lt;/span&gt; method    │
│  ImageUtils.java &lt;span class="o"&gt;(&lt;/span&gt;lines 12-35&lt;span class="o"&gt;)&lt;/span&gt; — no input size validation     │
│                                                               │
│  PROPOSED CHANGES                                             │
│  1. Add BitmapFactory.Options.inSampleSize &lt;span class="k"&gt;for &lt;/span&gt;downsampling   │
│  2. Add file size validation before compress&lt;span class="o"&gt;()&lt;/span&gt;                │
│  3. Use streaming compression instead of &lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="nt"&gt;-memory&lt;/span&gt; byte array │
│                                                               │
│  RISKS                                                        │
│  • Downsampling reduces image quality &lt;span class="k"&gt;for &lt;/span&gt;server-side use     │
│  • Need to &lt;span class="nb"&gt;test &lt;/span&gt;across API levels 26-34                       │
└──────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;--output file&lt;/code&gt;, it saves the analysis as markdown with a ready-to-paste prompt section at the bottom — hand it directly to a coding agent to implement the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch Out For
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Config nesting.&lt;/strong&gt; The &lt;code&gt;code_analysis&lt;/code&gt; block goes at the &lt;strong&gt;top level&lt;/strong&gt; of your config YAML, not nested under an app entry. I made this mistake myself — the parser reads &lt;code&gt;code_analysis&lt;/code&gt; from the root, so nesting it under &lt;code&gt;apps[]&lt;/code&gt; means it silently gets ignored and falls back to defaults.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;prepare_tools&lt;/code&gt; &lt;strong&gt;is called every turn.&lt;/strong&gt; It receives the full message history, so counting tool calls is O(n) over all messages. For 25 turns this is negligible, but if you remove the request limit, it could get slow on very long conversations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;External agent stdout formats vary.&lt;/strong&gt; Claude Code with &lt;code&gt;--output-format json&lt;/code&gt; wraps its response in a JSON object; Codex prints plain text. The &lt;code&gt;ExternalAgentBackend._extract_text()&lt;/code&gt; method handles both, but if you add a new backend, check what it actually writes to stdout.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Model name prefix matters.&lt;/strong&gt; PydanticAI wants &lt;code&gt;openai-chat:gpt-4o-mini&lt;/code&gt; (not just &lt;code&gt;gpt-4o-mini&lt;/code&gt;). Without the prefix, you get a deprecation warning and potentially the wrong provider.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'm Thinking About Next
&lt;/h2&gt;

&lt;p&gt;This pipeline keeps evolving. Some things on the radar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Per-app backend config.&lt;/strong&gt; Right now &lt;code&gt;code_analysis&lt;/code&gt; is global. If you have multiple apps, you might want Grok for one and builtin for another. The config structure needs to support this.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Batch analysis.&lt;/strong&gt; &lt;code&gt;apppulse analyze --category bug --severity critical&lt;/code&gt; to analyze all critical bugs in one run instead of one-by-one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Analysis history.&lt;/strong&gt; Tracking which analyses led to successful fixes — creating a feedback loop that improves future analysis quality.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Auto-analyze in pipeline.&lt;/strong&gt; Option to run analysis on all critical bugs during &lt;code&gt;apppulse run&lt;/code&gt;, so the morning digest includes code-level briefs, not just classification.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with classification, then add analysis.&lt;/strong&gt; Getting reviews categorized (bug vs. feature request vs. praise) is the foundation. Code analysis is the layer on top — it's valuable but depends on good classification first.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Give the agent a map before tools.&lt;/strong&gt; The repo map cut tool calls by 60%. An LLM exploring a codebase blind will waste most of its budget on orientation. Front-load structure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Funnel, don't limit.&lt;/strong&gt; Progressive tool gating works better than a hard request limit. The agent naturally transitions from exploration to deep-dive to output, rather than being cut off mid-investigation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Make the backend a strategy, not a hard dependency.&lt;/strong&gt; The two-stage pipeline (explore → structure) decouples the exploration engine from the output format. Today it's Grok and Claude Code; tomorrow it could be any agent that can read files and write markdown.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Save raw artifacts.&lt;/strong&gt; The raw markdown from external agents is invaluable for debugging bad analyses. If the structured output looks wrong, the raw artifact tells you whether the problem was Stage 1 (bad exploration) or Stage 2 (bad extraction).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;AppPulse is a personal project I keep iterating on. The code analysis pipeline is the part I'm most excited about — it's the difference between "you have 3 critical bugs" and "here's exactly where they are and what to change." If you're building something similar, the pluggable backend pattern and two-stage pipeline might save you some architecture headaches.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
      <category>python</category>
    </item>
    <item>
      <title>I built a very cheap App Review analysis pipeline that also finds Bugs in my app code</title>
      <dc:creator>Ashish Mishra</dc:creator>
      <pubDate>Mon, 20 Jul 2026 19:18:20 +0000</pubDate>
      <link>https://dev.to/ashish_mishra_8491c3b9912/i-built-a-very-cheap-app-review-analysis-pipeline-that-also-finds-bugs-in-my-app-code-48ng</link>
      <guid>https://dev.to/ashish_mishra_8491c3b9912/i-built-a-very-cheap-app-review-analysis-pipeline-that-also-finds-bugs-in-my-app-code-48ng</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I got tired of reading app reviews manually every morning, so I built a command-line tool that pulls reviews from Google Play and the App Store, classifies them (bug, feature request, crash, praise), and sends me a daily summary. It costs about $0.50/month. In version 2, I added the ability to point it at my source code and get an analysis: which files are causing the bug, what to change, and how complex the fix is.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you need to checkout code for your own setup : &lt;a href="https://github.com/Mr-Ashish/AppPulse" rel="noopener noreferrer"&gt;https://github.com/Mr-Ashish/AppPulse&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;I have a few apps on Google Play. Every morning, I'd open the Play Console, scroll through new reviews, and try to figure out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Is this a bug or a complaint?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Is this the same issue three other people mentioned yesterday?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Does this match any of the crashes I'm seeing in Firebase?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;About 15 minutes of work. But it's &lt;em&gt;unfocused&lt;/em&gt; work. Scanning text, making mental classifications, switching between tabs. On days with 20+ reviews, I'd either rush through them or skip the whole exercise.&lt;/p&gt;

&lt;p&gt;I wanted a morning briefing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"You got 12 new reviews. 3 are bugs (1 critical, photo upload crashes on large images). 2 are feature requests. 7 are praise. The critical bug matches a Firebase crash that affected 312 users this week."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I built one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 1: Classify and Digest
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Flow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Google Play ──┐
               → Pull new reviews→Classify review → Save to database
App Store ───┘         │             (using AI)              
                         │                                           
Firebase/Sentry ──────
---------------------------------------------------------------    
     ↓

Daily Digest                                                                             ┌──────────────────┐
│ 3 bugs (1 critical)
│ 2 feature requests
│ 7 praise
│ Top crash: OOM in
│  ImageUploadService
└──────────────────┘
      ↓         ↓
     Terminal   Slack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Classification
&lt;/h3&gt;

&lt;p&gt;Each review goes through an LLM (a language model; I use OpenAI's &lt;code&gt;gpt-4o-mini&lt;/code&gt;, their cheapest option). The LLM reads the review text and categorizes it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Example review&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;bug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Something broken&lt;/td&gt;
&lt;td&gt;"Upload button does nothing after I pick a photo"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;crash&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;App closes or freezes&lt;/td&gt;
&lt;td&gt;"App crashes every time I open settings"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;feature_request&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;User wants something new&lt;/td&gt;
&lt;td&gt;"Please add dark mode"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Slow, laggy, battery drain&lt;/td&gt;
&lt;td&gt;"App drains 20% battery in an hour"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;praise&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Positive feedback&lt;/td&gt;
&lt;td&gt;"Best app I've used for this!"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;complaint&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;General dissatisfaction&lt;/td&gt;
&lt;td&gt;"Used to be good, now it's terrible"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Along with the category, the LLM extracts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Severity&lt;/strong&gt; (critical, major, or minor)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keywords&lt;/strong&gt; (the specific feature or area mentioned, e.g., "photo upload", "settings screen")&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Functional area&lt;/strong&gt; (which part of the app is affected)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reviews go to the LLM in batches of 10, so one API call classifies 10 reviews at once. That's what keeps the cost low.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost
&lt;/h3&gt;

&lt;p&gt;For an app with about 500 reviews per week:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Monthly Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google Play API&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App Store Connect API&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firebase/Sentry API&lt;/td&gt;
&lt;td&gt;Free (free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI classification (~2,000 reviews/month)&lt;/td&gt;
&lt;td&gt;~$0.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage (SQLite, local file)&lt;/td&gt;
&lt;td&gt;Free&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;~$0.50/month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you use Ollama (runs AI models on your own computer), the cost drops to $0.00/month.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Tech Stack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python&lt;/strong&gt; , the whole tool is a Python command-line app&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Click&lt;/strong&gt; , a library for building command-line interfaces (handles &lt;code&gt;apppulse run&lt;/code&gt;, &lt;code&gt;apppulse reviews&lt;/code&gt;, etc.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SQLAlchemy + SQLite&lt;/strong&gt; , stores everything in a single local database file at &lt;code&gt;~/.apppulse/data.db&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rich&lt;/strong&gt; , makes the terminal output clean with tables, colors, and panels&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;OpenAI / Anthropic / Ollama&lt;/strong&gt; , LLM providers for classification (you pick one during setup)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No servers. No cloud infrastructure. No Docker. Run &lt;code&gt;pip install&lt;/code&gt; and a 5-minute setup wizard.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Typical Morning
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;apppulse run
&lt;span class="c"&gt;# Pulling reviews for MyApp Android... 12 new reviews&lt;/span&gt;
&lt;span class="c"&gt;# Classifying... done (0.8s)&lt;/span&gt;
&lt;span class="c"&gt;# Pulling crash data from Firebase... 3 active crashes&lt;/span&gt;
&lt;span class="c"&gt;# Correlating reviews with crashes... 1 match found&lt;/span&gt;

&lt;span class="c"&gt;# ── Daily Digest ───────────────────────────────────────&lt;/span&gt;
&lt;span class="c"&gt;#  12 new reviews: 3 bugs (1 critical), 2 feature requests, 7 praise&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;#  🔴 Critical: Photo upload crashes for &amp;gt;10MB images&lt;/span&gt;
&lt;span class="c"&gt;#     → Matches Firebase crash: OutOfMemoryError (312 users affected)&lt;/span&gt;
&lt;span class="c"&gt;#     → 3 reviews mention this issue&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;#  🟡 Major: Search doesn't update after applying filters&lt;/span&gt;
&lt;span class="c"&gt;#     → 2 reviews&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;#  💡 Feature requests: Dark mode (2 requests)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five seconds. I know what matters. No tab-switching, no scrolling through the Play Console.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 2: From Classification to Code Analysis
&lt;/h2&gt;

&lt;p&gt;Version 1 told me &lt;em&gt;what&lt;/em&gt; users were experiencing. It didn't tell me &lt;em&gt;where in my code&lt;/em&gt; the problem lived or &lt;em&gt;what to change&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;For the critical bug above, "Photo upload crashes for &amp;gt;10MB images," I'd still need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Open the project&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Search for upload-related code&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Look at the Firebase stacktrace&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Connect the dots&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Figure out a fix&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So I built a second stage: &lt;strong&gt;code analysis&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Mechanism
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    apppulse analyze 42
                         │
                         ▼
        ┌────────────────────────────────┐
        │    Load the review from the    │
        │    database + crash data       │
        └───────────────┬────────────────┘
                        │
                        ▼
        ┌────────────────────────────────┐
        │    AI agent explores your      │
        │    source code with read-only  │
        │    tools (search, read files,  │
        │    find function definitions)  │
        └───────────────┬────────────────┘
                        │
                        ▼
        ┌────────────────────────────────┐
        │    Produces an Analysis Brief  │
        │    • Root cause                │
        │    • Affected files + lines    │
        │    • Proposed changes          │
        │    • Complexity estimate       │
        │    • Testing notes             │
        └────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You point AppPulse at your app's source code folder during setup. Running &lt;code&gt;apppulse analyze 42&lt;/code&gt; (where 42 is a review ID) triggers the AI agent to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Read the review and any matched crash data&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Get a structural overview of your project (a map of every file and its key classes/functions)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Search the code for relevant keywords, error messages, class names, the feature area mentioned in the review&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Read the specific files that look promising&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Produce a structured analysis with file paths, line numbers, and proposed changes&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Output
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────────────────────┐
│  CODE ANALYSIS — Review #42                                   │
│  Photo upload crashes for &amp;gt;10MB images                        │
│  Type: bug │ Complexity: medium                               │
├──────────────────────────────────────────────────────────────┤
│                                                               │
│  ROOT CAUSE                                                   │
│  ImageUploadService.compress() loads the full image into      │
│  memory. Images &amp;gt;10MB exceed the memory limit on phones       │
│  with ≤4GB RAM, causing the app to crash.                     │
│                                                               │
│  AFFECTED FILES                                               │
│  ImageUploadService.java (lines 45-89) — compress() method    │
│  ImageUtils.java (lines 12-35) — no file size check           │
│                                                               │
│  PROPOSED CHANGES                                             │
│  1. Resize large images before loading them into memory       │
│  2. Add a file size check before compression starts           │
│  3. Use streaming compression instead of loading everything   │
│     into memory at once                                       │
│                                                               │
│  TESTING NOTES                                                │
│  Test with 1MB, 10MB, and 50MB images on a low-memory device  │
└──────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of a category label, I get the file, the line, what to change, and how to test it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pick Your Analysis Engine
&lt;/h3&gt;

&lt;p&gt;The analysis engine is pluggable. I built it with four options:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Good for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Built-in&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A lightweight AI agent that runs inside AppPulse, using PydanticAI (a Python framework for building AI agents with structured output)&lt;/td&gt;
&lt;td&gt;Quick analyses, small codebases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Grok Build&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;xAI's coding assistant in headless mode&lt;/td&gt;
&lt;td&gt;Fast, targeted exploration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Anthropic's coding assistant in headless mode&lt;/td&gt;
&lt;td&gt;Deep multi-file investigations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenAI Codex&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OpenAI's coding assistant&lt;/td&gt;
&lt;td&gt;Sandboxed, autonomous mode&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The external backends (Grok, Claude Code, Codex) use a &lt;strong&gt;two-stage approach&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The coding assistant explores the codebase and writes its findings as a markdown document&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A cheap AI call ($0.001) extracts that document into the structured analysis format&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Consistent output regardless of which AI did the exploration. Adding a new backend takes about 20 lines of code.&lt;/p&gt;

&lt;p&gt;Switching is one line in the config file:&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;code_analysis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&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;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grok"&lt;/span&gt;    &lt;span class="c1"&gt;# or "builtin", "claude_code", "codex"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h3&gt;
  
  
  1. Classify before you analyze
&lt;/h3&gt;

&lt;p&gt;Categorization (bug vs. feature request vs. praise) determines everything downstream. Code analysis needs good classification as input, or it wastes time investigating a feature request as if it were a bug.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Cheap AI models handle most of this
&lt;/h3&gt;

&lt;p&gt;The classification pipeline runs on &lt;code&gt;gpt-4o-mini&lt;/code&gt;, OpenAI's cheapest model. For categorizing app reviews, which are 1-3 sentences, it's accurate enough. Expensive models are overkill.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Give the AI a map of your codebase
&lt;/h3&gt;

&lt;p&gt;The biggest analysis quality improvement came from generating a project map upfront, a compact overview of every file and its key components. Without the map, the AI spent most of its budget figuring out where things lived. With it, the AI goes to the relevant files on its first move. A GPS for the codebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Save raw output
&lt;/h3&gt;

&lt;p&gt;Every analysis saves the AI's raw output as a markdown file. If the structured summary looks off, I can check what the AI found and pinpoint whether the problem was in the exploration or the extraction.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. No infrastructure required
&lt;/h3&gt;

&lt;p&gt;The whole tool runs as a single command-line app. The database is a SQLite file on disk. The cache is a text file. No server, no Docker container, no cloud deployment. I run &lt;code&gt;apppulse run&lt;/code&gt; by hand or on a daily schedule.&lt;/p&gt;




&lt;h2&gt;
  
  
  Up Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Batch analysis&lt;/strong&gt; , analyze all critical bugs at once instead of one at a time&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Per-app configuration&lt;/strong&gt; , different analysis settings for different apps&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Auto-analysis&lt;/strong&gt; , run code analysis on critical bugs during the daily pipeline, so the morning digest includes root causes alongside classifications&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tracking outcomes&lt;/strong&gt; , connect analyses to fixes, so the tool can learn which investigations led to successful resolutions&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;The classification half took a weekend. Code analysis took another. The pipeline costs less per month than a cup of coffee, and it replaced 15 minutes of unfocused tab-switching with a 5-second terminal readout.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>mobile</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Your Team Can Build Dashboards — But Can You Share Them Safely?</title>
      <dc:creator>Ashish Mishra</dc:creator>
      <pubDate>Mon, 20 Jul 2026 19:17:14 +0000</pubDate>
      <link>https://dev.to/ashish_mishra_8491c3b9912/your-team-can-build-dashboards-but-can-you-share-them-safely-45jf</link>
      <guid>https://dev.to/ashish_mishra_8491c3b9912/your-team-can-build-dashboards-but-can-you-share-them-safely-45jf</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Non-tech teammates can already build useful internal dashboards — often as a single HTML file with AI help. The real problems are sharing, secrets, and control. We solved that with a small publish app, two repositories, and a clear rule: code goes through review; passwords never go into Git.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;More teams can create internal tools than ever before. Someone in support, ops, or finance asks an AI assistant for “a dashboard that shows X,” downloads one HTML file, and it works on their laptop.&lt;/p&gt;

&lt;p&gt;Then reality hits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How do we share it?&lt;/strong&gt; Emailing files and “open this on your machine” does not scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where do API keys go?&lt;/strong&gt; Those keys unlock company systems. Pasting them into a file that gets copied around is a security incident waiting to happen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who checked it?&lt;/strong&gt; One edit can break the dashboard — or worse, leak data. Someone should review changes before the whole company uses them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who can open the link?&lt;/strong&gt; It should be company people only, not anyone on the internet who finds the URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the issue is not “can we build dashboards?” It’s &lt;strong&gt;can we publish them like a product&lt;/strong&gt;: safely, repeatedly, without forcing every creator to learn Git.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Were Aiming For
&lt;/h2&gt;

&lt;p&gt;Think of it as &lt;strong&gt;internal websites for ops dashboards&lt;/strong&gt;, not a full developer platform.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Goal&lt;/th&gt;
&lt;th&gt;In plain terms&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Easy publish&lt;/td&gt;
&lt;td&gt;Drag a file into a simple UI — no terminal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Safe secrets&lt;/td&gt;
&lt;td&gt;Keys live in a locked vault (database), not in the file people share&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review before live&lt;/td&gt;
&lt;td&gt;Changes go through a pull request (a proposed change someone can approve)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Company-only access&lt;/td&gt;
&lt;td&gt;Sign in with work Google account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Room to grow&lt;/td&gt;
&lt;td&gt;Same design can later support bots, coding agents, and stricter permissions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Overall Idea
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Creator + AI                Publish app                 Two stores
 ─────────────               ────────────                ──────────
  One HTML file  ──upload──►  Company login               ┌─ Git: redacted HTML (via PR)
                              Save settings               └─ Database: config + secrets
                              Strip secrets
                                      │
                                      ▼
                              After review + merge
                                      │
                                      ▼
                              Shareable company link
                                      │
                                      ▼
                              Server calls APIs with secrets
                              (keys never shown in the page)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;In one sentence:&lt;/strong&gt; people upload a dashboard; the app saves safe settings in a database, sends a cleaned-up copy of the HTML for review in Git, and only after approval serves a company login–protected link — while secrets stay on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Two Repositories? (The Original Thinking)
&lt;/h2&gt;

&lt;p&gt;This was the biggest structural choice. Early on it was tempting to keep &lt;em&gt;everything&lt;/em&gt; in one place: the publish website &lt;em&gt;and&lt;/em&gt; all the dashboard HTML together.&lt;/p&gt;

&lt;p&gt;Here’s why we didn’t.&lt;/p&gt;

&lt;h3&gt;
  
  
  What each repo is for
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repository&lt;/th&gt;
&lt;th&gt;What’s inside&lt;/th&gt;
&lt;th&gt;Who cares&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Platform repo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The publish website, login, “serve this dashboard” logic, secret proxy&lt;/td&gt;
&lt;td&gt;Engineers who ship the product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dashboards repo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Only the HTML (and a tiny list of setting &lt;em&gt;names&lt;/em&gt;, not values)&lt;/td&gt;
&lt;td&gt;Reviewers, automated checks, and later AI agents that edit dashboards&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Why split them
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Different jobs, different risk&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Platform code is “how publishing works.” Dashboard HTML is “what the business sees.” Mixing them means a dashboard tweak and a security fix compete in the same history. Splitting them makes reviews clearer: &lt;em&gt;Is this a product change or a dashboard change?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Secrets and HTML must not travel together&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
If HTML and the app live in one pile, it’s too easy for keys to end up next to the files people download and copy. A dedicated dashboards repo has a hard rule: &lt;strong&gt;only cleaned-up HTML&lt;/strong&gt;. Real keys never belong there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Review that non-engineers can still understand&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A pull request on a dashboards-only repo looks like: “here’s the new support board page.” Reviewers aren’t wading through unrelated website code. That matches how the team actually works — ops owns the dashboard content; engineering owns the platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Safer automation later&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
We knew we might want Slack bots or coding agents to open dashboard updates. Giving an automation access to &lt;em&gt;only&lt;/em&gt; the dashboards repo is much safer than giving it keys to the whole platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Cleaner automated checks&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Checks on dashboards can be simple: “is there one HTML file?”, “does this look like a leftover API key?”, “is the file too big?” Those checks don’t need to understand the full website.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tradeoff we accepted
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Two repos&lt;/th&gt;
&lt;th&gt;One repo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Slightly more setup&lt;/td&gt;
&lt;td&gt;Simpler at first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clear ownership and safer automation&lt;/td&gt;
&lt;td&gt;Faster to start, messier later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Platform releases don’t churn when dashboards change&lt;/td&gt;
&lt;td&gt;Every dashboard edit looks like a product change&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Original thinking in short:&lt;/strong&gt; treat dashboards like &lt;em&gt;content&lt;/em&gt; and the publish app like &lt;em&gt;product&lt;/em&gt;. Content gets a content repo. Product gets a product repo. Secrets get neither — they get the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Publishing Works (For Humans)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sign in&lt;/strong&gt; with your work Google account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upload&lt;/strong&gt; one HTML file (title, optional team label).&lt;/li&gt;
&lt;li&gt;The app &lt;strong&gt;reads&lt;/strong&gt; any preview settings used for local testing, &lt;strong&gt;saves&lt;/strong&gt; them for that dashboard, and &lt;strong&gt;strips secrets&lt;/strong&gt; out of the copy that goes to Git.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;opens a pull request&lt;/strong&gt; — a proposed change — on the dashboards repo.&lt;/li&gt;
&lt;li&gt;Someone &lt;strong&gt;reviews and merges&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The owner &lt;strong&gt;marks it live&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Teammates open a &lt;strong&gt;share link&lt;/strong&gt;, sign in, and use the dashboard.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Updates follow the same path: new upload → update the open pull request (or open a new one) → merge → live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security — What Can Go Wrong, and How This Helps
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;th&gt;What it looks like in real life&lt;/th&gt;
&lt;th&gt;How the design responds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Keys in the file&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HTML emailed around with an API key inside&lt;/td&gt;
&lt;td&gt;Keys saved encrypted in the database; Git only gets placeholders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Keys in the browser&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Anyone can “View Source” and steal a key&lt;/td&gt;
&lt;td&gt;Page only gets non-secret config; the &lt;strong&gt;server&lt;/strong&gt; calls external APIs on your behalf&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Open link on the internet&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Company data on a URL with no login&lt;/td&gt;
&lt;td&gt;Company Google sign-in before viewing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Silent go-live&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Broken or unsafe change hits everyone at once&lt;/td&gt;
&lt;td&gt;Pull request + merge before live&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;One mega-repo blast radius&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A bot or leak touches the platform &lt;em&gt;and&lt;/em&gt; every dashboard&lt;/td&gt;
&lt;td&gt;Two repos; automation can be limited to dashboards only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The simple rule we tell creators:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Config (folder IDs, public URLs) can appear on the page. Secrets (API keys, tokens) never do — the server holds them.&lt;/p&gt;

&lt;p&gt;The same HTML can still work on a laptop for preview: it uses a local preview block when it’s not on the platform, and platform-provided config when it is. Creators aren’t maintaining two different apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture at a Glance
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    ┌─────────────────────────────────────┐
                    │           Platform app              │
   Creator ───────► │  Upload · Settings · Google login   │
                    │  Serve link · Secret proxy          │
                    └───────────┬─────────────┬───────────┘
                                │             │
                    save config │             │ redacted HTML
                    + secrets   │             │ as a pull request
                                ▼             ▼
                         ┌──────────┐   ┌──────────────┐
                         │ Database │   │ Dashboards   │
                         │ (locked) │   │ Git repo     │
                         └────┬─────┘   └──────┬───────┘
                              │                │
                              │   after merge  │
                              └───────┬────────┘
                                      ▼
                               Teammate opens
                            share link (+ login)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three ideas to remember:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Git holds the dashboard pages&lt;/strong&gt; (after review).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The database holds settings and secrets&lt;/strong&gt; (encrypted).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The platform app&lt;/strong&gt; is the only door: login, upload, serve, and proxy.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Tradeoffs (Honest Ones)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Upside&lt;/th&gt;
&lt;th&gt;Downside&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pull request before live&lt;/td&gt;
&lt;td&gt;Safer, reviewable history&lt;/td&gt;
&lt;td&gt;Not “instant publish”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets in the database + server proxy&lt;/td&gt;
&lt;td&gt;Keys stay off pages and out of Git&lt;/td&gt;
&lt;td&gt;Slightly more setup for API connections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two repositories&lt;/td&gt;
&lt;td&gt;Clear ownership, safer bots later&lt;/td&gt;
&lt;td&gt;Two places to understand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Company sign-in only&lt;/td&gt;
&lt;td&gt;Fits internal tools&lt;/td&gt;
&lt;td&gt;Not for public / customer-facing sites&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single HTML dashboards first&lt;/td&gt;
&lt;td&gt;Matches how people actually create with AI today&lt;/td&gt;
&lt;td&gt;Multi-file apps come later&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We deliberately deferred finer team permissions, Slack bots, and heavy hosting complexity. The skeleton — two repos, secrets out of Git, company login, review before live — is what has to be right first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Infrastructure (Small Picture)
&lt;/h2&gt;

&lt;p&gt;You don’t need a huge cloud setup for this.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Typical choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Run the publish website&lt;/td&gt;
&lt;td&gt;A small hosted web service (for example on Google Cloud or AWS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database for settings and secrets&lt;/td&gt;
&lt;td&gt;Managed Postgres&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keep the platform’s own production secrets safe&lt;/td&gt;
&lt;td&gt;The cloud provider’s secret store&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Traffic is light for an internal pilot — tens of dashboards, a handful of people using any one at a time. Cost is usually dominated by the database staying on, not by the website. Google Cloud and AWS both work; pick wherever your company already lives.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;The bottleneck isn’t building dashboards — it’s &lt;strong&gt;sharing them without leaking keys or skipping review&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Treat dashboard HTML as &lt;strong&gt;content&lt;/strong&gt; and the publish website as &lt;strong&gt;product&lt;/strong&gt;: that’s why two repositories beat one pile of everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security for humans:&lt;/strong&gt; keys in the vault, pages for viewers, review before live, company login on the link.&lt;/li&gt;
&lt;li&gt;Start simple on infrastructure; invest first in the rules of the road — where HTML lives, where secrets live, who can open the URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your non-tech teams are already creating internal dashboards, you don’t need to stop them — you need a &lt;strong&gt;front door&lt;/strong&gt; that makes the safe path the easy path.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>git</category>
      <category>productivity</category>
      <category>security</category>
    </item>
  </channel>
</rss>
