Purpose: Presentation prep for a 90-member senior engineering team. This document covers everything about how the project works — architecture, LLM design decisions, internal workings, and anticipated Q&A.
Juspay maintains two payment SDKs for the same product (Hyperswitch):
| SDK | Language | Platform | Repo |
|---|---|---|---|
| hyperswitch-web | ReScript + React | Web browsers | juspay/hyperswitch-web |
| hyperswitch-client-core | ReScript + React Native | iOS & Android | juspay/hyperswitch-client-core |
Both repos were developed independently. Features added to one SDK sometimes never make it to the other. Finding these gaps manually means reading thousands of lines of ReScript across both repos.
This dashboard automates that — it detects feature gaps, verifies them with AI, generates patches to fill them, and provides 5 additional AI "skill" agents for day-to-day SDK development work.
┌──────────────────────────────────────────────────────────────────┐
│ Browser (React + Vite) │
│ Tab-based UI: Gap Analysis │ Patch │ Add Prop │ Translator │
│ Test Writer │ PR Review │ Docs │ History │
│ Vite Dev Server (:5173) │
│ Proxies /api → :5174 │
└────────────────────────────┬─────────────────────────────────────┘
│ HTTP (REST + NDJSON streaming)
▼
┌──────────────────────────────────────────────────────────────────┐
│ Node.js + Express Server (:5174) │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
│ │ 11 Routes │ │ 7 Skills │ │ Analyzer Pipeline │ │
│ │ (Express) │ │ (AI agents) │ │ (Extract→Filter→ │ │
│ │ │ │ │ │ Normalize→Derive) │ │
│ └──────┬──────┘ └──────┬───────┘ └──────────┬─────────────┘ │
│ │ │ │ │
│ ┌──────▼──────────────────────────────────────────────────────┐ │
│ │ LLM Layer (llm.ts) │ │
│ │ Spawns `claude -p` subprocess per LLM call │ │
│ │ Uses Claude CLI login (Max plan) — NO API key │ │
│ │ Two functions: ask() (blocking) + askStream() (NDJSON) │ │
│ │ Tracks all child processes for cancel support │ │
│ └──────┬──────────────────────────────────────────────────────┘ │
│ │ subprocess │
│ ┌──────▼──────┐ ┌──────────────┐ ┌────────────────────────┐ │
│ │ SQLite DB │ │ SHA Cache │ │ Git Workspace │ │
│ │ data/app.db│ │ data/cache/ │ │ workspace/ │ │
│ └─────────────┘ └──────────────┘ └────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
| Layer | Technology | Why |
|---|---|---|
| Frontend | React + Vite + Tailwind CSS | Fast dev, HMR, good DX |
| Backend | Node.js + Express + TypeScript | Simple, typed, subprocess-friendly |
| Database | SQLite (better-sqlite3) | Zero config, local file, synchronous reads |
| Git ops | simple-git | Clone, pull, branch, diff — all from JS |
| AI | Claude CLI (claude -p) |
No API key needed, uses Max plan subscription |
| Caching | SHA-keyed JSON files on disk | Same commits = zero AI calls |
Important
This is the core innovation. The project does NOT use the Anthropic API SDK. It shells out to the Claude CLI (claude -p) as a subprocess.
❌ Traditional approach:
npm install @anthropic-ai/sdk
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })
→ Requires API key, per-token billing, credential management
✅ This project's approach:
spawn("claude", ["-p", "--model", "opus", "--output-format", "text"])
→ Uses the logged-in Claude Max plan session
→ No API key, no environment variable, no credentials
→ Bills against the existing subscription, not API credits
The team already has a Claude Max plan subscription. Using the CLI means:
- Zero credential management on a shared machine
- No
ANTHROPIC_API_KEYanywhere - No per-token billing — flat subscription cost
- The
claudeCLI handles authentication via~/.claudesession
// Spawns: claude -p --model <model> --output-format text --no-session-persistence
// Optionally: --allowed-tools "Read Grep Glob Edit Write Bash"
// --permission-mode bypassPermissions
// --append-system-prompt "<system>"
export function ask(prompt: string, opts: AskOptions): Promise<string>How it works internally:
- Builds CLI args from options (model, tools, system prompt, cwd)
spawn("claude", args)— creates a child process- Pipes the prompt into
stdin, closes stdin - Collects
stdoutchunks into a string - On process exit with code 0 → resolves with the text
- On timeout →
SIGKILLthe process, reject - Tracks every child in
activeChildrenset for cancel support
// Spawns: claude -p --model <model> --output-format stream-json --verbose
export function askStream(
prompt: string,
opts: AskOptions,
onChunk: (chunk: StreamChunk) => void
): Promise<void>How it works internally:
- Same subprocess spawn, but with
--output-format stream-json --verbose stdoutemits NDJSON lines (one JSON object per line)- Each line is parsed and mapped to our own
StreamChunktype:type: "text"— assistant text deltatype: "tool_use"— agent is calling Read/Grep/Edit/etc.type: "tool_result"— result of a tool calltype: "done"— stream complete
- The
onChunkcallback is invoked for each chunk, which the Express route forwards as NDJSON to the browser
export async function askJson<T>(prompt: string, opts): Promise<T>Calls ask(), then extracts JSON using two strategies:
- Fenced block extraction: looks for
```json ... ``` - Balanced-brace extraction: walks from the first
{or[to its matching closer, tracking quote escaping and nesting depth
This dual strategy handles the common case where the model wraps JSON in markdown fences or adds prose before/after the JSON.
Tip
This is a key design decision worth explaining in the demo. Different tasks get different models based on cost/capability tradeoffs.
| Tier | Model | Tools Given | Purpose | When Used |
|---|---|---|---|---|
| Cheap bulk | Sonnet | None | Text analysis, pattern matching | Extract features, normalize names, convention review, documentation |
| Expensive precision | Opus | Read, Grep, Glob | Verify claims by searching code | Validate gaps, back-translate checks |
| Expensive creative | Opus | Read, Grep, Glob, Edit, Write, Bash | Write code, generate patches | Patch generation, add prop, write tests |
Why two models?
- Sonnet is ~5x cheaper and ~3x faster — perfect for bulk extraction where you don't need deep reasoning
- Opus is the most capable model — needed only when the agent must actually search code or write code
- Using Opus for everything would cost 10x more tokens for the same job
When allowedTools is specified, the CLI launches with:
--allowed-tools "Read Grep Glob Edit Write Bash"
--permission-mode bypassPermissions
The cwd option sets the working directory so relative paths resolve to the target repo.
| Tool | What It Does | Which Agents Use It |
|---|---|---|
Read |
Read a file's contents | Verify, Patch, Props, Tests, Review |
Grep |
Search file contents by pattern | Verify, Patch, Props, Tests, Review |
Glob |
Find files by name pattern | Verify, Patch, Props, Tests |
Edit |
Modify existing files | Patch, Props, Tests |
Write |
Create new files | Patch, Props, Tests |
Bash |
Run shell commands | Patch, Props, Tests (for npm run re:build) |
Every spawn() call registers the child process in activeChildren: Set<ChildProcess>. This enables:
- Cancel button:
killAllSubprocesses()sendsSIGKILLto all tracked children - Timeout enforcement: Each call has a
setTimeoutthat kills the process if it exceeds the budget - Clean shutdown: Server shutdown kills any lingering processes
This is the core feature — detecting what's in one SDK but missing in the other.
graph TD
A["1. Sync Repos<br/>(git clone/pull)"] --> B["2. Extract Features<br/>(8 Sonnet calls, parallel)"]
B --> C["3. Deterministic Prefilter<br/>(zero LLM, ~30 lines of code)"]
C --> D["4. Normalize Names<br/>(4 Sonnet calls, parallel)"]
D --> E["5. Derive Gaps<br/>(one side null = gap)"]
E --> F["Insert into SQLite<br/>(verified=0)"]
F --> G["On-demand: Verify Button<br/>(1 Opus call per gap)"]
G --> H["On-demand: Generate Patch<br/>(1 Opus call per gap)"]
const repos = await syncAllRepos();
// Clones or pulls both repos into workspace/
// Returns { web: { sha: "abc123" }, mobile: { sha: "def456" } }Uses simple-git to clone/pull both repos. The SHA of each repo's HEAD is captured and used as the cache key for all downstream stages.
4 categories × 2 repos = 8 parallel calls:
| Category | What It Extracts |
|---|---|
payment_method |
Payment method names (Klarna, SEPA, ACH, etc.) |
config |
Integrator-facing configuration props |
component |
UI components (PaymentSheet, CardForm, etc.) |
backend_api |
Backend API endpoints called by the SDK |
Each extractor:
- Reads relevant source files from disk (not via Claude — the server reads them)
- Passes file contents to Sonnet via
askJson() - Sonnet returns
[{name, file, snippet}]— a list of features found
Caching: Results are cached to data/cache/extract/<repo>/<sha>/<category>.json. Same git commit = zero calls. This is the single most impactful optimization — re-running gap analysis on unchanged repos costs exactly zero tokens.
Tip
This is the critical "aha" moment for the audience. ~60% of noise is removed by 30 lines of filter code — no AI needed.
The filter runs AFTER the cache (so tuning rules never invalidates cached extract data). Two passes:
Pass 3a — Canonical Group Collapse (config category):
appearance_theme ┐
appearance_variables │
color_primary ├── All become ONE entry: "appearance_api"
color_background │
font_family │
shapes_border_radius ┘
Both SDKs have appearance APIs, but web exposes them as a structured object while mobile flattens them to dozens of color_* keys. Without collapsing, every sub-key looks like a separate gap.
5 canonical groups defined: appearance_api, layout_config, wallets_ui_config, billing_fields_config, saved_payment_methods_config
Pass 3b — Noise Denylist:
- Component noise (18 items):
tab_bar,button_element,checkout_view,blik_code_input, etc. — infrastructure primitives, not integrator features - Config noise (12 items):
element_classes,icon_style,placeholder_*,confirm_params_*— low-level styling, not features
Pass 3c — Structural False Positive Rule:
// Mobile SDK loads payment methods dynamically from backend responses.
// Static extraction will NEVER find payment method names in mobile source.
// Every web payment method always looks "missing in mobile" = always false positive.
if (category === "payment_method" && missingIn === "mobile") return true; // DROPImpact measured against real data:
| Stage | Features | Naive Gaps |
|---|---|---|
| Raw extraction | 256 | 203 |
| After prefilter | 143 | 81 |
| Reduction | 44% | 60% |
Each category gets one Sonnet call that receives the web list and mobile list side-by-side.
Purpose: Map naming variants across repos:
card_holder_name(web) ↔cardHolder(mobile)saved_payment_methods↔saved_payment_method(singular vs plural)payment_methods_header_text↔payment_sheet_header_text
Output: [{canonical_name, web: Feature|null, mobile: Feature|null}]
Where web = null → missing in web; mobile = null → missing in mobile.
Fallback: If the LLM normalize call fails, there's a naivePairs() function that does pure string-normalization matching (toLowerCase().replace(/[^a-z0-9]/g, "")).
Caching: Results cached by both SHAs — data/cache/normalize/<category>/<webSHA>_<mobileSHA>.json
Simple loop: for each canonical feature, if one side is null, it's a gap. Gaps are inserted into SQLite with verified=0.
Smart insert: Previously verified gaps (from earlier runs) carry forward their verdict, so you don't re-verify the same gap after a re-run.
Dismissed gaps: False positives confirmed by Opus are stored in a dismissed_gaps table and automatically skipped on future runs — you never waste tokens re-verifying a known false positive.
Trigger: User clicks "Verify" button on a gap row
What happens:
- Server sends Opus into the missing repo with Read/Grep/Glob tools
- Opus actually searches the codebase for the feature under its name and any plausible alias
- Returns one of three verdicts:
confirmed→ feature is genuinely absent → green "verified" badgefalse_positive→ found it under another name → row auto-deletedplatform_specific→ can't exist on that platform → dimmed row
Prompt structure:
"Feature 'click_to_pay' is claimed missing from this repo.
USE your tools to actually look. Search under that name and
any plausible alias. Report: confirmed / false_positive / platform_specific."
Cached by repo SHA — verify once at a given commit, free forever at that commit.
Trigger: User clicks "Generate Patch" on a verified gap
Important
This is the most sophisticated agent. It went through 3 major iterations to get right.
Two-phase prompt design (Iteration 7 breakthrough):
Phase 1 — Source Analysis: Agent is given both sourceDir (where the feature exists) and targetDir (where it's missing). First, it uses Read/Grep/Glob with absolute paths on sourceDir to understand:
- The type declaration
- The config parser
- State threading pattern
- Rendering component
Phase 2 — Implementation: Using what Phase 1 revealed, implement the equivalent pattern in targetDir following the target repo's conventions.
Self-verification loop: After every meaningful edit, the agent runs npm run re:build via the Bash tool. If it fails, it reads all errors, finds the root cause, fixes it, and re-runs. No attempt limit — uses the full time budget.
Post-patch flow:
- Capture git diff (submodule-aware)
- Server-side build verification (safety net)
- Commit on feature branch
- Resolve an immutable commit and run the fail-closed secret scan over
origin/main..commit, workspace.env*values, and PR metadata - Verify
originis the expected canonicaljuspay/*repository - Recheck the branch tip and push only the scanned commit with an exact
--force-with-lease - Open or reuse a PR in that canonical repository
Streaming: The browser shows real-time tool-call chips (Read/Grep in Phase 1, Edit/Write in Phase 2, Bash for builds) via NDJSON streaming.
Trigger: User fills out a prop spec form (name, type, default, behavior, platforms)
What it does: Adds a new integrator-facing configuration prop to BOTH SDKs in one click.
Per-platform agents (up to 4 Opus calls):
| Platform | Prompt Template | Pattern Files Read |
|---|---|---|
web |
buildWebPrompt() |
PaymentType.res, DynamicFieldsUtils.res, DynamicFields.res |
mobile |
buildMobilePrompt() |
SdkTypes.res, NativeSdkPropsKeys.res, DynamicFieldsContext.res |
android_native |
buildAndroidPrompt() |
PaymentSheet.kt, LaunchOptions.kt |
ios_native |
buildIosPrompt() |
PaymentSheetConfiguration.swift |
Key technique: Each prompt includes real pattern files from the target repo (up to 12KB each) so the agent knows exactly how props are wired. The prompt says "FOLLOW THIS PATTERN EXACTLY" — no guessing.
Flow for mobile: All mobile platforms (ReScript + Android Kotlin + iOS Swift) share one git branch. The agents run sequentially on the same repo dir, each editing their own layer. Then one submodule-aware commit captures everything.
Build verification: After all edits, runs npm run re:build and rejects if it fails.
Trigger: User provides a key name, English value, and context
3-phase flow:
Phase 1 — Translate (1 Opus call):
"Translate this UI string into 31 languages. Return a flat JSON object."
→ Returns { "de": "...", "fr": "...", "ja": "...", ... }
Phase 1b — Deterministic Quality Checks (zero tokens):
The validateTranslations() function checks for:
- Empty translations → blocking (won't write to disk)
- English text leaked into non-English locale → warning
- String >250% of English length → warning (UI overflow risk)
- Missing
{placeholder}tokens → warning
Phase 1c — Back-translation spot-check (1 Sonnet call, parallel with Phase 2): Back-translates 5 key languages (de, fr, ja, ar, es) to English and checks for semantic drift.
Phase 2 — Surgical File Insertion (zero LLM, parallel both repos):
// Pure string manipulation — no reformatting
// Exactly +1 line per locale file (new key at end)
// Previous last key gains a comma — unavoidable JSON syntax
// ZERO existing keys touched
insertKeyIntoLocaleFile(filePath, keyName, value)This runs on all 32 locale files in both repos. Total: 64 file writes, zero LLM calls for the writes themselves.
Trigger: User provides a branch/PR URL, repo target, and feature description
Per-repo agents:
| Target | Framework | Pattern Files Read |
|---|---|---|
| Web | Cypress | sdk-initialization-test.cy.ts, utils.ts, commands.ts, cards.ts |
| Mobile | Detox (Jest) | card-validation-e2e.test.ts, DetoxHelpers.ts, Constants.ts, APIUtils.ts |
What makes this effective:
- Fetches the actual PR diff first (supports both branch names and GitHub PR URLs like
https://github.com/juspay/hyperswitch-web/pull/420) - Embeds 4 existing test pattern files in the prompt so the agent matches the repo's exact test style
- Requires coverage of: happy path, validation errors, failure paths (declined, network error), edge cases
- Post-generation validation (zero tokens):
- File exists on disk?
- Contains
describe()block? - Contains
it()ortest()blocks? - Has assertions (
should()for Cypress,expect()for Detox)? - No hardcoded credentials?
- Framework-specific:
cy.commands for Cypress,device./element()for Detox
Trigger: User provides a branch/PR URL and repo target
Important
This agent uses a multi-pass architecture — three focused reviewers instead of one general-purpose reviewer. This is a key LLM architecture decision.
| Pass | Focus | Model | Tools | Why Separate |
|---|---|---|---|---|
| Security | PCI, credential exposure, 3DS integrity, amount mutation | Opus | Read, Grep | Security issues need deep reasoning + code context |
| Logic | Null safety, async races, error propagation, edge states | Opus | Read, Grep | Logic bugs need surrounding file context |
| Convention | Naming, file placement, test coverage, i18n coverage | Sonnet | None | Pattern matching doesn't need deep reasoning |
Why this beats a single prompt: A single prompt asking for security + logic + patterns simultaneously causes the model to "spread thin" across 7 dimensions. The security pass finds PCI issues the combined prompt missed because it wasn't distracted by counting test coverage. Separation of concerns applies to LLM prompts just as much as to code.
After all 3 passes return, four deterministic validators run (zero tokens):
-
filterHallucinatedFilePaths(): Drops findings that cite files not present in the diff. This is the #1 LLM failure mode in review — inventing plausible-looking paths for issues it "knows about" but which aren't in this PR. -
deduplicateIssues(): Merges findings from multiple passes bycategory + first 80 chars of message. Higher severity wins on conflict. -
computeVerdict(): Ignores the model's stated verdict entirely. Models have a strong bias toward approving PRs even when they list blocking issues. This function derives the verdict mechanically:- Any
blockingissue →request_changes - Only
suggestion→comment - Nothing /
nitpickonly →approve
- Any
-
buildReviewSummary(): Templated summary from facts — no extra LLM call, always accurate.
Trigger: Automatically after any successful skill run
Produces two markdown bodies:
- Internal dev notes: What it does / How it was implemented / Configuration / Testing notes
- Official GitBook copy: Publish-ready markdown matching docs.hyperswitch.io voice and structure
The official copy is generated by buildOfficialPrompt() which includes:
- Do/don't style rules derived from the live docs
- A markdown skeleton
- A concrete before/after few-shot modeled on the
showCardBrandproperty
Skip list: tests and review skills don't get official docs (scaffolding, not public API).
Important
This is a key architectural principle: deterministic before LLM. Every LLM output passes through zero-token validators before reaching the user.
LLM Output → filterHallucinatedFilePaths() (review)
→ computeVerdict() (review)
→ deduplicateIssues() (review)
→ parseDiffStats() (review)
→ buildReviewSummary() (review)
→ validateTranslations() (translator)
→ validateGeneratedTests() (test writer)
The principle: the #1 source of agent mistakes is not bad reasoning — it's hallucinated file paths, leaked placeholder values, and empty outputs. Catching these with pure logic means the user never sees a bad result.
| Cache | Key | What's Cached | Effect |
|---|---|---|---|
| Extract cache | {repo}/{sha}/{category} |
Raw extractor output per category per repo | Re-run with same commit = zero extract calls |
| Normalize cache | {category}/{webSHA}_{mobileSHA} |
Normalized feature list per category | Re-run with same commits = zero normalize calls |
| Validate cache | {repo}/{sha}/{gapName} |
Verify verdict per gap | Re-verify at same commit = free |
data/cache/
├── extract/
│ ├── web/
│ │ └── abc123/
│ │ ├── payment_method.json
│ │ ├── config.json
│ │ ├── component.json
│ │ └── backend_api.json
│ └── mobile/
│ └── def456/
│ └── ...
├── normalize/
│ └── config/
│ └── abc123_def456.json
└── validate/
└── mobile/
└── def456/
└── click_to_pay.json
Impact: A demo re-run costs exactly 0 tokens. A run after a single commit change in one repo costs 4 extract calls (only the changed repo) + 4 normalize calls = 8 Sonnet calls instead of 12.
reports -- Gap analysis runs (id, created_at, web_sha, mobile_sha, status)
gaps -- Individual gaps (id, report_id, category, canonical_name,
-- missing_in, present_in, evidence, rationale, severity,
-- platform_specific, verified)
dismissed_gaps -- False positives that survive across SHA changes
patches -- Generated patches (diff_path, branch, summary, pr_url, pr_number)
skill_runs -- History of all skill executions (skill_id, status, input_json, result_json)
reviews -- PR review history (branch, verdict, result_json)
docs -- Auto-generated documentation (content, official_content)A: No API key management needed. The team's Max plan subscription covers everything. On a shared machine, this means zero credential management — no .env files, no secrets rotation, no per-user API key provisioning.
A: Iteration 1 validated all ~240 gaps upfront with Opus. It took 11 minutes and burned massive tokens. Most gaps are never acted on. Lazy verification means you only pay for the gaps you care about. Dropped from 11 minutes to <1 minute.
A: Through 7 iterations, we learned that ~60% of "gaps" were structural noise — appearance sub-keys listed as separate features, generic UI primitives, platform-specific form inputs. A 30-line filter function eliminated them for free. Tuning filter rules takes milliseconds against cached data; tuning LLM prompts costs 1+ minute of API calls per attempt.
A: Focused prompts beat comprehensive prompts. The security pass finds security issues the combined prompt missed because it wasn't distracted by counting test coverage. Separation of concerns applies to LLM prompts just as much as to code.
A: Models have a strong approval bias. In testing, they would say "approve" even when listing blocking issues. Ignoring the model's verdict and computing it from issue severities produces a trustworthy result every time.
A: Without Bash, the agent edited blindly and the server checked the build after the fact. Every failed run wasted a full Opus context window. With Bash, the agent runs npm run re:build itself, reads errors, and iterates — drastically reducing failed patches.
A: Before Phase 1, the agent only saw a single truncated file (up to 8KB) from the source repo. For features spanning 4-6 files (type definitions + parsers + components), this was catastrophically insufficient. Phase 1 forces the agent to explore the source repo in depth before touching the target.
| Stage | Calls | Model | Approx Time |
|---|---|---|---|
| Extract | 8 parallel | Sonnet | ~20s |
| Normalize | 4 parallel | Sonnet | ~15s |
| Total | 12 calls | Sonnet | ~1 min |
| Stage | Calls | Time |
|---|---|---|
| Everything | 0 | Instant |
| Operation | Calls | Model | Time |
|---|---|---|---|
| Verify | 1 | Opus | ~15s |
| Patch | 1 (streaming) | Opus | ~1-2 min |
| Skill | Calls | Models |
|---|---|---|
| Add Prop | 1-4 Opus (per platform) + 1-2 Sonnet (docs) | Opus + Sonnet |
| Translator | 1 Opus (translate) + 1 Sonnet (back-translate) + 1-2 Sonnet (docs) | Opus + Sonnet |
| Test Writer | 1 Opus per repo + 0 Sonnet (deterministic validation) | Opus |
| PR Reviewer | 2 Opus (security + logic) + 1 Sonnet (convention) | Opus + Sonnet |
| Documentation | 2 Sonnet (internal + official) | Sonnet |
| # | Date | What Changed | Result |
|---|---|---|---|
| 1 | Apr 9 | Bulk Opus validation on every run | ❌ 11 min, too expensive |
| 2 | Apr 9 | Lazy per-gap validation | |
| 3 | Apr 9 | Deterministic prefilter | ✅ 240 → 81 gaps, zero token cost |
| 4 | Apr 10 | Multi-pass PR review + validators | ✅ Focused passes > combined prompt |
| 5 | Apr 11 | Real PRs + Android preview + bot forks | ✅ End-to-end: gap → PR |
| 6 | Apr 11 | Agent self-verification (Bash) + build fix | ✅ Agent iterates on build errors |
| 7 | Apr 13 | Streaming + two-phase source analysis | ✅ Agent reads source repo first |
| 8 | Aug 11 | Canonical parent publishing replaces fork publishing | ✅ Direct juspay/* branch + PR with origin/lease guards |
Q: Why not use a proper agent framework like LangChain or CrewAI? A: The project uses Claude's built-in tool calling via the CLI, which already provides an agent loop (think → tool call → observe → think). Adding LangChain would add abstraction without benefit since we only use one LLM provider. The subprocess approach is simpler, has fewer dependencies, and gives direct control over process lifecycle (cancel, timeout, streaming).
Q: Why SQLite instead of Postgres/MySQL?
A: This is a local-only tool on a shared machine. SQLite is zero-config, single-file, and better-sqlite3 provides synchronous reads which simplifies the Express handlers. There's no multi-user concurrency to worry about.
Q: How do you handle concurrent requests?
A: The workspace layer uses an async mutex (workspace/mutex.ts) to ensure only one git operation happens at a time per repo. LLM calls are independent subprocesses — they can run concurrently. The database uses SQLite's built-in WAL mode for concurrent reads.
Q: What happens if the Claude CLI isn't installed or the user isn't logged in?
A: The spawn("claude", ...) call will fail with failed to spawn claude: ENOENT. The error propagates to the Express route and is shown in the UI. The README has setup instructions for npm install -g @anthropic-ai/claude-code and claude login.
Q: How do you prevent hallucinations? A: Multiple layers:
- Tool access: The verify agent doesn't guess — it actually searches the codebase with Grep/Read
- Deterministic validators:
filterHallucinatedFilePaths()drops review findings that cite files not in the diff - Deterministic verdict: Never trust the model's stated verdict — compute it from issue severities
- Build verification: The patch agent must produce code that compiles — Bash tool runs the build
- Back-translation: Translator does a reverse translation spot-check
Q: What's the context window usage like? A: For extraction, we read relevant source files ourselves and pass them in the prompt (typically 20-40KB per call). For patch generation, we give the agent tool access so it can read files on demand rather than cramming everything into the prompt. The streaming two-phase approach (Iteration 7) means the agent builds its context incrementally.
Q: Can this work with other LLMs (GPT-4, Gemini)?
A: The LLM layer (llm.ts) is tightly coupled to the Claude CLI. However, the architecture is model-agnostic at the design level — prompts, validators, and caching would work with any sufficiently capable model. You'd need to rewrite llm.ts to call a different API. (Note: Iteration 4 in LEARNINGS.md mentions a brief migration to GitHub Models/GPT-4.1 — that path exists but was superseded.)
Q: How do you handle rate limiting or quota issues? A: The Claude Max plan subscription handles this at the CLI level. The caching layer means most operations don't hit the LLM at all on re-runs. For bulk operations (8 parallel extract calls), we rely on the CLI's built-in queuing.
Q: How accurate is the gap detection? A: After the full pipeline (extract → prefilter → normalize → verify), precision is very high. The remaining false positives are caught by the per-gap Verify button (Opus + tools). The key insight is that most false positives are structural (e.g., mobile loads payment methods dynamically), not lexical — and those are caught by deterministic rules, not AI.
Q: What happens if the patch agent generates bad code? A: Three safety layers:
- The agent runs the build itself via Bash tool — iterates until green
- Server-side build check after the agent finishes (safety net)
- If both pass, the code is committed on a feature branch — never on main
- A PR is opened for human review — the agent doesn't merge anything
Q: How do you handle the mobile SDK's submodules?
A: The mobile repo has 3 git submodules (shared-code, android, ios). The project has submodule-aware git operations throughout:
getDiffWithSubmodules()captures diffs from both parent and submodulescommitWithSubmodules()commits inside submodules first, then parent- parent-only changes can be published directly to the canonical parent repository
- automatic publishing stops with
SUBMODULE_PRS_REQUIREDwhen a submodule changed; the local branch is preserved until separate canonical submodule PRs and merge ordering are handled
Q: Can this be used in CI/CD? A: Currently it's a local dashboard, but the Express API is standard REST — you could call it from CI. The main constraint is the Claude CLI login (interactive OAuth), which would need a persistent session or API key migration.
Q: What's the cost of running this? A: With a Claude Max plan ($100/month), there's no per-token cost. A full gap analysis costs ~12 Sonnet calls (~1 minute). Each patch costs 1 Opus call (~2 minutes). The caching layer means repeated operations are free.
Q: Why not just use Claude Code directly? A: Claude Code is the underlying engine — this dashboard adds:
- A curated pipeline (extract → filter → normalize → verify) that individual Claude Code sessions can't replicate
- Persistent state (SQLite DB, cached results, skill history)
- Deterministic quality gates (validators that catch hallucinations)
- A visual UI for non-CLI users
- Process management (cancel, timeout, streaming)
| Metric | Value |
|---|---|
| Raw features extracted | ~256 across both repos |
| After deterministic filter | ~143 features (44% reduction, zero tokens) |
| After normalization | ~75 candidate gaps |
| After structural FP removal | ~40-50 actionable gaps |
| Cold run time | ~1 minute (12 Sonnet calls) |
| Cached run time | Instant (0 calls) |
| Per-gap verification | ~15 seconds (1 Opus call) |
| Per-gap patch generation | ~1-2 minutes (1 Opus call, streaming) |
| Locale files translated per run | 64 (32 per repo × 2 repos) |
| Supported languages | 32 |
| Review passes (parallel) | 3 (security + logic + convention) |
| Filter code | ~30 lines (kills 60% of noise) |
| Total server source files | ~25 TypeScript files |
| Total lines of server code | ~4,000 lines |
- Two-phase streaming (source analysis → implementation → build loop)
- Real-time tool-call chips in the UI
- The diff viewer showing actual code changes
- PR link at the end
- Web preview via iframe (localhost:9050)
- Mobile preview via adb screencap polling (2fps, zero dependencies)
- Single form → agents for web + mobile + Android + iOS
- Pattern files injected into prompts
- Build verification before commit
- PR opened in both repos
- One key → 32 languages in one Opus call
- Deterministic quality checks (empty, leaked English, overflow, placeholders)
- Back-translation spot-check
- Surgical JSON insertion (exactly +1 line per file)
- PR URL input → fetches actual diff
- Pattern files from existing tests embedded in prompt
- Cypress (web) or Detox (mobile) output
- Post-generation validation (describe blocks, assertions, no hardcoded creds)