/debug-extension

SkillFiles & storage

Diagnose and fix failures in a built third-party `.ppmplugin` control: crashes, silent no-ops, PCF error outputs, or incorrect behavior. Uses the reported symptom, `shared/error-codes.md`, and file-level evidence to trace the manifest, Android/iOS modules, PCF dispatch, and build configuration. Produces a ranked diagnosis, asks for approval, then applies a surgical fix while keeping the committed manifest and affected contracts synchronized. Re-validates manifest changes through /generate-ppmplugin-manifest.

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-extension skill

What this skill tells your AI

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

Investigate a failure the user observed while testing a built .ppmplugin control, find the root cause, and fix it. The wrap binary runs inside the customer's shell with no logcat / Xcode console / native debugger reachable, so the evidence is usually just the PCF's ErrorCode / ErrorMessage, the raw <name>Json diagnostic output, a host log line, or the user's description of what they saw. This skill turns that thin evidence into a located root cause and a fix.

Investigation-first, fix as the resolution. Unlike a plain "apply this change" flow, /debug-extension starts from a symptom and works backward to a cause before touching code. When the cause is found, it proposes the fix and applies it under the same discipline a careful edit uses (spec-vs-drift diagnosis, contract-consistency, surgical edits, gates).

This is one door, not the only door. Per shared/shared-instructions.md §7.5, a fix can be applied from any skill or a plain conversational turn — the user is never blocked or forced to route through this skill. What this skill adds is structure for a reported problem: the symptom→layer triage, the dispatch-path trace, and the located-evidence diagnosis before any edit. Reach for it when something broke on device and you don't yet know why; for a planned feature change where you already know what to edit, just edit directly.

When to use:

  • "I tapped the button and nothing happened — no error, no UI." (silent no-op)
  • "The PCF shows ErrorCode: PARSE / ErrorMessage: ...." (a code to trace)
  • "The app crashes the moment the screen loads." (crash-at-launch)
  • "Host log says Loaded 0 plugin package(s)." (a native-load signature)
  • "It works on iOS but does nothing on Android." (parity / transport bug)
  • "The Done button returns the wrong data." (behavior drift)

When NOT to use:

  • No repo yet → /generate-native-extension.
  • A brand-new operation → /design-native-extension-feature, then generate.
  • A planned change with a known edit and no reported failure → just edit (any skill / chat).
  • A build that never produced a binary → the failure is a build failure; run /generate-ppmplugin and read its stage output first.

Decoupled from generate-*. This skill refuses to run if no extension repo is detected. It does NOT scaffold, install dependencies, build binaries, or assemble the bundle. It diagnoses, then edits files.


Step 0 — Verify this is an extension repo

Detect the repo in this order. Stop with BLOCKED: not an extension repo (no <X> found) if any required signal is missing.

