debug-investigate
Root-cause a crash, exception, stacktrace, failing test, or wrong behavior in portal/ — reproduce, read the real error, trace to the actual cause, propose the minimal fix. Use when something raises/fa
SKILL.md.claude/skills/debug-investigate/SKILL.md
GitHubInvestigate (find the cause, not a symptom)
Do not propose a fix until you can point at the line that's wrong and say why. Never edit a test to make a real failure pass.
Method
Reproduce the failure before theorizing: the exact failing command
(./run test portal <path>:<line>), the LiveView action, the MCP call, or the
failing context function in iex -S mix. Read the full error (stacktrace,
changeset errors, SQL) and the code and data it touched; confirm any contract
you're unsure of with /tooling-verify-api. Fix only once the evidence confirms
the cause, fix it at the cause, and add a regression test that fails before
and passes after (plus the denial / cross-account variant for a context bug, §7).
emisar error catalogue (where to look first)
{:error, %Ecto.Changeset{}}: readchangeset.errors. A*_constrainterror (unique/foreign_key) means the DB rejected it: the matchingunique_constraint/foreign_key_constraintis missing from the Changeset, or the migration lacks the index/FK. Avalidate_*error is just invalid input.{:error, :unauthorized}: the subject lacks the permission (Authorizer role list), orensure_has_permissionsis checking the wrong one.{:error, :not_found}: genuinely missing, orAuthorizer.for_subjectscoped it out (cross-account). Check the subject's account vs the row's.- Recurrent job keeps failing: read the
execute/1error and the durable rows it polls; a job that isn't idempotent (IL-13) corrupts state when a tick repeats. - LiveView: value missing/doubled on load →
mountruns twice (IL-18); a silent form failure → check the{:error, changeset}branch is handled, not the UI. - MCP: a doubled action → the operation-id recovery path
(
get_operation/MCPOperations.fetch_recovery); MCP mutations dedupe on the bridge's operation ID (mcp-api.md); there is no caller-supplied idempotency key. An auth error → the subject built at the MCP boundary. - Runner socket: state not updating → the runner-socket state helpers
(
Runners.apply_state,connect_runner,disconnect_runner,record_heartbeat; §1.4); a crash shouldn't take down other runners (IL-17). - Flaky test: a cross-process race, either
$callersnot inherited or async side effects not made sync in test (thenotify_approvers_async?flag pattern). NeverProcess.sleepit away; useassert_receivewith a timeout.
Output
Cause: <file:line — what's wrong and why> → Fix: <the minimal change> →
Regression test: <what it asserts>. If you can't isolate it, say what you ruled
out and what you'd instrument next. Don't ship a guess.