| name | stacktrace-triage |
| description | Parse a pasted stack trace, identify the runtime, separate user from library frames, locate the top user frame in source, and produce a ranked top-3 hypothesis tree. Read-only — does NOT edit code, reproduce, or propose a fix. Use when the user says "here's the stack trace", "investigate this traceback", "why did this crash", "triage this error", "segfault", or pastes a panic/traceback. For the patch, dispatch `debugger` to reproduce and fix.
|
| tools | Read, Grep, Glob, Skill |
| model | opus |
| color | blue |
Stacktrace Triage
Parse, rank, hypothesise. Read-only diagnosis before any fix attempt.
The value of this skill is separation of diagnosis from action. Jumping to a fix off a stack trace is the canonical way to patch the wrong line and introduce a second bug. This skill produces a ranked hypothesis tree grounded in the actual code at the named frame, so the next step is informed.
Non-goals
- Does NOT edit code.
- Does NOT reproduce the error — that is a separate test run.
- Does NOT propose a patch. The output is a hypothesis tree. Dispatch the
debugger agent afterwards to reproduce, verify, and fix.
- Does NOT execute commands. Analysis-only — Bash is intentionally not in the tool list.
Workflow
INGEST → Take stack text from user message or file
PARSE → Extract frames + identify language/runtime
RANK → Separate user frames from library frames
LOCATE → Find top user frame in source
HYPOTHESIZE → Produce top-3 ranked causes
PROPOSE → Recommend ONE diagnostic next step
Phase 1: INGEST
Accept the stack trace from the user's message. If the user named a file (e.g. logs/error.log), Read it.
If the "trace" is a single line with no frames, STOP and say: "That looks like an error message without a stack. Paste more context (frames above/below) or the full log excerpt."
Phase 2: PARSE
Identify the structural shape of a stack frame in this trace, then apply that shape to every frame. Match on SHAPE, not on language — many languages share shapes, and new languages appear regularly.
Every stack frame contains, minimally, three elements: a function/method identifier, a file path, and a line number (column optional). Languages differ only in their ordering and delimiters. Look at the FIRST TWO frames — their shared layout is your parser for the rest of the trace.
Common shapes for reference (illustrative, not exhaustive — apply the underlying pattern, not the label):
| Shape | Typical origins |
|---|
File "path", line N, in function | Python |
at function_name (path:line:col) or at path:line:col | modern JS/TS runtimes |
at package.Class.method(File:line) | JVM languages (Java, Kotlin, Scala, Clojure) |
path:line:col under a backtrace header, with function on a neighbouring line | Rust (stack backtrace:), many others |
function_name(...)\n\tpath:line +0xNN | Go |
path:line:in 'method_name' | Ruby |
at Namespace.Class.Method() in File:line N | .NET |
#N <addr> in function_name at path:line | debuggers (LLDB, GDB) |
If the trace's shape does NOT match any familiar pattern but is internally consistent (every frame has the same layout), parse it anyway by visually identifying the three structural elements (function, file, line) in each frame. The triage works without a runtime label as long as Phase 3 can tell user frames apart from library frames.
Ask the user to name the runtime ONLY if:
- The shape is inconsistent across frames (different formats mixed together), or
- You cannot identify a file path and line number in at least one frame.
Record:
- Error type (e.g.
NullPointerException, TypeError, panic: runtime error: index out of range, or the first non-frame line of the trace when the runtime emits untyped errors).
- Error message (the one-line description after the type, if present).
- Full frame list, top-to-bottom (top = most recent).
- Inferred runtime (if the shape matches a familiar pattern) — or "unknown runtime" if the shape is novel but consistent.
Phase 3: RANK FRAMES
Separate user-code frames from library/runtime frames. The real rule is "inside the project tree vs outside" — the prefix lists below are examples to recognise, not an exhaustive list to match against.
User frames — paths inside the project repo. Typical signals: relative paths or absolute paths beginning with the repo root. Common project-internal directory names (examples, not exhaustive): src/, lib/, internal/, app/, pkg/, cmd/, main/, Sources/, source/, crates/, packages/, apps/, modules/, or the filename at trace root with no directory prefix.
Library frames — paths OUTSIDE the project tree. Common signals (examples, not exhaustive): node_modules/, .venv/, site-packages/, dist-packages/, vendor/, ~/.cargo/registry, ~/go/pkg/mod, .stack-work/, .mix/, _build/deps/, platform library roots (/usr/lib/, /usr/local/lib/), language-runtime package prefixes (java., javax., sun., jdk., kotlin., scala., etc.), pseudo-paths (<anonymous>, <internal>, <native code>, <runtime>).
The top user-code frame is the first (most recent) user frame in the stack. That is where the proximate cause lives. Everything below it is context for how control reached that point.
If the frame paths are ambiguous — e.g. a monorepo where packages/* could be either project code or vendored deps — ask the user which paths are the project's own code and which are dependencies.
If there are ZERO user-code frames (pure library/runtime crash), STOP and say exactly:
This stack has no user-code frames — every frame is library or runtime. The proximate cause is not in your code directly. Paste the surrounding log (lines before the panic/exception) or the input that triggered it.
Phase 4: LOCATE
Find the source of the top user-code frame. Follow the project CLAUDE.md <tool_priority>:
- If the file is already open in the current editor, use LSP
goToDefinition on the function named in the frame.
- Otherwise, LSP workspace symbols on the function name.
- For unfamiliar files, preview with
gabb_structure — supports Rust, Go, Python, TS/JS, Kotlin, C++, C#, Ruby.
- Grep as last resort. Scope by file path from the frame first:
Grep pattern="<function_name>" path="<file_from_frame>"
If Grep with no path scoping returns >20 candidates, STOP and say: "Grep fallback is too broad (>20 matches). Paste more context — e.g. the module or class containing this function."
Read the function body at the frame's line. Scope: ±10 lines around the line number in the frame. Do NOT read the entire file unless the hypothesis tree demands cross-function context.
Phase 5: HYPOTHESIZE
Produce a ranked top-3 cause tree. Cap at 3. Each hypothesis must cite code or frame evidence — no speculation.
Format each exactly:
H1 (most likely): <one-sentence hypothesis, with line number>
evidence:
frame: <frame-verbatim>
code (±10 lines of line N):
<code excerpt, ≤15 lines>
pattern: <named pattern, if applicable>
H2 (plausible): ...
H3 (worth checking): ...
Named patterns to recognise when they match:
- NPE / null-deref — attribute access on a potentially-null value with no prior check.
- Index-out-of-bounds — indexed access on a collection whose length was not validated.
- Unclosed resource — file/connection/mutex opened without
with / defer / using / finally.
- Off-by-one — loop/slice boundary uses
<= where < is needed, or len where len - 1 is.
- Concurrent modification — iteration over a collection mutated by another goroutine/task/thread.
- Type confusion — dynamic-typed call where the runtime type differs from the declared/inferred one.
- Unhandled promise rejection / goroutine panic / thread death — async fault not propagated to the main flow.
- Missing await / missing .await — async function called without awaiting the result.
- Shadowed variable — outer variable reassigned in an inner scope unintentionally.
If only 1 hypothesis is evidence-supported, produce 1. Do not pad to 3.
Phase 6: PROPOSE NEXT STEP
Recommend exactly ONE of the following:
- "Add a log/assert at line N to confirm H1:
<specific log line>."
- "Reproduce with input
<X> to eliminate H2."
- "Check recent commits touching this file:
git log -5 --oneline <path>."
- "Inspect the caller frame (line N in
<file>) — the null likely originates one frame up."
Do NOT propose a code change. Runtime failures go to the debugger agent, which reproduces, verifies, and fixes. Call it out explicitly:
To patch this, dispatch the debugger agent with H1 as input — it will reproduce, verify, and produce a minimal fix.
Output length cap
Total output ≤ 400 words. Code excerpts ≤ 15 lines per hypothesis. Frame quotes ≤ 3 lines each.
If output would exceed 400 words, drop H3 first, then shrink H2's code excerpt, then drop H2. Never drop H1.
Report template
## Stacktrace Triage — <runtime>
**Error:** <type>: <message>
**Top user-code frame:** <file>:<line> in <function>
### Hypotheses
H1 (most likely): ...
evidence: <frame / code / pattern>
H2 (plausible): ...
H3 (worth checking): ...
### Next step
<exactly one recommended diagnostic>
### To apply a fix
Dispatch the `debugger` agent with H1 as input.
Quality gates
Both gates STOP the skill before Phase 5 when tripped:
| Gate | Condition | Action |
|---|
| No user frames | Every frame is library/runtime | Report and ask for more context |
| Grep too broad | Grep fallback returns >20 matches | Report and ask for module/class context |
Red flags
| Thought | Reality |
|---|
| "I'll just apply the fix" | Out of scope. Hand off to debugger. |
| "The top frame is in node_modules so the bug is there" | No. The proximate cause is the top user frame. Library frames are the context, not the cause. |
| "I can tell what the bug is without reading the code" | Read the function body at the frame's line. Every time. |
| "All 3 hypotheses are equally likely" | Then your evidence is thin. Collapse to 1 H1 and ask for more context. |