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