SignalRequired?Check
PRD.md exists at repo rootYesThe spec is the baseline the observed behavior is compared against.
package.json exists at repo rootYesConfirms this is a generated extension repo, not a random directory.
ARCHITECTURE.md exists at repo rootNoStrongly preferred — holds the dispatch contract + per-op impl the trace follows. Note its absence as a concern.
.extension-state.md exists at repo rootNoInformational — prior edits / drift entries are debugging leads. Created at Step 9 if absent.
pcf/ folder with a ControlManifest.Input.xml insideNoDrives PCF detection. Use Glob under pcf/ to locate the manifest and capture `has_pcf: true
ppmplugin/ build output / a .ppmplugin artifactNoInformational — confirms a binary was built (this skill debugs built controls). Absence → the failure may be pre-build; note it.

If PRD or package.json is missing, suggest /generate-native-extension and stop.


Step 1 — Read shared docs, PRD, ARCHITECTURE, manifest, and state

In this order:

  1. shared/shared-instructions.md — constants, return-status codes (DONE / DONE_WITH_CONCERNS / BLOCKED / NEEDS_CONTEXT), safety rules.
  2. shared/error-codes.md — the canonical catalog + the symptom → likely cause → where-to-look map. This is the core input to triage (Step 3). Read it fully.
  3. shared/naming-conventions.md — maps PRD identity to file paths for the trace.
  4. shared/repo-layout.md — the expected file tree.
  5. shared/ppmplugin-format.md — the dispatch contract, the wrap sendAsync transport, the { isUpdate, message } response container, and the native-load model (§2, §5, §5b). Essential for tracing transport / load failures.
  6. ./PRD.md — full read (identity, operations, expected behavior).
  7. ./ARCHITECTURE.md — full read (SDK pin, per-op impl walkthroughs, message contract, §5 error codes, manifest impl). The trace follows this.
  8. ./manifest.json — the committed dispatch contract (name, receivers[].method, receivers[].nativeModule).
  9. ./.extension-state.md — prior ## Edits / ## Debug entries and any recorded drift — often the fastest lead.

Skip shared/prereq-check.md. Debug installs/auths nothing. If a fix later needs the PCF npm run build, Step 8 surfaces a missing toolchain then.

Per shared-instructions.md §9.2, print a one-line prereq notice at the start of Step 1:

Prereq check — /debug-extension: skipped (skill does no installs / auth / network — investigation only until a fix's smoke check).

Step 2 — Capture the bug report

Gather the symptom. If the user invoked the skill with no detail, prompt for it — ask for whichever of these they have (one consolidated prompt, not five):

  • What happened vs. what they expected (the observable behavior).
  • ErrorCode / ErrorMessage shown on the PCF (or in Power Fx via Self.ErrorCode / Self.ErrorMessage).
  • The raw <name>Json diagnostic output (the wire bytes — transport-level forensics).
  • Any host log line (e.g. Loaded 0 plugin package(s), native module '<x>' not loaded, method '<m>' not found, a stack trace).
  • Platform (iOS / Android / both) and when it happens (at launch / on tap / after the operation).
  • Repro steps, if any.

Keep the raw report in working context for the trace — do not paraphrase away detail, since an exact code or message is the highest-signal input. Do not persist it verbatim. .extension-state.md is committed to the repo, and a pasted report routinely carries a raw response, stack trace, host log lines, file paths, URLs, tokens, or customer data. Step 9 writes a redacted one-line summary instead — see the redaction rule there.


Step 3 — Triage: map the symptom to candidate layers

Using shared/error-codes.md (§2 module codes, §3 transport codes, §4 no-code signatures), classify the symptom into one or more candidate layers, most-likely first:

LayerReached when the symptom looks like…
PCF / transport (pcf/<Pascal>PCF/index.ts)PARSE, UNEXPECTED_PAYLOAD, BRIDGE_FAILED, NOT_IN_WRAP; silent no-op on tap; every call fails identically.
Dispatch contract (./manifest.json ↔ native names ↔ PCF key)method '<m>' not found, native module '<x>' not loaded, BRIDGE_FAILED with a routing message; works on one platform only.
Native module — Android (android/.../<Pascal>Module.kt)INTERNAL_ERROR / PERMISSION_DENIED / NO_ACTIVITY on Android; Android-only crash; Loaded 0 plugin package(s).
Native module — iOS (ios/RCT<Pascal>Module.m)INTERNAL_ERROR / PERMISSION_DENIED on iOS; iOS-only crash / no-op; +moduleName / requiresMainQueueSetup load issue.
Native load / lifecycle (constructor, package class)Crash at launch before any UI; module never loads.
Build config / RN pin (package.json, android/build.gradle, .podspec)React header / undefined-symbol errors; behavior tied to an SDK level; a pin divergence from the host RN.
Behavior / spec (native op body vs PRD/ARCHITECTURE)Wrong result, missing control, incorrect payload — no error code, just wrong output.

A single report can span layers (e.g. UNEXPECTED_PAYLOAD is usually PCF, but can be a non-conforming native response). List every plausible layer; Step 4 confirms/eliminates.

If the report is too thin to triage, ask one targeted clarifying question (e.g. "Does it fail on both platforms or just one?"). If still unclear, stop with NEEDS_CONTEXT: <what's unclear>.


Step 4 — Investigate: trace the path and gather evidence

For each candidate layer, read the implicated files and confirm or eliminate the hypothesis with concrete evidence. Do NOT guess — open the file and cite the line.

Convention-derived files (substitute <Pascal> / <lower> from PRD identity via shared/naming-conventions.md):

  • PCF / transportpcf/<Pascal>PCF/index.ts (invokeBridge, extractResponse, onTrigger outcome branch, args: [request], the composite key), pcf/<Pascal>PCF/ControlManifest.Input.xml.
  • Dispatch contract./manifest.json receivers[]; native getName() (Android) / +moduleName (iOS); the PCF composite key <name>/<receiver> + method. Cross-check all three agree.
  • Native Androidandroid/src/main/java/com/powerapps/<lower>/<Pascal>Module.kt (+ <Pascal>CaptureActivity.kt), the ReactPackage class (public no-arg constructor), android/src/main/AndroidManifest.xml.
  • Native iOSios/RCT<Pascal>Module.{h,m} (+moduleName, +requiresMainQueueSetup, no-arg init), the presented VC.
  • Build / pinpackage.json (RN pin 0.79.7), android/build.gradle, ios/<Pascal>Extension.podspec.

Trace techniques:

  • Follow the dispatch path end-to-end: PCF key → sendAsync envelope ({ method, args: [request] }) → manifest receivers[] → native method → response JSON → extractResponse → PCF output. A break anywhere is the bug.
  • Grep for the specific symbol in the report (an error code, a field name, a method name) across ios/, android/, pcf/ to find every site that emits or consumes it.
  • Compare iOS vs Android when the symptom is platform-specific — the delta is the lead.
  • Check the raw <name>Json against the { status, result?/error?, message? } convention and the wrap { isUpdate, message } container — a shape mismatch points to extractResponse vs a bare parse.
  • **Match against error-codes.md §4 signatures**: Loaded 0 plugin package(s)→ Android package no-arg ctor;cordova.exec` in the PCF → forbidden (silent no-op); React header errors → RN pin divergence.

