debug-app

SkillMonitoring & ops

Use when the user has finished building a mobile app, started it with `npm run dev`, and wants the running app monitored for runtime errors AND silent failures (empty lists, blank screens, swallowed network errors) and fixed autonomously. Accepts a free-text symptom (e.g., `/debug-app "todos not appearing on home screen"`) to drive terminal-log diagnostics — injects temporary console.log statements at data-path boundaries, reads Metro terminal output, and cleans up logs after the root cause is fixed. Otherwise polls the Metro terminal every 5s, classifies errors using an 8-category table, fixes inline or routes to the right skill, verifies each fix from terminal output, and exits after 3 consecutive clean polls. Foreground loop — blocks the conversation while running. Run only after the app is loaded.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the debug-app skill

What this skill tells your AI

The instructions your AI receives, as published by microsoft/power-platform-skills in plugins/mobile-apps/skills/debug-app/SKILL.md and read by ahel’s review.

📋 Shared instructions: shared-instructions.md — read first.

Debug App — Monitor & Fix

Monitor the running app by reading the Metro dev-server terminal output, detect runtime and bundle errors, and fix them autonomously by editing the affected files (or routing to the right skill when the fix belongs in a domain like Dataverse schema or auth registration). For silent failures, inject temporary console.log statements at data-path boundaries, read the Metro terminal for output, then clean them up after the root cause is fixed. Modeled on the upstream app-debugger.agent.md pattern — foreground loop, 5-second cadence, exit on 3 consecutive clean polls.

Dev-client limitation: the standalone dev client outputs app/runtime logs, React errors, and Metro bundler output to the terminal running npm run dev. This includes host runtime diagnostics that use strings such as [AuthProvider] MSAL init failed:, [bridge] fetch THREW for, [bridge] HTTP <status> for, [addAadAppToConnectionAcl] failed HTTP <status> for connection, [useConnectionRefs] could not verify connection ACLs; treating existing connections as setup-required, and [PAHost][ErrorBoundary] Unhandled JS error:. There is no separate device log stream. All diagnosis happens by reading that terminal and, where needed, injecting strategic trace statements into source files.

Subcommands (parsed from $ARGUMENTS)

