<?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>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>
