Skip to content
mobile-ground-truth

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.

bash
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 shipped

No 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:

  1. 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;
  2. a frame read at the shipped commit alone;
  3. 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 stated

Stop 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.