Read shared/self-critique-protocol.md if the trace touches a per-operation impl — its gates (state coverage, cross-platform parity, lifecycle) sharpen the hypotheses.


Step 5 — Root-cause diagnosis + gate

Present a ranked diagnosis. Each hypothesis is anchored in evidence, not intuition:

Diagnosis for: "<verbatim symptom>"

1. [HIGH confidence] <one-line root cause>
   Evidence: <file>:<line> — <what the code does / doesn't do>
   Why it produces this symptom: <one sentence tied to error-codes.md>
   Layer: PCF | dispatch contract | native-android | native-ios | native-load | build/pin | behavior

2. [MEDIUM confidence] <alternative cause>
   Evidence: ...

Ruled out: <hypothesis> — <why the evidence eliminates it>

Recommended fix (for #1): <what would change, in which file(s)>

Gate: Proceed with the fix for #1? (yes / investigate #2 instead / show me <file> / stop).

  • On stopBLOCKED: user stopped after diagnosis (nothing edited; diagnosis logged at Step 9).
  • On investigate #2 → deepen that hypothesis, re-present.
  • If no hypothesis reaches at least MEDIUM confidence after the trace → stop with NEEDS_CONTEXT: <what additional evidence is needed> (e.g. "please paste the raw <name>Json output" or "a host log line from the crash"). Never fabricate a fix for an unconfirmed cause.

Step 6 — Fix plan + spec-vs-drift check + gate

Now derive the fix. First classify it the same way a careful edit does, because a fix can be more than a code patch:

CaseWhat the fix isAction
B — code drift (most common)The code diverged from a spec that is already correct (e.g. a missing extractResponse, a wrong composite key, a swallowed exception).Fix the code only. Print <doc> §<n> already specifies the correct behavior — fixing code only.
A — spec wrongThe observed behavior is actually what PRD/ARCHITECTURE currently says, but that spec is wrong.Propose the PRD/ARCHITECTURE edit first (its own mini-gate), apply it, then derive the code.
C — bothSpec is ambiguous/partial and code is partial.Update the doc detail, then fix the code.

Then present the code fix plan:

Fix plan:

<path/to/file>
  - Replace: <specific symbol / region> → <replacement> (rationale tied to the diagnosis)
  - Add:     <specific addition>

Contract impact: <"none" | "method set / receiver / nativeModule moves — ./manifest.json + PCF key updated in this same change, re-staged via /generate-ppmplugin-manifest">
Total: N files changed.
Apply? (yes / no / show me <file>)

Contract seam. If the fix changes the method set, the receiver/routing name, or the native-module name (Android getName() / iOS +moduleName = <Pascal>Module), three artifacts move together in this fix: native source, the committed ./manifest.json (receivers[] / methods, edited surgically), and the PCF composite key <name>/<receiver>. Then /generate-ppmplugin-manifest re-validates + re-stages the manifest. A pure-behavior fix that leaves those unchanged does not touch the manifest.

If the fix requires PCF edits but has_pcf: false, stop with BLOCKED: this fix requires PCF edits but pcf/ is not scaffolded — run /generate-pcf-companion first.

Gate: wait for explicit yes. On noBLOCKED: user declined fix plan (diagnosis still logged). On show me <file> → print the proposed content and re-ask.


Step 7 — Apply the fix

Apply the planned edits with the Edit tool. Rules:

  • Surgical, not wholesale. Change the lines the diagnosis identified; don't rewrite the function or file. Reserve Write for a genuinely new file (rare in debug).
  • Atomic per file. Apply all edits to one file in sequence; never leave a file half-edited.
  • Atomic across files (best effort). If a multi-file batch fails mid-way, stop, report which files were written and which weren't, and tell the user to git diff / git checkout the half-written ones. Do NOT auto-revert (destructive, not on the safe list).
  • No collateral edits. Only touch files on the plan. Note unrelated issues in the summary; don't fix them in this pass.

Step 7.5 — Self-critique against the proactive protocol

After applying, re-read every touched file and walk shared/self-critique-protocol.md. A fix that resolves the reported symptom can introduce a new one (fixing an Android crash by deferring init might leave a first-call race; correcting the composite key might orphan an output).

  1. Re-read each edited file fresh from disk (not from memory).
  2. Walk the gates — PRD coverage, user journey, layout, state, cross-platform parity, reversibility, lifecycle, spec-drift, plus the 3P Gates 10 (buildability / bundle-fit) and 11 (PCF↔native round-trip). Pay special attention to Gate 11 — most debug fixes touch the very round-trip that broke.
  3. Report + apply fixes per the protocol's severity/autofix cadence: mechanical fixes in one batch (one yes); structural fixes each gated; judgment calls surfaced as concerns. Re-loop up to 3 iterations.

Return-status impact: all gates clean → continue. Blockers deferred → BLOCKED: self-critique blockers — <list> (fix stays applied; user re-runs after deciding). Concerns remain → continue with DONE_WITH_CONCERNS.


Step 8 — Verify the fix

Verify the fix actually addresses the symptom, scoped to what was edited:

Validate before you interpolate. <Pascal> comes from PRD identity, not from a constant — a crafted or malformed value turns the command below into arbitrary shell or escapes the project directory. Before running it: require <Pascal> to match ^[A-Za-z][A-Za-z0-9]*$ (no separators, dots, or path segments), resolve pcf/<Pascal>PCF and confirm the real path stays inside pcf/, then pass it as a single quoted argument rather than splicing it into shell syntax. On failure, STOP with BLOCKED: refusing to run a build command with an invalid <Pascal> value — <value>.

Files editedVerificationWhy
Any pcf/<Pascal>PCF/ .ts / ControlManifest.Input.xmlnpm run build --prefix "$PCF_DIR"The only TS build in the repo — catches type + manifest errors immediately.
Only .kt / .m / XMLPrint: Native files fixed — compile + on-device validation defer to /build-android-binary // /build-ios-binary (via /generate-ppmplugin) and /test-native-extension Layer 5. Rebuild + retest on device to confirm the symptom is gone.Native standalone compile isn't reliable here; the build skills do the real compile.
Contract moved (./manifest.json / names)Re-run /generate-ppmplugin-manifest (re-validate + re-stage), then note that /generate-ppmplugin (rebuild + /audit-ppmplugin) is needed.The staged manifest and the binary must be regenerated for the fix to reach the device.

The definitive verification for a field bug is a rebuild + on-device retest — a passing smoke check confirms the fix compiles, not that the symptom is gone. Say so explicitly in the summary. On smoke-check failure: report the failing command + the most relevant error line, do NOT auto-revert, stop with BLOCKED: smoke check failed — <one-line cause> (still log at Step 9).


Step 9 — State log + summary

9.1 Update .extension-state.md

Append (don't overwrite) to a ## Debug section (create it if absent):

## Debug

- <ISO timestamp> — <one-line summary of the bug + fix>
  - Symptom: "<redacted one-line summary — see the redaction rule below>"
  - Root cause: <located cause> (<file>:<line>)
  - Diagnosis case: A | B | C
  - Docs changed: <sections, or "none">
  - Code changed: <file paths>
  - Contract moved: <"none" | "receiver/method/nativeModule changed — ./manifest.json updated; re-staged via /generate-ppmplugin-manifest">
  - Verification: <PCF npm run build → PASS | native — rebuild + device retest required | etc.>
  - Status: DONE | DONE_WITH_CONCERNS: <reasons> | BLOCKED — <reason>

Redact before writing. .extension-state.md is committed to the repo, so treat every field as published. The Symptom line is a short paraphrase — the error code, the affected operation, and the observable behaviour — never the pasted report. Strip, from every field: secrets and tokens; PII and customer data; request/response payloads and their fragments; absolute or internal filesystem paths; internal URLs and hostnames; and stack traces beyond the single frame that locates the cause. Keep Root cause to the repo-relative <file>:<line> that already lives in source control. If a detail is needed to justify the fix but can't be redacted safely, leave it out of the file and keep it in the chat.

9.2 Final summary + next step

One paragraph: the located root cause, what was fixed, and the verification outcome — and state plainly that an on-device retest (after a rebuild) is what confirms the symptom is resolved. Then offer the next step via AskUserQuestion (shared-instructions §9.1), picking the options that fit the fix:

  • Run /generate-ppmplugin — rebuild the .ppmplugin + re-audit (re-validates + re-stages ./manifest.json first). The recommended next step for a native or contract fix.
  • Run /test-native-extension — re-validate the contract (Layer 0 cross-check; Layer 4 PCF compile). Good for a PCF or contract fix before the full rebuild.
  • Run /generate-pcf-companion — only if the fix needs PCF edits but pcf/ isn't scaffolded.
  • Stay — I'll retest on device first.

Per the Execute, don't describe HARD RULE (§9.1), when the user picks a Run /… option, invoke that skill via the Skill tool in the same turn. Don't auto-chain on your own.


Hard rules

  1. Never fix without a located cause. Every fix traces to file:line evidence from Step 4/5. No confirmed cause → NEEDS_CONTEXT, not a speculative patch.
  2. Two gates: diagnosis, then fix plan. Never edit code silently. On a case-A/C spec fix, the doc edit gets its own mini-gate first.
  3. Never blow away unrelated files. Only files on the Step 6 plan are touched — no drive-by refactors.
  4. Never auto-revert on failure. Surface it, stop; the user reviews git diff.
  5. File-edit policy — three categories (identical to the generate/edit discipline):
    • Tool-managed — NEVER edit: .git/, lockfiles (pnpm-lock.yaml, package-lock.json, Podfile.lock), the generated bundle + its staging (ppmplugin/staging/manifest.json, ppmplugin/ outputs, any .ppmplugin), PCF generated artifacts (pcf/<Pascal>PCF/generated/), build outputs (lib/, dist/, build/, pcf/<Pascal>PCF/out/), *.bak.*, .claude/. (The committed ./manifest.json at repo root is the opposite — a consumer site you DO edit when the contract moves.)
    • Skill-managed — updated only via the canonical state step: .extension-state.md (this skill's Step 9). No mid-flow direct edits.
    • User-editable on request: .gitignore, CHANGELOG.md, LICENSE, README.md, PCF eslint.config.js / tsconfig.json, and all source (ios/**, android/**, pcf/<Pascal>PCF/{index.ts,ControlManifest.Input.xml}).
  6. Atomic per file, best-effort across files.
  7. Contract stays consistent. If a fix moves the method set / receiver / nativeModule, ./manifest.json + native + PCF move together, then re-stage via /generate-ppmplugin-manifest. Verify with /test-native-extension Layer 0.
  8. Spec and code stay in sync. A case-A/C fix updates PRD/ARCHITECTURE first; a case-B fix logs the drift as such. Never silently update the spec to match a bug.
  9. PCF is auto-detected, never assumed. No pcf/<Pascal>PCF/ControlManifest.Input.xml → don't write to pcf/; route to /generate-pcf-companion if the fix needs it.
  10. Don't auto-chain; do honor an explicit pick at Step 9.2.
  11. A smoke check is not an on-device confirmation. Always tell the user the fix must be rebuilt and retested on device to confirm the field symptom is gone.

Scenarios — how the flow plays out

Scenario 1 — Silent no-op on Android (PCF transport bug)

Step 2: Symptom = "tap does nothing, no error, only on Android."
Step 3: Triage → PCF/transport (error-codes.md §4 top row) + dispatch contract.
Step 4: Read pcf/.../index.ts — invokeBridge calls cordova.exec directly, no sendAsync.
        Evidence: index.ts:NN. Matches the §4 signature (cordova undefined in PCF sandbox).
Step 5: [HIGH] cordova.exec used instead of the host-injected sendAsync → silent no-op,
        worst on Android. Gate: proceed.
Step 6: Case B (ppmplugin-format §2 already specifies sendAsync). Fix plan: replace
        cordova.exec with window.PowerApps.NativeExtension.sendAsync + args:[request].
Step 7: Apply. 7.5: self-critique Gate 11 (round-trip) clean.
Step 8: cd pcf && npm run build → PASS. Note: rebuild PCF + retest on device.
Step 9: Log case B; suggest /test-native-extension then /generate-pcf-companion publish path.

Scenario 2 — Crash at launch (native load)

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-extension
Source
github.com/microsoft/power-platform-skills