FormBehavior
/debug-app (no args)Default — terminal log-driven mode. Run Phase 0 (startup check), enter monitor loop. Log source is the Metro terminal (BashOutput on the $METRO_TERMINAL_ID recorded in memory-bank.md by /create-mobile-app Step 12). One read covers Metro bundler errors, app/runtime log lines (including host diagnostics), and red-box stack traces. If the terminal ID is not in memory-bank.md, ask the user which terminal is running npm run dev before starting.
/debug-app "<symptom text>"Symptom-driven mode (recommended when there's a user-visible problem). Free-text symptom such as "todos not appearing on home screen", "login button does nothing", "list empty after refresh". Run Phase 0 → Phase 0.5 (parse symptom → ask the user to reproduce/navigate → walk the likely data path from terminal traces) → enter monitor loop. Catches silent failures (empty lists, blank screens, swallowed errors) that pure log polling misses.
/debug-app statusPrint current state (last poll, fixes applied this session, unresolved errors). Do NOT enter loop.
/debug-app stopIf a loop is in progress, the user can type "stop" or this command to exit. State files preserved at .claude/debug-app/.

Dispatch rule: if $ARGUMENTS is non-empty and is not one of the reserved subcommand tokens (status, stop, help, --help, -h, version, --version), treat the entire string (everything after the command name; outer quotes optional) as the symptom and use symptom-driven mode. For help / --help / -h, print the subcommands table above and exit.

Tip — "play around then debug": in primary mode, BashOutput($METRO_TERMINAL_ID) returns Metro output accumulated since the last read. So if something weird just happened, keep using the app the way you would normally — then run /debug-app (or /debug-app "<what you saw>") and the very first cycle will see the entire history of your session, not just what arrives after the skill starts. No need to reproduce the bug under the agent's eye.

Core Principles

  • Foreground autonomous loop — Once started, this skill owns the conversation until 3 consecutive clean polls confirm the app is healthy, the user types stop, or the escalation rule trips. Do not run other skills concurrently — they'll queue behind the loop.
  • Run AFTER the app is loadednpm run dev must be running and the simulator/device must have the app open. Phase 0 verifies this; the skill stops cleanly if no app is detected.
  • Native-only runtime target — The app must be loaded in a native dev client on a device or simulator; Metro terminal output is the log source for that native session.
  • No web or direct Metro probes — Do not use React Native Web, browser automation, curl, fetch, WebFetch, or any direct request to a Metro/localhost endpoint for runtime diagnosis. Read only the Metro terminal and source files.
  • No screen-by-screen verification — Do not crawl routes or validate every screen. In symptom mode, focus only on the user-reported workflow and the terminal/source evidence needed to diagnose it.
  • One fix at a time — Fully resolve one issue (context → fix → type-check → reload → re-poll) before starting the next. No batching.
  • Working-dir state — All session state lives in .claude/debug-app/ (gitignored): fixes.md for audit log, unresolved.md for escalations, injected-logs.md for tracking injected console.log statements. Survives across runs.
  • Reference resolution order — For unfamiliar errors: in-repo references first (skills/add-dataverse/references/dataverse-reference.md, etc.), then mcp__microsoft-learn__microsoft_docs_search, then general web search.

Workflow — Task List First

Before entering the monitor loop, write a task list and keep it up to date:

- [ ] Verify dev server is running (BashOutput on Metro terminal — expect Metro banner)
- [ ] Capture baseline terminal state (read BashOutput, note most recent activity)
- [ ] (Symptom mode only) Phase 0.5: parse symptom → ask user to navigate → inject console.logs → read terminal → walk data path → clean up logs
- [ ] Monitoring cycle 1: collect → classify → fix if needed
- [ ] Monitoring cycle 2: collect → classify → fix if needed
- [ ] Monitoring cycle 3: collect → classify → fix if needed
      (add cycles as needed; stop after 3 consecutive clean cycles AND symptom resolved/flagged)
- [ ] Fix: <error summary> → <inline edit | skill route>  (one task per error found)

Mark each cycle complete (clean OR fixed) before starting the next.


Phase 0 — Startup Check

Before entering the loop:

0.0 Resolve the Metro terminal

The Metro terminal is the only log source. The dev-player routes all JS output there.

  1. Read memory-bank.md for the Metro terminal id: line (written by /create-mobile-app Step 12).
  2. If found, call BashOutput against that id once. If it returns any Metro output (even just the banner), set $METRO_TERMINAL_ID and continue.
  3. If memory-bank.md has no terminal id, or BashOutput returns "shell not found" / "no such background shell": ask the user:

    "Which terminal is running npm run dev? I need its terminal ID to read Metro logs. If you started it in VS Code, look for the active terminal tab name." Wait for the user to provide the ID, then retry BashOutput against the provided id. Set $METRO_TERMINAL_ID and continue.

Record the resolved id in fixes.md:

[<HH:MM:SS>] Log source — Metro terminal $METRO_TERMINAL_ID

0.1 Ensure state directory

mkdir -p .claude/debug-app
touch .claude/debug-app/fixes.md
touch .claude/debug-app/unresolved.md
touch .claude/debug-app/injected-logs.md
rm -f .claude/debug-app/symptom-state    # per-session — Phase 0.5 rewrites it if symptom mode is active

If fixes.md is empty, write a session header:

# Debug session — <date>

0.2 Verify Metro bundled and the app is running

Telemetry checkpoint: validate_metro_session

Branch on the source resolved in 0.0.

If $METRO_TERMINAL_ID is set (primary path):

Call BashOutput on it once and scan the captured Metro output:

  • Most recent error-class line is SyntaxError, Unable to resolve module, transform failed, or error: Bundling failed → bundle is broken. Treat as a Step B "Import / Bundle" critical error and route through Step D immediately. Do NOT enter the steady-state loop until the bundle is healthy.
  • Output contains Bundling complete / iOS Bundled / Android Bundled with no later error-class line → Metro is healthy. Proceed.
  • Output contains a Metro banner (Metro waiting on, Logs for your project) but no native Bundled / bundling lines yet → Metro is up but no native client has connected. Tell the user:

    Metro is running but no app is connected yet. Open the app on a device or simulator, then re-run /debug-app. Stop here.

  • Output is empty, OR contains no Metro banner at all → the recorded shell is alive but Metro isn't running in it (the user repurposed the terminal). Tell the user:

    Metro not detected in the recorded terminal. Restart with npm run dev and re-run /debug-app — the new terminal id will be picked up from memory-bank.md. Stop here.

If $METRO_TERMINAL_ID is NOT set:

Ask the user:

"Which terminal is running npm run dev? Please provide the terminal ID so I can read Metro output."

Wait for the user to reply. Set $METRO_TERMINAL_ID to the provided ID, call BashOutput($METRO_TERMINAL_ID) once, and continue with the checks above.

0.3 Capture baseline

Telemetry checkpoint: capture_runtime_baseline

Read the latest output from BashOutput($METRO_TERMINAL_ID). Note the most recently bundled native platform (iOS / Android) and any recent runtime log lines. Append to fixes.md:

[<HH:MM:SS>] Baseline — last Metro activity: <1-line summary of most recent lines>

0.4 Initialize cursor

BashOutput maintains an internal stream cursor against $METRO_TERMINAL_ID — each call returns only output produced since the previous call. No separate cursor file is needed. The .claude/debug-app/cursor file is no longer used and can be ignored if present from a previous session.


Phase 0.5 — Symptom-driven setup (only when $ARGUMENTS is a symptom string)

Skip this entire phase if no symptom was provided. The standard log-polling loop alone is good at visible errors but blind to silent ones: an empty list because the connector wasn't added, a blank screen because useFocusEffect wasn't wired, blank rows because column names don't match the model. Phase 0.5 closes that gap.

0.5.1 Parse the symptom

Extract three signals from the user's text:

SignalHow to derive
Affected screenMatch keywords against route filenames in app/ (e.g., "todos"app/(tabs)/todos.tsx, app/todos/index.tsx, app/(tabs)/index.tsx). Use Glob to enumerate app/**/*.tsx; pick the closest substring match. If multiple, ask once.
Affected entity / serviceSame keyword against src/generated/services/*Service.ts and src/generated/models/*Model.ts (e.g., "todos"TodosService, Todo model). Use Glob.
Symptom classMap the text to one of: empty-list, blank-screen, wrong-data, unresponsive-control, stale-data, wrong-navigation, crash, pdf-viewer, pdf-report, pen-input, geolocation, dataverse-upload. Default for "PDF won't open / preview PDF fails": pdf-viewer. Default for "report PDF not generated / print report fails": pdf-report. Default for "signature / pen / ink fails": pen-input. Default for "location not tracking / GPS not updating / background location stopped / breadcrumb gaps / route not consistent": geolocation. Default for "signature/report saved but missing", or "location rows not reaching Dataverse": dataverse-upload. Default for "not appearing / not showing / nothing here / missing": empty-list. Default for "doesn't load / freezes / spinner forever": blank-screen.

Append to fixes.md:

[<HH:MM:SS>] Symptom — class=<class> screen=<path> entity=<service>

If no screen/entity match: keep screen=unknown / entity=unknown and proceed — Phase 0.5 still injects diagnostic logs and reads the terminal from whatever data path is most likely based on the symptom text.

0.5.2 Ask the user to navigate to the affected screen

The dev-player has no automation API for navigation. Ask the user:

"Please open the <screen> screen on the device/simulator, then reply ready."

Wait for the user to confirm before proceeding.

0.5.3 Inject diagnostic console.log statements and read terminal

Inject targeted console.log statements at the boundaries of the suspected data path so the Metro terminal reveals what's happening.

Injection sites — choose the minimum set that covers the symptom class:

Symptom classInject at
empty-list(a) entry point of the data-fetching hook, logging [TRACE items] the raw response length; (b) the screen component, logging [TRACE render] the items array length before the list renders
blank-screenEntry point of the screen component, logging [TRACE mount] a timestamp and any auth/data props passed in
wrong-data / stale-dataThe hook that calls the generated service (NOT inside src/generated/), logging [TRACE service-response] the raw return value
unresponsive-controlThe event handler (onPress, onSubmit, etc.) logging [TRACE handler-called] before any async work
crashSkip injection — jump to the monitor loop (Step A), crash stacks appear in the terminal

Console.log injection pattern — all injected lines MUST use this exact format:

console.log('[TRACE <tag>]', <value>); // [INJECTED-TRACE]
  • <tag> — short unique label for this site (e.g., items, render, service-response)
  • // [INJECTED-TRACE] trailing comment on the SAME LINE — this is the cleanup grep key
  • Log the smallest useful value; use JSON.stringify(value) for objects
  • Never inject inside src/generated/ — inject in the hook/screen that calls into it

Record every injection in .claude/debug-app/injected-logs.md:

[<HH:MM:SS>] Injected [INJECTED-TRACE] at <file>:<line> — tag=<tag>

Then tell the user:

"I've added diagnostic console.log statements. Please reload the app (press r in the Metro terminal), navigate to <screen>, and trigger the symptom (e.g., scroll the list, tap the button). Reply done when finished."

Wait for the user to reply, then call BashOutput($METRO_TERMINAL_ID) and filter for [TRACE lines.

0.5.4 Walk the data path from terminal output

Use the [TRACE lines to walk the chain:

  1. Screen TSX (app/<route>.tsx)

    • Find the useListData(...) / use*Data(...) call.
    • Check service-call options — a stray top: 0, an over-strict filter, a search: query bound to a never-cleared input, or orderBy on a missing column can each silently return zero rows.
    • Check any client-side .filter(...) after the data lands.
  2. Data hook (src/hooks/useListData.ts or sibling)

    • Critical: the template hook has TWO mock-fallback paths:
      • Error path: service returns { error } → hook substitutes mock AND may call setError. Silent if the screen ignores error.
      • Empty-result path: service returns { data: [] } (no error) → hook silently substitutes mock. Always invisible without a [TRACE] log.
    • Detect: Grep for MOCK_ imports in the screen file. If present, mock data is wired in.
    • Confirm useFocusEffect is used (not useEffect) — useEffect won't re-run on back-navigate.
  3. Generated service (src/generated/services/<Name>Service.ts)

    • If a TODO stub or file missing → route to /add-connector or /add-dataverse. Do NOT edit src/generated/.
    • If it exists and the [TRACE service-response] log shows an error field → read that error; 401/403 = auth issue; 404 = wrong resource name.
  4. Generated model (src/generated/models/<Name>Model.ts)

    • Confirm field names match what the screen references. item.title vs cr3e9_title produces blank rows.
  5. power.config.json

    • Confirm the datasources array contains the suspected entity / connector. If absent, npx power-apps add-data-source was never run for it.
  6. Auth state (src/playerConfig.ts, app.config.js, auth.config.json, useAuth() hook)

    • 401 from the service wrapped as { error } — the [TRACE service-response] log surfaces the error string.
    • OAuth deeplink handoff: verify app.config.jsexpo.scheme matches src/playerConfig.tsconnectorOAuthRedirectUri, AND the same redirect URI is in auth.config.json and the Entra ID registration. If the app registration is missing, route the user to the Power Apps Wrap page via /set-app-registration-native.

Classify the [TRACE output:

OutputMeaningNext step
[TRACE items] 0 or [] — no error fieldService returned empty — check filter/query or data not seededFix the query; if no records exist, seed sample data
[TRACE items] undefinedHook never received a response — likely service stub or missing datasourceRoute to /add-connector or /add-dataverse
[TRACE service-response] shows error stringService threw — read the error; 401/403 = auth; 404 = wrong resourceFix auth config or re-run add-data-source
[TRACE render] N > 0 but list looks emptyField name mismatch between model and screenFix screen field references to match the model
[TRACE handler-called] never appearsonPress not wired or component not mountedRead TSX, fix the event binding
No [TRACE lines at allMetro may have cached the old bundleAsk user: stop Metro, run npx expo start --clear, reload

Record the outcome in .claude/debug-app/symptom-state (single line: resolved, flagged, or pending).

  • Fix is clear and local → apply via Step D3 + D4 (type-check + reload + re-poll). After the fix, ask the user to interact with the screen again and read the terminal. If the [TRACE items] line shows N > 0, write resolved.
  • Fix routes to another skill → tell the user, log to unresolved.md. Write flagged.
  • No obvious cause → log a structured note to unresolved.md. Write pending and enter the monitor loop.

0.5.5 Clean up injected console.log statements

After the root cause is identified and a fix is applied (or Phase 0.5 concludes), remove ALL injected logs:

grep -rn 'INJECTED-TRACE' app/ src/hooks/ src/services/

For each matching file, edit out the console.log(...); // [INJECTED-TRACE] lines. Verify with:

grep -rn 'INJECTED-TRACE' app/ src/hooks/ src/services/  # must return zero results

Clear the tracking file:

echo '' > .claude/debug-app/injected-logs.md

Run npm run type-check once after cleanup.

Hard rule: Never leave [INJECTED-TRACE] lines in code. Clean up before marking the session done, even if the symptom is pending or flagged.

0.5.6 Re-enter the standard monitor loop

After Phase 0.5 completes, fall through to the monitor loop (Step A). The "3 consecutive clean cycles" exit condition is suspended until the symptom is either marked resolved or recorded as NEEDS ATTENTION in unresolved.md. After that, the loop exits per the standard rule.


Monitor Loop

Repeat until 3 consecutive clean cycles, OR the user types stop, OR the escalation rule trips.

Step A — Collect logs

Telemetry checkpoint: collect_runtime_logs

BashOutput(bash_id=$METRO_TERMINAL_ID)

BashOutput returns only output produced since the previous call against the same shell — its built-in stream cursor IS the cursor.

In the new output, surface as classifiable signal:

  • Runtime ERROR / WARN / LOG prefixes
  • Host diagnostic lines with prefixes like [PAHost], [bridge], [AuthProvider], [AuthContext], [useConnectionRefs], [useConnectionSetup], [addAadAppToConnectionAcl], [PAHost][ErrorBoundary]
  • Stack frames (at <fn> (<file>:<line>:<col>))
  • Bundle-class errors (Unable to resolve module, SyntaxError, transform failed) — re-classify as Step B "Import / Bundle" Critical and route through Step D
  • HTTP method + status from Metro's request log (e.g., "GET /index.bundle?platform=ios&dev=true ..." 500 -) — non-200 on .bundle is a bundle/transform failure; non-2xx on connector / Dataverse hosts feeds Step B "Network / API"
  • Lines containing Bundling complete / iOS Bundled / Android Bundled are informational — log to fixes.md at debug volume but do NOT classify as an issue
  • [TRACE prefixed lines from injected trace statements — classify under the symptom walk (Phase 0.5.4), not as errors

Interpretation rule for host diagnostic lines:

  • Treat as classifiable signal only when lines match emitted host strings such as:
    • [AuthProvider] MSAL init failed:
    • [AuthProvider] Intune enrollment failed:
    • [AuthContext] acquireTokenSilent failed for scopes:
    • [AuthProvider] Intune unenroll failed:
    • [bridge] unhandled plugin call
    • [bridge] fetch THREW for
    • [bridge] HTTP <status> for
    • [addAadAppToConnectionAcl] failed HTTP <status> for connection
    • [addAadAppToConnectionAcl] error:
    • [useConnectionRefs] could not verify connection ACLs; treating existing connections as setup-required
    • [useConnectionRefs] Failed to load connections:
    • [useConnectionSetup] could not grant connection ACL: missing Power Apps token or user OID
    • [PAHost] getConnectorToken: acquireToken threw for apiId="...":
    • [PAHost] getConnectorToken: acquireToken returned null for apiId="..."
    • [PAHost] getDataverseToken: acquireToken threw for orgUrl="...":
    • [PAHost] getDataverseToken: acquireToken returned null for orgUrl="..."
    • [PAHost][ErrorBoundary] Unhandled JS error:
    • [PAHost][ErrorBoundary] Error stack:
    • [PAHost][ErrorBoundary] Component stack:
  • Treat as informational when lines are lifecycle/status-only, such as bridge setup/ready, token acquisition start/success, bridge registration, and connection-setup screen visibility.

Step B — Classify each new log entry

Telemetry checkpoint: classify_runtime_failures

Apply the 8-category table. Treat each unique stack trace / error message as one issue.

PriorityPatternCategory
CriticalUncaught exception, unhandled promise rejection, app crashJS Runtime
CriticalUnable to resolve module, Cannot find moduleImport / Bundle
CriticalSyntaxError, Unexpected token, transform failed (multi-line block from Metro terminal, primary mode only)Import / Bundle
CriticalCannot read properties of undefined, is not a functionJS Runtime
HighNATIVE_MODULE_MISSING from pdfViewer or penInput wrapperNative
HighNATIVE_MODULE_MISSING, PERMISSION_DENIED, or TRACKING_FAILED from geolocation wrapperNative
HighINVALID_URL from pdfViewer, or logs mentioning content://, blob:, or http:// PDF viewer inputJS Runtime
HighVIEWER_FAILED or CAPTURE_FAILED from PDF/pen wrapperNative
HighERROR level runtime logJS Runtime
HighHTTP 4xx / 5xx surfaced in logsNetwork / API
HighNative module or bridge errorNative
MediumReact Warning: component errorReact
LowWARN level log that is not known noiseGeneral

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
859
Forks
176
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
debug-app
Source
github.com/microsoft/power-platform-skills