- Codebase: intrebit backend (Rust/Axum)
- Branch:
feat/ai-scenarios(20 commits, 77 files, +1566/-5900 lines) - Diff range:
master..HEAD - Vectors: correctness, security
- Flags:
--no-tests --no-cache - Date: 2026-03-30
| Metric | Claude CLI (cached) | Claude CLI (fresh) | API Sonnet | API Opus |
|---|---|---|---|---|
| Time | ~5 min (cached: 82ms) | 1 min 47s | 7 min 20s | 8 min |
| Total findings | 10 | 7 | 84 | 44 |
| Critical | 0 | 0 | 1 | 8 |
| High | 1 | 1 | 4 | 2 |
| Medium | 6 | 3 | 40 | 23 |
| Low | 3 | 3 | 39 | 11 |
| Canary detected | Yes | No | Yes | Yes |
| Security findings | 2 | 0 | 22 | 2 |
| Correctness findings | 8 | 7 | 62 | 42 |
| Silent file warnings | 0 | 17 | 8 | 11 |
The cached CLI run (from earlier today) found 10 issues including 2 security findings and detected the canary. The fresh rerun found 7 issues, zero security findings, and missed the canary. Same provider, same engine, same diff — 30% variance in finding count and inconsistent canary detection. This is a key reliability concern: non-deterministic results mean you can't trust a single run.
| Severity | Finding |
|---|---|
| HIGH | Missing .await on on_activity_created — scenario trigger future dropped without polling |
| MEDIUM | No expired_at column for the 'expired' status (schema symmetry gap) |
| MEDIUM | ADD COLUMN IF NOT EXISTS silently skips on partial re-run with wrong type |
| MEDIUM | ListExecutionsQuery.limit has no validation, allows negative/zero (PostgreSQL treats negative LIMIT as unlimited) |
| LOW | spawn_scenario_loop runs even when seed_defaults fails |
| LOW | Cursor field is unparsed String, invalid cursors cause runtime errors |
| LOW | UpdateAiConfig accepts arbitrary JSON with no schema validation |
| Provider | Input tokens (est.) | Output tokens (est.) | Est. cost |
|---|---|---|---|
| Claude CLI | N/A (subscription) | N/A | Included in Pro/Max plan |
| API Sonnet | ~150k | ~30k | $1.11 |
| API Opus | ~150k | ~20k | $1.67 |
The brunt config used claude-sonnet-4-6-20250514 as the model ID. This works with claude-cli (Claude Code resolves aliases) but is not a valid API model ID. The Anthropic API returns 404 for this model.
Impact: Every API call returned a 404 error. Promise.allSettled in the engine caught these as rejected and silently dropped them. The result: zero findings, zero errors shown to the user, and a misleading "no issues found" output.
Fix needed: Validate model ID on startup or surface per-file errors instead of silently swallowing them.
The engine fires all 75 file analyses simultaneously via Promise.allSettled(files.map(...)). For the API provider, this means 75+ concurrent HTTP requests per vector (150+ total).
Impact: Mass rate limiting (429s) from the Anthropic API. Same silent failure as Bug 1 — all rejected promises get dropped.
Fix applied: Added runWithConcurrency() helper to engine.ts, limiting API providers to 5 concurrent calls. Claude CLI is unlimited (each subprocess manages its own rate).
Default was 4096 tokens. For large diffs producing many findings, the response would be truncated mid-JSON, causing parse failure and zero findings.
Fix applied: Bumped to 16384.
The anthropic provider threw immediately on any non-200 response. Rate-limited requests were not retried.
Fix applied: Added exponential backoff retry (up to 5 attempts) with retry-after header support for 429/529 responses.
All four bugs shared a pattern: the tool silently produced empty results instead of erroring. A user running brunt scan --provider anthropic would see "no issues found" and trust it. This is worse than a crash — it's a false negative that builds false confidence.
Lesson: AI dev tools need loud failure modes. "I couldn't analyze this" is infinitely more useful than "I found nothing" when the analysis didn't actually run.
- Claude CLI (subscription) found 7 findings on fresh run — focused, high-signal, zero false positives. The missing
.awaitbug is a genuine catch. - API Opus found 44 total, 8 critical — but many criticals were about removed module declarations that are actually cleaned up in the same PR. Higher confidence per-finding but still some isolation blindness.
- API Sonnet found 84 total — noisiest by far. Many concerns about deleted code that no longer exists in the codebase.
Higher finding count does not mean better review. A noisy reviewer that cries wolf on 84 issues trains developers to ignore findings. A precise reviewer that surfaces 7 real concerns gets attention.
The same provider (Claude CLI) produced different results on two runs: 10 findings (cached) vs 7 findings (fresh). The canary was detected in one run but not the other. This means a single brunt run is not reliable as a pass/fail gate — you'd need multiple runs or a consensus mechanism to build confidence.
Brunt plants a synthetic bug in the diff and checks if the model detects it. This is a clever reliability signal — but it's not reliable itself:
- Claude CLI detected it on one run, missed it on another
- API Sonnet and Opus both detected it
The canary is a useful signal but not a guarantee. A model that detects the canary can still miss real bugs, and a model that misses the canary might still find real issues (as the fresh CLI run proved — 7 real findings despite missing the canary).
Brunt analyzes each file independently for parallelism. This means cross-file issues (like "you removed this table from cascade delete but it still exists") are only caught if the model infers them from a single file's diff. This is why the API runs (which succeeded on all 75 files) found more cross-file issues than the CLI run.
Trade-off: per-file analysis scales better and avoids context window limits, but misses systemic issues. A hybrid approach (per-file scan + one cross-file summary pass) could get the best of both.
Claude CLI (subscription) and the Anthropic API can run the same underlying model. Key differences for tool builders:
| Claude CLI | Anthropic API | |
|---|---|---|
| Rate limiting | Handled internally | You build it yourself |
| Retries | Built in | You build it yourself |
| Model resolution | Aliases work (e.g., dated versions) | Exact model ID required |
| Default model | User's subscription default (often Opus) | Must specify explicitly |
| Cost | Included in subscription | Pay per token |
| Speed | Slower (subprocess per call) | Faster (HTTP requests) |
| Concurrency | Limited by CLI internals | You control it |
The CLI is simpler to integrate but gives you less control. The API is more work but more predictable once you handle the infrastructure correctly.
Real issues (caught across runs):
- Missing
.awaiton async trigger call — future silently dropped (CLI fresh) - CSV formula injection in data export (API Sonnet/cached CLI)
- TOCTOU race condition in scenario trigger — duplicate executions (API Opus/Sonnet)
- No
expired_atcolumn for expired status (CLI fresh) ListExecutionsQuery.limitallows negative values — PostgreSQL treats as unlimited (CLI fresh/API Sonnet)- Cursor pagination on non-unique timestamp silently skips records (cached CLI/API Sonnet)
UpdateAiConfigaccepts arbitrary JSON with no schema validation (CLI fresh/API Sonnet)- Shutdown signal missed during initial sleep in background jobs (cached CLI)
False positives (common patterns):
- "Removed module declaration breaks dependents" — true in isolation, but dependents were also removed in the same PR (API Opus flagged 8 of these as critical)
- Flagging bugs in deleted code that no longer exists in the codebase (API Sonnet)
- Concerns about parameter binding in code that was entirely removed (API Sonnet)
A subtle but important point: the CLI runs each file as a claude -p subprocess. With 75 files x 2 vectors = 150 subprocesses, the CLI may have hit internal rate limits or timeouts, silently dropping some analyses. The API (after our fixes) ran all 150 calls with controlled concurrency and retry logic, ensuring every file was actually analyzed.
More findings isn't always noise — sometimes it means more files were actually reviewed.
Before the next round of benchmarks:
- Clear all caches:
rm -rf .brunt-cache/ - Use correct model IDs:
claude-sonnet-4-6,claude-opus-4-6 - Add
--format jsonruns for programmatic comparison - Track actual token usage from API responses
- Consider running CLI 3x to measure variance