debug-app
SkillMonitoring & opsUse 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.
No other account needed.
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)
| Form | Behavior |
|---|---|
/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 status | Print current state (last poll, fixes applied this session, unresolved errors). Do NOT enter loop. |
/debug-app stop | If 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 loaded —
npm run devmust 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.mdfor audit log,unresolved.mdfor escalations,injected-logs.mdfor 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.
- Read
memory-bank.mdfor theMetro terminal id:line (written by/create-mobile-appStep 12). - If found, call
BashOutputagainst that id once. If it returns any Metro output (even just the banner), set$METRO_TERMINAL_IDand continue. - If
memory-bank.mdhas no terminal id, orBashOutputreturns "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 retryBashOutputagainst the provided id. Set$METRO_TERMINAL_IDand 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, orerror: 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 Bundledwith no later error-class line → Metro is healthy. Proceed. - Output contains a Metro banner (
Metro waiting on,Logs for your project) but no nativeBundled/bundlinglines 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 devand re-run/debug-app— the new terminal id will be picked up frommemory-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:
| Signal | How to derive |
|---|---|
| Affected screen | Match 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 / service | Same keyword against src/generated/services/*Service.ts and src/generated/models/*Model.ts (e.g., "todos" → TodosService, Todo model). Use Glob. |
| Symptom class | Map 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 replyready."
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 class | Inject 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-screen | Entry point of the screen component, logging [TRACE mount] a timestamp and any auth/data props passed in |
wrong-data / stale-data | The hook that calls the generated service (NOT inside src/generated/), logging [TRACE service-response] the raw return value |
unresponsive-control | The event handler (onPress, onSubmit, etc.) logging [TRACE handler-called] before any async work |
crash | Skip 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
rin the Metro terminal), navigate to<screen>, and trigger the symptom (e.g., scroll the list, tap the button). Replydonewhen 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:
-
Screen TSX (
app/<route>.tsx)- Find the
useListData(...)/use*Data(...)call. - Check service-call options — a stray
top: 0, an over-strictfilter, asearch: querybound to a never-cleared input, ororderByon a missing column can each silently return zero rows. - Check any client-side
.filter(...)after the data lands.
- Find the
-
Data hook (
src/hooks/useListData.tsor sibling)- Critical: the template hook has TWO mock-fallback paths:
- Error path: service returns
{ error }→ hook substitutes mock AND may callsetError. Silent if the screen ignoreserror. - Empty-result path: service returns
{ data: [] }(no error) → hook silently substitutes mock. Always invisible without a[TRACE]log.
- Error path: service returns
- Detect:
GrepforMOCK_imports in the screen file. If present, mock data is wired in. - Confirm
useFocusEffectis used (notuseEffect) —useEffectwon't re-run on back-navigate.
- Critical: the template hook has TWO mock-fallback paths:
-
Generated service (
src/generated/services/<Name>Service.ts)- If a TODO stub or file missing → route to
/add-connectoror/add-dataverse. Do NOT editsrc/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.
- If a TODO stub or file missing → route to
-
Generated model (
src/generated/models/<Name>Model.ts)- Confirm field names match what the screen references.
item.titlevscr3e9_titleproduces blank rows.
- Confirm field names match what the screen references.
-
power.config.json- Confirm the
datasourcesarray contains the suspected entity / connector. If absent,npx power-apps add-data-sourcewas never run for it.
- Confirm the
-
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.js→expo.schemematchessrc/playerConfig.ts→connectorOAuthRedirectUri, AND the same redirect URI is inauth.config.jsonand the Entra ID registration. If the app registration is missing, route the user to the Power Apps Wrap page via/set-app-registration-native.
- 401 from the service wrapped as
Classify the [TRACE output:
| Output | Meaning | Next step |
|---|---|---|
[TRACE items] 0 or [] — no error field | Service returned empty — check filter/query or data not seeded | Fix the query; if no records exist, seed sample data |
[TRACE items] undefined | Hook never received a response — likely service stub or missing datasource | Route to /add-connector or /add-dataverse |
[TRACE service-response] shows error string | Service threw — read the error; 401/403 = auth; 404 = wrong resource | Fix auth config or re-run add-data-source |
[TRACE render] N > 0 but list looks empty | Field name mismatch between model and screen | Fix screen field references to match the model |
[TRACE handler-called] never appears | onPress not wired or component not mounted | Read TSX, fix the event binding |
No [TRACE lines at all | Metro may have cached the old bundle | Ask 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, writeresolved. - Fix routes to another skill → tell the user, log to
unresolved.md. Writeflagged. - No obvious cause → log a structured note to
unresolved.md. Writependingand 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 ispendingorflagged.
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/LOGprefixes - 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.bundleis a bundle/transform failure; non-2xx on connector / Dataverse hosts feeds Step B "Network / API" - Lines containing
Bundling complete/iOS Bundled/Android Bundledare informational — log tofixes.mdat debug volume but do NOT classify as an issue [TRACEprefixed 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.
| Priority | Pattern | Category |
|---|---|---|
| Critical | Uncaught exception, unhandled promise rejection, app crash | JS Runtime |
| Critical | Unable to resolve module, Cannot find module | Import / Bundle |
| Critical | SyntaxError, Unexpected token, transform failed (multi-line block from Metro terminal, primary mode only) | Import / Bundle |
| Critical | Cannot read properties of undefined, is not a function | JS Runtime |
| High | NATIVE_MODULE_MISSING from pdfViewer or penInput wrapper | Native |
| High | NATIVE_MODULE_MISSING, PERMISSION_DENIED, or TRACKING_FAILED from geolocation wrapper | Native |
| High | INVALID_URL from pdfViewer, or logs mentioning content://, blob:, or http:// PDF viewer input | JS Runtime |
| High | VIEWER_FAILED or CAPTURE_FAILED from PDF/pen wrapper | Native |
| High | ERROR level runtime log | JS Runtime |
| High | HTTP 4xx / 5xx surfaced in logs | Network / API |
| High | Native module or bridge error | Native |
| Medium | React Warning: component error | React |
| Low | WARN level log that is not known noise | General |
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