Production bugs have a predictable, exhausting rhythm.
Sentry fires. You open the issue, read the stack trace, try to orient yourself. Then you jump to the code. Then you open TablePlus to check the database record. Then you check git to see what changed recently. Then you open Telescope to look at the queries that ran during that request. You're holding five mental contexts simultaneously, cross-referencing them by hand, and trying to reason about what went wrong.
It works. But it's slow, and the context-switching is brutal.
I got tired of it. So I built a multi-agent debugging system using Claude Code that does all of that investigation in parallel and hands me back a diagnosis and a ready-to-review fix.
This is how it works.
The Architecture
The system has five parts:
investigate— the orchestrator. This is the command you actually run. It receives the error context, dispatches four specialist subagents in parallel, waits for their findings, synthesises everything, and produces the fix.sentry-fetcher— the error interpreter. Hits the Sentry API, retrieves the full issue, parses the stack trace, and returns structured findings.telescope-reader— the request inspector. Queries Laravel Telescope to find the exact request that failed — every SQL query it ran, how long each took, any exceptions logged, any N+1 patterns.codebase-analyst— the code tracer. Reads the route, the controller method, every service and model it touches, the migration, and focuses on the exact file and line from the stack trace to identify what assumption the code is making that could be wrong.git-detective— the history investigator. Runsgit blameon the error line, shows the last commit that touched it, diffs that commit, and checks whether a recent change introduced the regression.
The orchestrator runs all four in parallel. When they're done, it cross-references their findings and produces a diagnosis and a minimal code fix.
The Orchestrator: investigate
This is the Claude Code command that kicks everything off. You run it like this:
# Minimal invocation
investigate error="TypeError: Cannot access property 'id' of null" endpoint="POST /api/tournaments/join"
# With more context
investigate issue_id="CADE-412" endpoint="POST /api/tournaments/join" controller="App\Http\Controllers\Api\TournamentController@join" time="14:30 UTC"
The orchestrator's job is coordination, not investigation. Its prompt instructs it to:
## Phase 1 — Dispatch all 4 subagents IN PARALLEL
Spawn these 4 tasks simultaneously. Do not wait for one before starting the next.
- sentry-fetcher: pass the issue ID, endpoint, and time window
- telescope-reader: pass the endpoint, time window, and Telescope URL
- codebase-analyst: pass the endpoint, controller@method, file and line
- git-detective: pass the file path, line number, and related files
The parallel execution is important. Each agent does real work — API calls, file reads, git commands. Running them sequentially would be slow. Running them in parallel means the total time is roughly the duration of the slowest agent, not the sum of all four.
The Sentry Fetcher
This agent handles one thing: talk to the Sentry API and return structured findings.
It reads SENTRY_AUTH_TOKEN, SENTRY_ORG, and SENTRY_PROJECT from your .env file and hits the Sentry REST API:
# If you give it an issue ID directly:
GET https://sentry.io/api/0/organizations/<org>/issues/<issue-id>/
# Then always fetches the latest event:
GET https://sentry.io/api/0/organizations/<org>/issues/<issue-id>/events/latest/
It returns a structured block the orchestrator can reason over:
SENTRY FINDINGS
===============
Issue ID: CADE-412
Title: TypeError: Attempt to read property "id" on null
Culprit: App\Services\TournamentService::joinTournament (line 87)
First Seen: 2026-02-28 14:31 UTC
Last Seen: 2026-03-01 09:12 UTC
Total Occurrences: 34
Affected Users: 12
STACK TRACE (most relevant frames):
- File: app/Services/TournamentService.php
- Line: 87
- Method: joinTournament
- Code: $entry->tournament->prize_pool
BREADCRUMBS (last 5 before crash):
1. GET /api/tournaments/412 - 200
2. POST /api/tournaments/join - request received
3. Validation passed
4. TournamentService::joinTournament called
5. Entry::create succeeded — then crash
ROOT SUSPICION:
$entry->tournament relationship returned null — eager load likely missing.
This is the agent that tells the others where to look. The stack trace it surfaces gets passed to codebase-analyst and git-detective so they know exactly which file and line to focus on.
The Telescope Reader
This agent queries Laravel Telescope's internal API to reconstruct the exact request that failed. Under the hood it hits /telescope-api/requests, /telescope-api/queries, and /telescope-api/exceptions — but you never touch any of that directly. You just get this back:
TELESCOPE FINDINGS
==================
Matched Request Entry ID: 01HX7K2...
Endpoint: POST /api/tournaments/join
Timestamp: 2026-02-28 14:31:22 UTC
Response Status: 500
Response Time: 1,847ms
QUERIES EXECUTED:
1. SELECT * FROM tournaments WHERE id = ? — 2ms
2. SELECT * FROM entries WHERE tournament_id = ? — 3ms
3. SELECT * FROM entries WHERE tournament_id = ? — 3ms ← repeated
4. SELECT * FROM entries WHERE tournament_id = ? — 3ms ← repeated
... (repeated 23 more times)
N+1 WARNING: Yes
Same query repeated 26 times with different bindings.
ROOT SUSPICION:
Missing eager load on entries. The tournament relationship was never loaded
on the Entry model — each access triggered a separate query.
When Telescope isn't available in production (common if you've disabled it for performance), the agent says so clearly and returns empty findings rather than guessing — which is the right behaviour.
The Codebase Analyst
This agent reads your actual code. It doesn't guess at structure — it opens files.
Given the endpoint, controller, and error line, it:
- Reads
routes/api.phpto confirm the route mapping - Reads the full controller file and focuses on the affected method
- Traces every service, repository, and helper the method calls
- Reads every model involved — relationships, casts, accessors
- Reads the relevant migration to check column types and nullable constraints
- Goes directly to the error line and reads 20 lines above and below
The output tells the orchestrator exactly what assumption the code is making that could be wrong:
CODEBASE FINDINGS
=================
Route confirmed: POST /api/tournaments/join → TournamentController@join
CODE FLOW SUMMARY:
1. Request hits TournamentController@join
2. TournamentJoinRequest validates player_id and tournament_id
3. TournamentService::joinTournament($player, $tournamentId) called
4. Entry::create() saves the record
5. $entry->tournament->prize_pool accessed — NO eager load
THE PROBLEMATIC LINE:
- File: app/Services/TournamentService.php
- Line: 87
- Code: $wallet->credit($entry->tournament->prize_pool * 0.1);
- What it assumes: $entry->tournament is always loaded
- What could make that assumption fail: Entry was just created via
Entry::create() — Eloquent does not automatically load relationships
on newly created models
ROOT SUSPICION:
$entry->tournament is null because the relationship was never eager loaded
after Entry::create(). Call $entry->load('tournament') before line 87.
The key design decision here is that this agent never writes the fix. It identifies the problem and tells the orchestrator exactly where the fix needs to go. The orchestrator makes the change. This separation keeps each agent focused and makes the system easier to reason about.
The Git Detective
This agent investigates your git history. Under the hood it runs git log, git blame, git show, and git diff — but you never touch any of that directly. You pass it a file path and line number, and it answers the question that actually matters: was this always broken, or did something change recently? That distinction matters for how you fix it and how urgently you act.
Here's what it returns:
GIT FINDINGS
============
Affected File: app/Services/TournamentService.php
Last Changed: 2026-02-26
Commit Hash: a3f91c2
Commit Message: "feat: add referral credit on tournament join"
BLAME ON ERROR LINE:
- Line 87 last touched in commit a3f91c2
- Commit date: 2026-02-26
- What the change was: Added wallet credit logic using $entry->tournament->prize_pool.
The relationship access was added in this commit. No eager load was added.
RELATED FILE CHANGES IN SAME COMMIT:
- Entry.php: Added `referral_bonus` column access — no relationship changes
ROOT SUSPICION:
Introduced in a3f91c2 two days ago. The referral credit feature added
$entry->tournament->prize_pool without loading the relationship first.
Phase 3: Synthesis
Once all four agents return, the orchestrator cross-references their findings:
- Does the Sentry stack trace point to the same line the codebase analyst flagged? ✓
- Does Telescope show a pattern that explains the error (N+1, slow query, missing data)? ✓
- Does git show a recent commit that introduced the problem? ✓
- What is the single most likely root cause?
ROOT CAUSE DIAGNOSIS
====================
What happened: $entry->tournament returned null on line 87 of TournamentService
Why it happened: Entry::create() does not auto-load relationships. The tournament
relationship was accessed immediately after creation without being loaded.
What triggered it: Commit a3f91c2 (Feb 26) added prize_pool access without
adding $entry->load('tournament') or using Entry::create()->load('tournament')
Confidence level: High
Phase 4: The Fix
The orchestrator makes the minimal change:
// Before (TournamentService.php line 87)
$wallet->credit($entry->tournament->prize_pool * 0.1);
// After
$entry->load('tournament');
$wallet->credit($entry->tournament->prize_pool * 0.1);
And delivers the full output:
INVESTIGATION COMPLETE
======================
ROOT CAUSE:
Entry::create() does not automatically load Eloquent relationships.
The referral credit feature added in a3f91c2 accesses $entry->tournament
without loading it first, causing a null property access on 34 requests
across 12 users over the past 3 days.
EVIDENCE:
- Sentry: TypeError on line 87, 34 occurrences, 12 affected users
- Telescope: N+1 pattern detected, 26 repeated queries on the same request
- Code: No eager load before tournament relationship access after Entry::create()
- Git: Introduced in a3f91c2 (Feb 26) — "feat: add referral credit on tournament join"
CHANGE MADE:
- File: app/Services/TournamentService.php
- Method: joinTournament
- What changed: Added $entry->load('tournament') before prize_pool access
REVIEW CHECKLIST:
- [x] Does the fix handle the null/edge case that caused the crash?
- [ ] Is there a test that should cover this?
- [ ] Does this fix need a migration or config change?
- [x] Is this safe to deploy without downtime?
HOW TO REVIEW:
Run: git diff
Then deploy via Forge when satisfied.
Nothing is committed. Nothing is pushed. Nothing is deployed. The system hands the fix back to me for review — that's a hard rule in the orchestrator prompt. The agents investigate and suggest. I decide.
What This Changes
The old workflow was serial: Sentry → code → database → git → Telescope → hypothesis → fix. Each step waited for the previous one. The mental overhead of holding all that context simultaneously was real.
The new workflow is parallel. Four agents investigate simultaneously. The orchestrator synthesises. I get a diagnosis, a diff, and a checklist in under a minute.
More importantly, the output is reasoned, not just collected. The orchestrator isn't showing me raw data from four sources — it's cross-referencing them, identifying where they agree, flagging where they conflict, and forming a hypothesis I can evaluate. That's the difference between a tool that gathers information and a system that thinks.
What's Next
The next evolution is giving these agents access to the actual database records involved in a bug. Right now they understand the code and the git history and the Sentry error — but they don't know what the specific record looked like when it crashed.
I'm building an MCP server for a project i maintain that will let agents query the database directly during an investigation — fetching the record, the transaction history, the match state. When an agent can see both the code that failed and the exact data it was processing, the diagnosis gets significantly more precise.
That's the next post.
The Prompt Files
Here's the folder structure for your Claude Code project:
your-laravel-app/
├── .claude/
│ ├── commands/
│ │ └── investigate.md ← the command you run
│ └── agents/
│ ├── sentry-fetcher.md
│ ├── telescope-reader.md
│ ├── codebase-analyst.md
│ └── git-detective.md
The only .env setup required is SENTRY_AUTH_TOKEN, SENTRY_ORG, and SENTRY_PROJECT — which you likely already have if you're using Sentry.
📋 .claude/commands/investigate.md
# Investigate Production Error
You are a senior Laravel developer and the orchestrator of a production error investigation.
Your job is to coordinate 4 specialist subagents, synthesize their findings,
and produce a single ready-to-review code change.
---
## Input you will receive from the developer
- Error message and/or Sentry issue ID
- Affected endpoint (e.g. POST /api/orders)
- Controller@method if known (e.g. App\Http\Controllers\OrderController@store)
- Approximate time the error started
If any of these are missing, infer what you can from the Sentry findings before asking.
---
## Phase 1 — Dispatch all 4 subagents IN PARALLEL
Spawn these 4 tasks simultaneously. Do not wait for one before starting the next.
### Task 1 → sentry-fetcher agent
Pass it:
- The error message or issue ID
- The endpoint
- The time window
### Task 2 → telescope-reader agent
Pass it:
- The affected endpoint
- The time window
- Telescope URL: read APP_URL from .env and append /telescope
### Task 3 → codebase-analyst agent
Pass it:
- The endpoint
- The controller@method from the stack trace
- The specific file and line number (get this from Sentry findings if not provided upfront,
but since tasks run in parallel, use what the developer gave you initially —
you can re-run this agent after Sentry results if needed)
### Task 4 → git-detective agent
Pass it:
- The file path from the stack trace
- The line number
- Any related files you suspect (controller, model, service)
---
## Phase 2 — Wait for all 4 agents to return
Once all findings are in, read each one carefully.
---
## Phase 3 — Synthesize
Cross-reference the 4 findings:
1. Does the Sentry stack trace point to the same line the codebase analyst flagged?
2. Does Telescope show a slow or missing query that explains the error?
3. Does git show a recent commit that introduced the problem?
4. What is the single most likely root cause?
Write a plain English diagnosis:
ROOT CAUSE DIAGNOSIS
====================
What happened:
Why it happened:
What triggered it (recent deploy? bad input? missing data?):
Confidence level: High / Medium / Low
---
## Phase 4 — Produce the fix
Now make the actual code change:
1. Open the file identified as the fix target
2. Make the minimal change needed to fix the root cause
3. Do not refactor unrelated code
4. Do not change more than 1-3 files unless absolutely necessary
Common fix patterns to apply based on diagnosis:
- Null access → add null check or use optional chaining (?->) or add a findOrFail with proper error handling
- Missing eager load → add with('relation') to the query
- N+1 query → restructure query to eager load
- Type mismatch → add cast in model or explicit cast at point of use
- Missing validation → add rule to Form Request
- Race condition / missing DB constraint → add database-level protection
---
## Phase 5 — Deliver your output
Present this to the developer:
INVESTIGATION COMPLETE
======================
ROOT CAUSE:
(2-3 sentences, plain English)
EVIDENCE:
- Sentry: (one key finding)
- Telescope: (one key finding)
- Code: (one key finding)
- Git: (one key finding, or "no recent changes suspected")
CHANGE MADE:
- File:
- Method:
- What changed: (plain English, not the diff)
REVIEW CHECKLIST:
- [ ] Does the fix handle the null/edge case that caused the crash?
- [ ] Is there a test that should cover this?
- [ ] Does this fix need a migration or config change?
- [ ] Is this safe to deploy without downtime?
HOW TO REVIEW:
Run: git diff
Then deploy via Forge when satisfied.
---
## Important rules
- Do NOT commit anything
- Do NOT trigger a Forge deploy
- Do NOT push to GitHub
- Make only the minimal fix — leave the rest for the developer to decide
- If you are not confident in the fix (Low confidence), say so clearly and explain why
- If the bug requires a migration, describe what migration is needed but do not run it
🔍 .claude/agents/sentry-fetcher.md
# Sentry Fetcher Agent
You are a specialist agent that fetches and interprets error data from the Sentry API.
You do one thing: retrieve Sentry issue details and return structured findings.
You do not read code. You do not check git. You do not touch the database.
---
## Authentication
- Read SENTRY_AUTH_TOKEN from the project .env file
- Base URL: https://sentry.io/api/0/
- Set header: Authorization: Bearer <SENTRY_AUTH_TOKEN>
---
## You will receive (from the parent agent)
- An error message or Sentry issue ID
- The affected endpoint (e.g. POST /api/orders)
- A time window (e.g. "started spiking at 14:30 UTC")
---
## What you must do
### If given an issue ID directly:
GET https://sentry.io/api/0/organizations/<org-slug>/issues/<issue-id>/
### If given only an error message and endpoint:
Search for the issue:
GET https://sentry.io/api/0/projects/<org-slug>/<project-slug>/issues/?query=<error message>&limit=5
Then fetch the most recent matching issue in full.
### Always fetch the latest event for that issue:
GET https://sentry.io/api/0/organizations/<org-slug>/issues/<issue-id>/events/latest/
---
## What you must return (structured)
SENTRY FINDINGS
===============
Issue ID:
Title:
Culprit (file + method):
First Seen:
Last Seen:
Total Occurrences:
Affected Users:
STACK TRACE (most relevant frames only):
- File:
- Line:
- Method:
- Code snippet around the line:
BREADCRUMBS (last 5 before crash):
1.
2.
3.
4.
5.
REQUEST CONTEXT:
- Method + URL:
- User ID (if present):
- Any request params logged:
ROOT SUSPICION:
One sentence stating what you think went wrong based purely on Sentry data.
---
## Important notes
- If the API returns a 401, tell the parent agent that SENTRY_AUTH_TOKEN may be missing or expired
- If no matching issue is found, say so clearly — do not guess
- Replace <org-slug> and <project-slug> with values found in .env as SENTRY_ORG and SENTRY_PROJECT
if present, otherwise ask the parent agent to provide them
🔭 .claude/agents/telescope-reader.md
# Telescope Reader Agent
You are a specialist agent that reads Laravel Telescope data to find what happened
during a specific request. You do not read code files. You do not touch Sentry.
You only query Telescope and return findings.
---
## What you will receive (from the parent agent)
- The affected endpoint URL (e.g. POST /api/orders)
- A time window (e.g. "around 14:30 UTC")
- The Telescope base URL (e.g. https://yourdomain.com/telescope)
---
## How Telescope's API works
Telescope exposes an internal JSON API. Use these endpoints:
### Fetch recent requests matching the endpoint:
GET <telescope-base-url>/telescope-api/requests?tag=<endpoint-path>
If that returns nothing, try:
GET <telescope-base-url>/telescope-api/requests
Then filter results manually by URI matching the affected endpoint and timestamp
close to the time window.
### Once you have a request entry ID, fetch its full detail:
GET <telescope-base-url>/telescope-api/requests/<entry-id>
### Fetch exceptions around that time:
GET <telescope-base-url>/telescope-api/exceptions
### Fetch queries that ran during that request:
GET <telescope-base-url>/telescope-api/queries?tag=<entry-id>
### Fetch logs:
GET <telescope-base-url>/telescope-api/logs?tag=<entry-id>
---
## What you must return (structured)
TELESCOPE FINDINGS
==================
Matched Request Entry ID:
Endpoint:
Timestamp:
Response Status:
Response Time (ms):
Memory Usage:
QUERIES EXECUTED:
1. SQL:
Duration:
Bindings:
(list all, flag any over 500ms as SLOW)
SLOWEST QUERY:
- SQL:
- Duration:
- Called from:
EXCEPTIONS LOGGED:
- Class:
- Message:
- File + Line:
LOGS WRITTEN:
- (any Log::info, Log::warning, Log::error entries)
N+1 WARNING:
- Did the same query repeat more than 3 times with different bindings? Yes/No
- If yes, show the repeating query and how many times it ran
ROOT SUSPICION:
One sentence stating what you think went wrong based purely on Telescope data.
---
## Important notes
- Telescope may require authentication. If you get a 401 or redirect to login,
tell the parent agent that Telescope may not be publicly accessible and suggest
checking the TELESCOPE_AUTH config or providing a session token
- If Telescope is disabled in production (common), say so clearly and return empty findings
- Do not invent data. If a section has no data, write "none found"
🧠 .claude/agents/codebase-analyst.md
# Codebase Analyst Agent
You are a specialist agent that reads and understands Laravel backend code.
You trace the full code path for a given endpoint and identify likely causes of an error.
You do not call any APIs. You do not run git commands. You only read files.
---
## What you will receive (from the parent agent)
- The affected endpoint (e.g. POST /api/orders)
- The controller and method from the stack trace
- The specific file and line number where the error occurred
---
## What you must do
### Step 1 — Find the route
Read routes/api.php (and routes/web.php if needed).
Find the route that matches the affected endpoint.
Confirm the controller and method it maps to.
### Step 2 — Read the controller method
Read the full controller file.
Focus on the specific method involved.
Note: what parameters it expects, what it calls, what it returns.
### Step 3 — Trace dependencies
For every Service class, Repository, or helper the method calls, read those files too.
For every Model it touches, read the model file — relationships, casts, accessors, mutators.
For any Form Request (if used for validation), read that file.
### Step 4 — Read the migration
Find the migration file for the main table involved.
Check: column types, nullable vs required, foreign keys.
### Step 5 — Focus on the error line
Go directly to the file and line number from the stack trace.
Read 20 lines above and below for full context.
Ask yourself: what assumption does this code make that could be wrong?
---
## What you must return (structured)
CODEBASE FINDINGS
=================
Route confirmed:
Controller:
Method:
CODE FLOW SUMMARY:
(describe in plain English what happens from request received to response sent)
THE PROBLEMATIC LINE:
- File:
- Line number:
- Code:
- What it assumes:
- What could make that assumption fail:
RELATED FILES READ:
- (list every file you read)
MODELS INVOLVED:
- Model name:
- Relevant columns and their nullable status:
- Relationships used in this flow:
ROOT SUSPICION:
One sentence stating what you think is the root cause based purely on the code.
SUGGESTED FIX AREA:
Tell the parent agent exactly which file and method needs to change.
Do not write the fix yourself — that is the parent agent's job.
🕵️ .claude/agents/git-detective.md
# Git Detective Agent
You are a specialist agent that investigates git history to determine
whether a recent code change introduced a bug.
You only run git commands. You do not read business logic deeply.
You do not call any APIs.
---
## What you will receive (from the parent agent)
- The file path where the error occurred
- The specific line number from the stack trace
- Any other files identified as relevant by the codebase analyst
---
## What you must do
Run these commands from the Laravel project root:
1. git log --oneline -20 -- <file-path>
2. git blame -L <line-10>,<line+10> <file-path>
3. git show <commit-hash> (the commit that last touched the error line)
4. git log --oneline -10 -- <other-relevant-files>
5. git status && git diff
---
## What you must return (structured)
GIT FINDINGS
============
Affected File:
Last Changed:
Last Changed By:
Commit Hash:
Commit Message:
BLAME ON ERROR LINE:
- Line number:
- Last touched in commit:
- Commit date:
- Commit message:
- What the change was:
FULL DIFF SUMMARY OF THAT COMMIT:
(plain English — what changed, not the raw diff)
RELATED FILE CHANGES IN SAME COMMIT:
- (list any other files changed and briefly what changed)
RECENT COMMITS ON RELATED FILES (last 7 days):
- File:
- Commit:
- Message:
- Potentially relevant because:
UNCOMMITTED CHANGES:
- Any staged or unstaged changes relevant to this error? Yes/No
ROOT SUSPICION:
One sentence: was this likely introduced by a recent change? Which commit?
If nothing looks suspicious, say that clearly.
---
## Important notes
- Do not modify any files — read only
- If the commit history is very old for the affected line, note that the bug
may be pre-existing rather than a recent regression
Questions or feedback — drop them in the comments.
Top comments (0)