Slash command
/crash-triage
Triage a mobile crash only when the trace is symbolicated and its symbols proven to match the shipped build; otherwise name what is missing and stop.
The prompt
What it actually sends
A skill is a prompt template, not code. This is the whole of it — expanded fresh on every call, with $ARGUMENTS and $1..$9 filled in from what you type.
Triage one crash report against the repository in $CWD. The report — a path to a .ips
or .crash file, a tombstone, a Play Console or Crashlytics export, or the text pasted
inline — and the build it came from: $ARGUMENTS
The output is a ranked hypothesis with its evidence, and there are two conditions under which this command produces no hypothesis at all. Both are checked before a single frame is read.
Gate 1 — is it symbolicated?
An unsymbolicated frame is an address. A module name and an offset look like information
and are not: MyApp 0x0000000102a4c1e8 0x1024f4000 + 5603816 names the binary, not the
function, and the arithmetic that turns it into a function requires a symbol file this
command does not have.
When the trace is not symbolicated, write UNSYMBOLICATED — no diagnosis, name the
exact artifact that is missing, print the command from $SKILL_DIR/symbolication.md
that would produce the symbolicated trace, and stop. Do not infer the fault from the
module names, from the exception type, from which framework appears most often, or from
the shape of the offsets. That inference is available, it is fluent, and it is a
coin-flip presented as a finding.
Gate 2 — do the symbols belong to this build?
This is the gate that gets skipped, and skipping it is worse than gate 1, because a mismatched symbol file does not fail — it prints confident, wrong function names. The addresses still resolve; they resolve against a different binary layout.
Each platform has one identity check, and $SKILL_DIR/symbolication.md carries the
commands:
| Platform | The identity that must match |
|---|---|
| iOS / macOS | the dSYM's UUID equals the Binary Images UUID for that image in the report — dwarfdump --uuid on the dSYM, per architecture |
| Android (JVM) | the mapping.txt archived for that exact versionCode, not one regenerated by rebuilding — R8's output is not reproducible across a toolchain or dependency change |
| Android (native) | the Build ID in the tombstone equals readelf -n on the unstripped .so |
| React Native / Hermes | the source map produced by the build of that bundle commit, matched by hashing the shipped bundle against the artifact the map was emitted with |
| Flutter / Dart | the build_id in the crash header equals the one in the --split-debug-info symbols file |
When the identity check fails, or cannot be run because the artifact is absent, write
IDENTITY-UNPROVEN, name which check failed and what would satisfy it, and stop. A
symbolicated trace whose symbols were not proven to match is not better evidence than an
unsymbolicated one; it is the same evidence with the uncertainty removed from view.
Read the termination reason before the frames
A crash report's first fact is how the process ended, and several endings are not "a crash in the top frame" at all. Getting this wrong sends a team to read code that was merely unlucky enough to be executing.
| Signal in the report | What actually happened | What the top frame means |
|---|---|---|
0x8badf00d |
the iOS watchdog killed the app for taking too long | where the main thread was blocked, not a defect at that line — the defect is whatever the thread was waiting on |
EXC_RESOURCE, per-process-limit, a jetsam event |
the app was terminated for memory or CPU, not a fault | the allocation site at the moment of death, which is usually not the leak |
0xdead10cc |
the app held a file lock or a SQLite handle while suspended | the holder, which is the real subject |
0xc00010ff |
the device was thermally throttled and the app was killed | almost nothing about the frame |
| Android ANR | the system gave up waiting; the report is every thread's stack at that instant | the main thread's block, and the lock owner's stack is the one worth reading |
java.lang.OutOfMemoryError |
allocation failed | the allocation that lost, not the one that leaked |
SIGSEGV / SIGABRT in a tombstone |
a genuine native fault or abort | a real frame, once gates 1 and 2 pass |
A termination reason is never restated as "a crash in X". Write what the reason means, then say what evidence would identify the actual subject — the blocking call, the lock owner, the retained graph.
Read frames at the shipped commit
A frame names a file and a line in the binary that shipped. Reading that line in the working tree reads a different file, and the difference is silent.
git log -1 --format=%H <tag-or-sha> # the commit that produced the build
git show <sha>:<path> | sed -n '<line>p' # the line as it actually shippedNo line is quoted that was not read at the shipped commit. When that commit is not
in this checkout, the frame is reported as line not read — commit <sha> not present,
and the hypothesis that depended on it drops a rank. Do not read the working-tree copy
"to get the gist": the gist is what a line-number shift turns into a wrong subject.
Then, and only then, the hypothesis
Rank hypotheses by the evidence behind them, not by plausibility:
- a frame read at the shipped commit, plus a state in the report that explains it — a thread state, a register, an exception message, an ANR lock owner;
- a frame read at the shipped commit alone;
- a pattern across reports — same signature, same OS version, same device class, same locale — where the count is stated and the source of the count is named.
Each hypothesis carries: what it claims, the evidence lines that support it, and the one observation that would kill it. A hypothesis nothing could falsify is not ranked; it is dropped, and the drop is reported with the count.
The refusals
- It will not diagnose an unsymbolicated trace. The output is
UNSYMBOLICATED — no diagnosis, the missing artifact, and the command. - It will not diagnose a trace whose symbols it could not prove match the shipped
build. The output is
IDENTITY-UNPROVEN, the failed check, and what would satisfy it. - It will not name a line it did not read at the shipped commit. The substitute is
line not read — commit <sha> not present. - It will not convert a termination reason into a defect. A watchdog kill, a jetsam OOM and a thermal kill are reported as what they are, with the subject named as unknown until evidence identifies it.
- It produces a hypothesis, never a fix. Asked for the patch, it names the file, the question the patch has to answer, and the check that would show the crash is gone — and stops. A fix written from a stack trace is a change to code nobody has read in the state that produced the crash.
Output
REPORT: <identifier — file, issue id, or "pasted">
Platform: <ios | android-jvm | android-native | react-native-hermes | flutter>
Termination: <signal / reason code / ANR> — <what that reason means>
Symbolication: SYMBOLICATED (<artifact>) | UNSYMBOLICATED — need <artifact>
Identity: PROVEN (<check> → <matching value>) | UNPROVEN (<which check, why>)
Shipped commit: <sha> — present | not present in this checkout
Occurrences: <count> — <source of the count> | not statedStop after this block when either gate failed, with the command that would clear it.
When both gates passed, continue:
HYPOTHESIS 1 — <one sentence>
evidence: <frame at path:line, read at <sha>>
<report state: thread, register, message, lock owner>
killed by: <the observation that would rule this out>
HYPOTHESIS 2 — …
Not hypotheses (<n>): <dropped candidate> — <why it is unfalsifiable or unevidenced>
Next artifact: <the one thing that would most narrow this — a full tombstone, the
ANR's other threads, a second report from a different OS version>Reference files it reads
Shipped beside the prompt and reachable from it as $SKILL_DIR.
- symbolication.md
Part of mobile-ground-truth. Install it with arcturn add sitharaj88/arcturn/kits/mobile-ground-truth, then type /crash-triage in a session.