Labsco
microsoft logo

create-mobile-app

โœ“ Officialโ˜… 413

by microsoft ยท part of microsoft/power-platform-skills

Use when the user wants to start a new Power Apps mobile app (Expo / React Native / TypeScript, targeting iOS and Android) from scratch.

๐Ÿงฉ One of 7 skills in the microsoft/power-platform-skills package โ€” works on its own, and pairs well with its siblings.

This is the playbook your agent receives when the skill activates โ€” you don't need to read it to use the skill, but it's here to audit before installing.

๐Ÿ“‹ Shared instructions: shared-instructions.md โ€” read first. Covers safety guardrails, memory bank usage, preferred-environment policy, connector-first rule, Windows CLI compat, command-failure handling.

Create Power Apps Code App (Native)

Top-level orchestrator. Owns the user-visible flow; delegates planning to the native-app-planner agent and per-domain mutation to dedicated /add-* skills.

Workflow

  1. Resume check + fresh-template gate โ†’ 1. Prerequisites โ†’ 2. Gather requirements โ†’ 2b. Requirements discovery โ†’ 2c. Plan preview (rough cost + abort gate) โ†’ 3. Plan (planner agent + 4 gates) โ†’ 4. Auth & environment โ†’ 5. Prepare existing template โ†’ 6. npx power-apps init โ†’ 6.5 verify npm install โ†’ 6.5b SafeAreaProvider gate (always runs, idempotent) โ†’ 6.6 scaffold tsc smoke check โ†’ 6.7 seed memory bank โ†’ 6.85 Offline profile (always asked) โ†’ 7. Auth config โ†’ 8. Apply data model โ†’ 9. Apply native capabilities โ†’ 9a. Install planned JavaScript dependencies โ†’ 9b. Design system โ†’ 10. Add connectors โ†’ 10b. Wire navigation layout โ†’ 11. Build screens (parallel) โ†’ 11.4 Stylistic fix sweep โ†’ 12. Start Metro (npm run dev) โ†’ 12.5 Optional debug handoff โ†’ 13. Summary

Fresh-template working-directory mode

This skill assumes the user already has a fresh microsoft/power-platform-skills/plugins/mobile-apps/template#main template materialized with degit in the target working directory and has already run npm install there. The skill turns that fresh template into an app; it does not clone, degit, or copy a template itself.

Fresh template required. If the working directory is not a template, or if it already looks like an app created by this skill, STOP and tell the user to materialize a fresh microsoft/power-platform-skills/plugins/mobile-apps/template#main template with degit into a new folder, run npm install, then rerun /create-mobile-app --working-dir <fresh-template-dir>.

Use these markers:

StateDetectionAction
Fresh templatepackage.json, app.config.js, auth.config.json, tamagui.config.ts exist; node_modules/expo exists; memory-bank.md, native-app-plan.md, .datamodel-manifest.json, and generated Dataverse services are absentProceed.
Template not installedFresh-template files exist but node_modules/expo is absentSTOP: ask user to run npm install in the template folder, then rerun. Do not provision ADO npm tokens here.
Already-created appmemory-bank.md, native-app-plan.md, .datamodel-manifest.json, or src/generated/services/*.ts existsSTOP: this is not a fresh create target. Ask user to materialize a fresh template folder with degit.
Not templateRequired template files are missingSTOP: ask user to materialize microsoft/power-platform-skills/plugins/mobile-apps/template#main into the working directory with degit and run npm install.

This gate is intentionally simple: /create-mobile-app creates a new app from a fresh template. It does not adopt, repair, resume, or overwrite an already-created app.


TypeScript Gate Policy โ€” no quality compromise

tsc is a phase gate, not a reflex after every tiny edit. The app may not advance past a gate until TypeScript is clean.

Required gates:

  • Scaffold gate: Step 6.6 after existing-template preparation, npx power-apps init, and dependency verification.
  • Dataverse/generated-services gate: immediately after Step 8 returns and generated services/models are refreshed.
  • Navigation/skeleton gate: after Step 10b layouts and Step 10.8 shared code/skeletons are written, before Step 11 builders launch.
  • Screen-wave gate: after each Step 11 screen-builder wave returns, before launching the next wave.
  • Final gate: before Step 12 starts the dev server.

When a gate fails:

  1. Capture the full tsc --noEmit output once.
  2. Classify errors by root cause (for example: generated model names, service option shapes, invalid UI props, typed percentage values, create/update payload typing, missing imports).
  3. Repair in a batch.
  4. Re-run the same gate once after the batch.
  5. Continue only when the gate is clean, or stop/block according to the retry policy.

Do not run full-app tsc after every microscopic local edit inside the same repair pass. That is slower and encourages line-by-line patching. Batch root-cause fixes, then re-run the gate. This is a speed improvement only; it does not lower the quality bar.

Hard stops:

  • Do not launch data-source work from a broken scaffold gate.
  • Do not launch screen-builders from broken generated services, layouts, shared code, or skeletons.
  • Do not launch wave N+1 until wave N passes its tsc gate.
  • Do not start the dev server until the final gate is clean.
  • Do not hide approved capability failures behind mocks or TODOs just to satisfy tsc.

Step 0 โ€” Resume check + fresh-template gate

Telemetry checkpoint: validate_fresh_template

If $ARGUMENTS includes a --working-dir (or the user names an existing directory), check whether <working_dir>/memory-bank.md exists.

  • Bank present โ†’ read it. Identify the highest-numbered completed step. Inform the user:

    "Found existing project '' at <dir>. Steps 1โ€“ already completed (last update ). Resume from Step <N+1>?" Wait for confirmation. If the user says yes, jump to that step. Skip the wizard (Step 2) and re-use the values stored in the bank.

  • Bank absent โ†’ fresh project. Continue to Step 1.
  • Bank present but corrupted (missing required headings) โ†’ surface the parse error, ask the user whether to overwrite (lose history) or fix manually before proceeding.

The bank is the only resume mechanism. Do not infer resume state from package.json or node_modules/ โ€” those can lie.

After the resume check, run the fresh-template gate from the section above. This is a create-only command:

  • If memory-bank.md exists and the user confirms resume, resume as documented above.
  • If any already-created-app marker exists and there is no approved resume path, STOP and tell the user to materialize a fresh template into a new folder with degit.
  • If required template files are missing, STOP and tell the user to materialize microsoft/power-platform-skills/plugins/mobile-apps/template#main into the working directory with degit and run npm install.
  • If node_modules/expo is missing, STOP and tell the user to run npm install in that template folder before rerunning this skill.

Do not silently copy a bundled template over the user's folder. A fresh plugins/mobile-apps/template template may contain placeholder power.config.json with an empty environmentId; Step 5 removes that placeholder immediately before Step 6 runs npx power-apps init.

Step 1 โ€” Prerequisites

Telemetry checkpoint: validate_development_toolchain

Run all checks first โ€” no point gathering requirements if the toolchain isn't ready.

Important: npm auth and Power Platform app auth are separate. The account used for npm install can be different from the account used by npx power-apps:

WhatUsesTypical account
npm install private feed accessnpm/Azure Artifacts auth configured outside this skillAccount with feed Reader access
npx power-apps init, Dataverse, deploynpx power-apps browser auth + az login --tenant <env-tenant> for Dataverse helper scriptsPower Platform environment account, often a test-tenant/admin account

Renewing npm feed auth does not sign the user into npx power-apps. If the Power Apps CLI prompts for browser auth later, that is expected and unrelated to the npm/ADO feed token.

Then run the checks:

node --version                                      # v22+
npm  --version                                      # v10+
az account show --query "user.name" -o tsv          # Azure CLI logged in (needed for Dataverse helper scripts)
git --version                                       # optional

Do NOT probe Xcode, Java, Android Studio, or CocoaPods here. This plugin's flow is plan โ†’ scaffold โ†’ code โ†’ local Expo dev server. Build + deploy (npm run build / npx power-apps push) is a separate user-driven step via the /deploy skill. Local native compile is the user's choice and lives outside this skill (run the platform-specific native command directly when needed). See shared/version-check.md โ€” only the Always required tier matters here.

MissingAction
Node < 22STOP โ€” instruct nvm install 22 && nvm use 22
azSTOP โ€” instruct az login

Template-only rule: this skill no longer provisions npm feed tokens, PAT fallbacks, vendor fallbacks, or registry rewrites. The user must run npm install in the fresh template folder before invoking /create-mobile-app.

Capture target Power Platform environment for the remaining flow.

Source of truth for env selection: the generated power.config.json first, explicit environment ID second. In the normal template-folder flow, npx power-apps init runs first and writes the selected environment ID into power.config.json; read that ID and pass it to scripts/resolve-environment.js to resolve the Dataverse URL and tenant. If power.config.json is missing or has an empty placeholder environmentId, ask for an environment ID. A Dataverse URL is useful as a resolver fallback for existing apps, but it is not enough for npx power-apps init because init needs --environment-id.

StepSourceWhen user is asked
0. power.config.json has environmentIdscripts/resolve-environment.js <environment-id>Never โ€” automatic after npx power-apps init
1. User supplies env IDscripts/resolve-environment.js <environment-id>Ask only if power.config.json is missing/empty or user wants a different env
2. User wants a different accountFollow shared-instructions standalone CLI auth handlingOnly if resolution/token acquisition fails or user asks
3. User wants different envAsk for another env ID and re-run resolverOnly if user selects "use a different environment" at Step 2
4. npx power-apps init -t MobileApp --display-name "$DISPLAY_NAME" --environment-id $ACTIVE_ENV_ID --non-interactivePersists choice into power.config.jsonOnly when this skill owns the initial init path
TARGET_ENV="<environment-id-or-empty>"
if [ -z "$TARGET_ENV" ] && [ -f power.config.json ]; then
  TARGET_ENV=$(node -e "try { const id=require('./power.config.json').environmentId || ''; console.log(id); } catch { console.log(''); }")
fi
test -n "$TARGET_ENV" || { echo "โœ— Environment missing. Provide an environment ID."; exit 2; }
ENV_JSON=$(node "${PLUGIN_ROOT}/scripts/resolve-environment.js" "$TARGET_ENV")
printf '%s\n' "$ENV_JSON" > .resolved-environment.json
ACTIVE_ENV_ID=$(node -e "const j=JSON.parse(process.argv[1]); console.log(j.environmentId || '')" "$ENV_JSON")
ACTIVE_ENV_NAME=$(node -e "const j=JSON.parse(process.argv[1]); console.log(j.displayName || j.environmentUrl || '')" "$ENV_JSON")
ACTIVE_ENV_URL=$(node -e "const j=JSON.parse(process.argv[1]); console.log(j.environmentUrl || '')" "$ENV_JSON")
ACTIVE_TENANT_ID=$(node -e "const j=JSON.parse(process.argv[1]); console.log(j.tenantId || '')" "$ENV_JSON")
test -n "$ACTIVE_ENV_ID" || { echo "โœ— Environment ID missing. Provide the environment ID directly."; exit 2; }
echo "โœ“ Target env: $ACTIVE_ENV_NAME ($ACTIVE_ENV_ID)"
echo "โœ“ Target env URL: $ACTIVE_ENV_URL"
echo "โœ“ Target tenant: ${ACTIVE_TENANT_ID:-unknown}"

Orchestrator handling for exit 2: ask the user for their environment ID directly, then re-run the capture block above. Do not run npx power-apps init here; Step 6 owns initialization after the user confirms the target environment.

Stash $ACTIVE_ENV_ID, $ACTIVE_ENV_NAME, $ACTIVE_ENV_URL, and $ACTIVE_TENANT_ID for Step 2 (env confirmation), Step 6 (npx power-apps init), and Step 7 (auth.config.json tenant/environment cache). If parsing fails, ask for an environment ID again.

If resolve-environment.js cannot get tokens, run az login --tenant <env-tenant> in the foreground. If npx power-apps init later uses the wrong account, follow shared-instructions standalone CLI auth handling and retry once.

Step 1.7 โ€” Detect publisher prefix

Detect the publisher prefix for the env's Default solution so the planner uses the correct prefix rather than assuming cr_.

Deferred execution: do not run the query at this point. Step 2b.4 first classifies the run as required or connector-only; only required runs execute the block below. Connector-only runs set $DETECTED_PUBLISHER_PREFIX = "" and make no Dataverse prefix query.

node "${PLUGIN_ROOT}/scripts/detect-publisher-prefix.js" "$ACTIVE_ENV_URL" --tenant-id "$ACTIVE_TENANT_ID"

Output is one line of JSON, e.g.:

{"prefix": "cr8142a", "source": "detected"}
{"prefix": null, "reason": "no token (run `az login --tenant <env-tenant>`)"}

The script queries the Default solution's publisher via: /api/data/v9.2/solutions?$select=uniquename&$expand=publisherid($select=customizationprefix)&$filter=uniquename eq 'Default'

A second solution name can be passed as a second argument if the env uses a different solution (defaults to 'Default').

Token tenant note: the script's getAuthToken discovers the env's tenant ID from the Dataverse HTTPS auth challenge and passes --tenant <env-tenant> to az, so detection works even when the active az identity is on a DIFFERENT tenant. If the user has not run az login --tenant <env-tenant> at any point, detection may return null.

Stash the result for Step 3 (planner spawn):

OutputStash asBehavior at Step 3
{"prefix": "cr8142a", ...}$DETECTED_PUBLISHER_PREFIX = "cr8142a"Pass to planner prompt as a fact: "Publisher prefix (detected from env): cr8142a_"
{"prefix": null, ...}$DETECTED_PUBLISHER_PREFIX = "" (empty)Pass to planner as: "Publisher prefix: NOT DETECTED โ€” use placeholder cr_ and warn the user that Dataverse will normalize the actual prefix at create time."

Do NOT block on null detection โ€” the user can still proceed; the Power Apps CLI normalizes prefixes when npx power-apps add-data-source runs. The detection step is purely to make the plan output accurate.

If the script exits non-zero (rare โ€” should always exit 0 with prefix: null), treat it as the null case and continue.

Step 2 โ€” Gather requirements

Telemetry checkpoint: gather_app_requirements

Skip questions the user already answered in $ARGUMENTS.

If the user gave no description, ask one open-ended question first:

"What would you like to build? Describe it in your own words โ€” what it does, who uses it, and what problem it solves."

Then collect with AskUserQuestion (batch where possible):

QuestionDefault
App display namederived from description
Target platformsios, android (multi-select, default both)
Aestheticminimal / playful / professional / matches existing brand
Target environmentConfirm <ACTIVE_ENV_URL> / <ACTIVE_ENV_ID> from Step 1.6, or choose "use a different environment" and provide another environment ID

App slug is auto-derived from the display name (slugify(displayName) โ€” kebab-case, ASCII-only, strip non-alphanumerics). Do NOT ask the user; the derived slug is correct >95% of the time. Show the resolved slug as part of Step 2c's plan preview so the user can override via edit if needed.

Environment override branch: If the user picks "use a different environment", ask for the Power Platform environment ID via AskUserQuestion, then run scripts/resolve-environment.js again and refresh $ACTIVE_ENV_ID / $ACTIVE_ENV_URL / $ACTIVE_TENANT_ID.

App-name collision pre-flight. Once <displayName> is fixed, check the chosen env for a name collision:

npx power-apps list-codeapps --environment-id "$ACTIVE_ENV_ID" --json 2>/dev/null | grep -F "<displayName>" >/dev/null && \
  echo "COLLISION" || echo "OK"

If COLLISION, ask the user via AskUserQuestion:

"An app named <displayName> already exists in <ACTIVE_ENV_NAME>. Choose:

  1. Pick a different name (recommended)
  2. Delete the existing app in Maker portal โ€” DESTRUCTIVE, asks confirmation outside this skill
  3. Continue anyway (bg npx power-apps init will fail; you'll have to rename later โ€” NOT recommended)"

Re-prompt for name if (1). If (2), send the user to Maker portal to delete the existing app, then re-run the collision check. Only proceed once collision is resolved.

If npx power-apps list-codeapps is unavailable in the installed CLI version, skip the pre-flight silently and continue.

Don't enter plan mode here โ€” that's the planner agent's job in Step 3.

Step 2b โ€” Requirements discovery

Goal: Turn the user's thin prompt into a confirmed feature brief before the planner runs. The planner agent receives this brief verbatim โ€” richer input means better data model inference, accurate connector detection, and correct screen specs.

Step 2b.0 โ€” Prompt richness scoring (decides which path to take)

Before asking anything, score the description on four signals. The score decides whether we ask a multi-select feature picker, a single confirmation, or skip the discovery question entirely.

Run this scorer mentally on <description> (the prompt the user gave with /create-mobile-app, plus any clarifying text from Step 2a). Count how many of the four trip:

SignalTrips when
Word countdescription has โ‰ฅ 60 words
Distinct nounsdescription names โ‰ฅ 5 distinct domain nouns (people, things, documents, places โ€” e.g. "inspector", "aircraft", "gate", "defect", "evidence")
Action verbsdescription uses โ‰ฅ 3 workflow verbs from this set: log, track, submit, assign, notify, scan, upload, approve, verify, complete, capture, override, dispatch, review, sign
Domain phrasedescription names a known industry domain โ€” match against the industry table in shared/references/universal-patterns.md (airline, hospital, retail, manufacturing, field-service, finance, logistics, โ€ฆ) OR explicitly says "field operations" / "ground operations" / "site visit" / similar

Tier the result:

ScoreTierWhat to do
4 / 4auto-planSkip both questions. Extract the brief silently from <description>, write native-app-plan.md placeholder, fall through to Step 2c. The user's next interaction is the cost-estimate gate.
3 / 4one-tapSkip the multi-select. Extract the brief, show it once, ask only "Look right? (yes / adjust)". On yes โ†’ Step 2c. On adjust โ†’ fall through to walk-through.
โ‰ค 2 / 4walk-throughCurrent behaviour. Run the multi-select feature picker described in Step 2b.1, then the brief confirmation.

Print the chosen tier so the user knows which path is running:

"โ†’ Prompt richness: 4/4 โ€” skipping discovery questions, extracting brief and going straight to the plan-cost preview." (or 3/4 / โ‰ค2/4 with the matching path name)

--full-discovery escape hatch: if $ARGUMENTS contains --full-discovery, force walk-through regardless of score. Use this in dogfood runs where you want to exercise the multi-select path.

--no-discovery escape hatch: if $ARGUMENTS contains --no-discovery, force auto-plan regardless of score. Use this for fully-headless runs from the wrapper templates repo.

Step 2b.1 โ€” Walk-through path (only when tier = walk-through)

Read references/requirements-discovery.md. Infer context-aware options from the user's description, ask exactly one structured AskUserQuestion, and never use markdown checkboxes in the question text.

Wait for the user's response. Summarize their answers into a requirements brief โ€” 4โ€“8 bullet points covering what users can do, what data is tracked, and integrations.

Confirm once:

"Here's the brief I'll use for planning: โ€ข (bullet 1) โ€ข (bullet 2) ... Look right? (yes / adjust)"

Store the confirmed brief as <requirements_brief>. This replaces the thin $ARGUMENTS as the primary input to the planner.

Step 2b.2 โ€” One-tap path (tier = one-tap)

Skip the multi-select question. Extract a 6โ€“10 bullet brief directly from <description> covering: user roles, key entities, primary workflow, severity / status enums if present, integrations / connectors, native capabilities, and any explicit constraints. Show it with a single confirm:

"Your description is detailed enough to skip the feature picker. Here's the brief I extracted: โ€ข (bullet 1) โ€ข (bullet 2) ... Look right? (yes / adjust / start over)"

  • yes โ†’ store as <requirements_brief>, fall through to Step 2c.
  • adjust โ†’ drop to Step 2b.1 (walk-through) so the user can edit via the multi-select.
  • start over โ†’ return to Step 2a and re-prompt for the description.

Step 2b.3 โ€” Auto-plan path (tier = auto-plan)

Skip both the multi-select AND the brief confirmation. Extract the brief silently and store it as <requirements_brief>. Print it as a transparency log only:

"โ†’ Auto-plan tier (4/4). Extracted brief from your description: โ€ข (bullet 1) โ€ข (bullet 2) ... โ†’ Going straight to the plan-cost preview (Step 2c). The brief above is locked in unless you abort there."

Do not ask for confirmation here โ€” the user agreed to this when their prompt scored 4/4. The plan-preview gate at Step 2c remains in force as the last cheap exit before any side effects.

Step 2b.4 โ€” Common to all paths

Auto-proceed after yes (or after auto-plan transparency log). Fall through directly to Step 2c (plan preview). Do NOT add a separate "Proceed to planning?" prompt โ€” the brief confirmation IS the planning go-ahead. The only abort gate after this is Step 2c's proceed/edit/abort block, which is intentionally distinct because it shows the rough cost estimate.

Classify Dataverse planning before Step 2c and stash <dataverse_planning_mode>:

  • connector-only only when every record source and write target is an explicit non-Dataverse connector/system of record, and the app needs no app-owned persistent rows, Dataverse offline data, retained File/Image artifact, existing Dataverse table, or Dataverse-backed native capability.
  • required for every other case, including ambiguity. Do not infer connector-only merely because the brief names a connector.

Also stash <exact_target_facts_required> = yes when planning depends on any existing/standard/managed table, reuse or extension decision, proposed-name collision decision, relationship target, computed column, or target customizability fact. Otherwise set it to no. This flag controls only safe planning degradation; it never relaxes /add-dataverse reconciliation.

Now execute the deferred Step 1.7 publisher-prefix detection only when <dataverse_planning_mode> = required. For connector-only, set $DETECTED_PUBLISHER_PREFIX = "", print โ†ท Publisher-prefix discovery skipped โ€” connector-only planning., and do not call detect-publisher-prefix.js.

Design decisions are deferred to Step 6.75 โ€” /design-system (ships with this plugin) handles brand inputs, the style picker, and visual companion preference in one flow after the project is scaffolded. Do NOT ask design questions here.

Set tentative defaults (used by Step 3b before /design-system runs):

  • <visual_companion> = yes โ€” open _plan_preview.html in browser at Gate 4 by default. /design-system at Step 6.75 may downgrade this to no (path (d) in its cost picker), persisted to memory-bank for future runs.
  • <design_vibe_opt_in> = deferred โ€” Step 6.75 sets the real value. While deferred, the planner does NOT prompt for a direction; it writes a placeholder ## Design Direction: <deferred โ€” set by /design-system> block so screen-planner can still run.

--no-design escape hatch. For headless / token-constrained runs, set --no-design in $ARGUMENTS. It forces <visual_companion> = no, skips the style-picker handoff at Step 3a entirely, and short-circuits Step 6.75 to a no-op (placeholder block stays in native-app-plan.md; screen-builders fall back to industry-inferred defaults).

Step 2c โ€” Plan preview (rough, always shown)

Goal: Give the user a cheap exit before any mutation happens. This is the last point in the flow with zero side effects โ€” no git clone, no npm install, no npx power-apps init, no agent tokens spent on planning. After Step 3 starts, every abort gets more expensive (half-written native-app-plan.md, partial _screens_section.md, architect tokens already burnt).

Always runs. There is no --no-preview flag in v0 โ€” we need calibration data (~10+ runs with recorded estimate-vs-actual) before we can trust the rough estimates enough to let users skip them. Once the data shows estimates are reliably within ยฑ50%, evaluate adding a skip flag for repeat-user workflows.

Compute the estimates from inputs already in hand (no agent spawn โ€” pure heuristics on the confirmed brief and the wizard answers):

OutputInput proxyComputationConfidence
TablesDistinct nouns in confirmed briefcount(unique_nouns) ร— [0.7, 1.3] roundedlow โ€” architect may merge or split
ConnectorsStep 2b inferred connector listlen(inferred) (already exact)high
ScreensConfirmed features in briefcount(features) ร— [2, 3]low โ€” depends on navigation choice
Planning minTables + screenslower bound max(10, tables ร— 0.3 + screens ร— 0.4 + 2); upper bound max(15, computed upper)low โ€” protects the quality-first Gate 1 budget
Scaffold minFixed1-2 (template preparation + npm install already happened before skill invocation)high
Build minScreens, parallel cap of 5ceil(screens / 5) ร— 0.6medium
Extra prompts<industry_confidence> + <design_vibe_opt_in>+1 if low-confidence industry; +1 if vibe-opt-in == yeshigh

Print the block once, exactly in this format (substitute computed values; ranges as low-high):

โ”€โ”€โ”€ Plan preview (rough) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Based on your confirmed brief, before any agent runs:

Scope (proxy estimates โ€” actual numbers come from architects):
  Tables       ~<low>-<high>      โ† from <N> nouns in brief; architect may merge/split
  Connectors    <N> inferred      โ† <comma-separated names>  (confirm at Gate 3)
  Screens     ~<low>-<high>       โ† from <N> features ร— ~2-3 screens each
  Approval gates  4               โ† fixed (data model, native, connectors, screen plan)

Time (rough โ€” agent time only, excludes your approval latency at gates):
  Planning      ~<low>-<high> min โ† includes the quality-first 10โ€“15 min data-model target; approvals add latency
  Scaffolding   ~1-2 min          โ† validates prepared template + runs power-apps init
  Screen build  ~<low>-<high> min โ† parallel, capped at 5 concurrent

Token tier: Opus everywhere in v0 (model routing not yet shipped).

โš  These are proxies, not measurements:
  โ€ข Table count is "noun count in brief" โ€” architect may collapse or split
  โ€ข Time excludes your approval latency at the 4 gates
  โ€ข If industry inference is low-confidence, +1 picker prompt
  โ€ข If you opted into the design vibe picker, +1 prompt + planner re-spawn
  โ€ข If any gate is rejected, that section regenerates (~2-3 min each)

Proceed, edit brief, or abort? [proceed/edit/abort]
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

Three-option exit:

User answerAction
proceed (or empty / Enter)Continue to Step 3. Default.
editJump back to Step 2b. Re-confirm the brief with the user's changes. After 2b re-confirms, return here for a fresh preview. No working dir mutations โ€” Step 2c runs before mkdir -p <working_dir> in Step 3.
abortPrint "Aborted at Step 2c. No files created. Re-run /create-mobile-app when ready." and exit cleanly. No working dir, no memory bank, no scaffold.

Why "always show" is correct in v0 (do not skip without explicit user request):

  • Cost when user proceeds: ~30s (read + decide). Token cost ~500/run = ~$0.008.
  • Cost when user aborts late (after Step 3 starts): 5-10 min + dirty working dir + frustration.
  • Asymmetry: bounded 30s vs unbounded 30 min. Always show the bounded cost.
  • Forced calibration: every run produces the <estimate, actual> data we need for v0.x model routing decisions. Skipping drops calibration data.

Set expectations before handing off to the planner:

"Brief locked in. Planning surfaces 4 approval prompts (data model โ†’ native capabilities โ†’ connectors โ†’ screens). Data-model readiness is quality-first, with a 10โ€“15 minute target: โ€ข Gate 1 (data model) โ€” budget 10โ€“15 min for verified reuse/extend/create decisions, ER columns, relationships, tiers, and risks โ€ข Gate 2 (native capabilities) โ€” ~10s (quick) โ€ข Gate 3 (connectors) โ€” ~30โ€“60s โ€ข Gate 4 (screens + design) โ€” 3โ€“8 minutes (this is the heavy one: design vibe picker if opted in, then per-screen specs and HTML preview generation)

For Dataverse-required apps, factual foreground milestones will show environment, inventory, candidate, detail, and timing counts within 30 seconds. Connector-only apps skip those metadata milestones. While the architect runs, new milestone IDs from .tmp/data-model-planning-status.json are rendered without inventing percentages. If Gate 1 has not surfaced after 15 minutes, inspect the last applicable milestone before interrupting."

Step 2d โ€” Template-only mode

No background scaffold pipeline is used. The template is already present in <working_dir> and dependencies are expected to be installed before this skill starts (npm install). Continue directly to Step 3.

Step 3 โ€” Plan (planner agent + 4 approval gates)

Telemetry checkpoint: plan_app_architecture

First, create the working and planning-artifact directories:

mkdir -p <working_dir> <working_dir>/.tmp

Step 3.0 โ€” Foreground Dataverse planning snapshot and evidence

Planning stays read-only. Branch on <dataverse_planning_mode>:

  • connector-only โ€” skip every command in this section. Set SNAPSHOT_PATH and EVIDENCE_PATH to empty/not supplied, print โ†ท Foreground planning snapshot skipped โ€” the confirmed brief is connector-only., and continue to planner dispatch. Connector-only planning does not perform Dataverse metadata reads; the skill's existing global prerequisites remain unchanged.
  • required โ€” resolve the already selected environment again in the foreground and create one normalized foreground planning snapshot as below. Do not make the nested planner or architect rediscover the tenant.
PLANNING_ENV_JSON=$(node "${PLUGIN_ROOT}/scripts/resolve-environment.js" "$ACTIVE_ENV_ID")
ACTIVE_ENV_URL=$(node -e "const j=JSON.parse(process.argv[1]); console.log(j.environmentUrl || '')" "$PLANNING_ENV_JSON")
ACTIVE_TENANT_ID=$(node -e "const j=JSON.parse(process.argv[1]); console.log(j.tenantId || '')" "$PLANNING_ENV_JSON")
test -n "$ACTIVE_ENV_URL" -a -n "$ACTIVE_TENANT_ID" || {
  echo "โœ— Foreground planning snapshot requires a resolved Dataverse URL and tenant."; exit 2;
}
echo "โœ“ Planning environment resolved: $ACTIVE_ENV_URL (tenant $ACTIVE_TENANT_ID)"

Build <DATAVERSE_CONCEPTS> from every domain noun and workflow family in the approved brief, not a sample. Preserve multiword/header-child families such as medical assessments, care activities, release events, custody transfers, test results, and evidence attachments. Add known standard or required-existing logical names to <EXPLICIT_TABLES>. Build <PROPOSED_TABLES> from the detected publisher prefix for every clearly proposed custom table so collisions and missing names are explicit; leave a name out rather than inventing it when the concept is not yet stable.

Detailed advisory discovery is quality-bounded:

  • Keep the complete customizable-table inventory and ranking.
  • Required exact-name tables are always detailed and do not consume advisory capacity.
  • A concept credibly covered by an exact table does not receive speculative advisory alternatives.
  • Every unresolved concept receives its best advisory candidate first.
  • Only then allocate second/third candidates, with at most 3 per concept and a target ceiling of 40 unique advisory tables.
  • When more than 40 unresolved concepts have distinct best candidates, exceed 40 only enough to preserve one candidate per concept. Quality coverage takes priority over the target ceiling.
  • Inventory-only alternatives remain available for the existing one-time bounded exact-name expansion.
SNAPSHOT_PATH="<working_dir>/.tmp/dataverse-foreground-planning-snapshot.json"
EVIDENCE_PATH="<working_dir>/.tmp/dataverse-planning-evidence.md"

node "${PLUGIN_ROOT}/scripts/create-dataverse-snapshot.js" \
  --env-url "$ACTIVE_ENV_URL" \
  --tenant-id "$ACTIVE_TENANT_ID" \
  --output "$SNAPSHOT_PATH" \
  --concepts "<DATAVERSE_CONCEPTS>" \
  --tables "<EXPLICIT_TABLES>" \
  --proposed-tables "<PROPOSED_TABLES>"

node "${PLUGIN_ROOT}/scripts/render-dataverse-planning-evidence.js" \
  --snapshot "$SNAPSHOT_PATH" \
  --output "$EVIDENCE_PATH"

node -e '
  const s=require(process.argv[1]);
  const t=s.timings;
  const d=s.detailLoadSummary;
  console.log(`โœ“ Dataverse inventory: ${s.inventoryFacts.customizableTables} customizable + ${s.inventoryFacts.exactNameTables} bounded exact-name discoveries (${s.inventoryFacts.requiredExactNameTables} required, ${s.inventoryFacts.proposedCollisionTables} proposed collisions) (${t.inventoryRetrievalMs} ms)`);
  console.log(`โœ“ Candidate selection: ${s.candidateRanking.length} concepts โ†’ ${d.attemptedCandidates} detailed candidates (${d.requiredCandidates || 0} required, ${d.advisoryCandidates || 0} advisory, ${d.exactCoveredConcepts || 0} exact-covered concepts; ${t.candidateSelectionMs} ms)`);
  console.log(`โœ“ Detail loading: ${d.attemptedCandidates} attempted, ${d.loadedCandidates} loaded, ${d.failedCandidates} failed; ${s.tables.reduce((n,x)=>n+x.facts.columnCount,0)} columns, ${s.tables.reduce((n,x)=>n+x.facts.relationshipCount,0)} relationships, ${s.tables.reduce((n,x)=>n+x.facts.keyCount,0)} keys (${t.detailLoadingMs} ms)`);
  console.log(`โœ“ Exact names: requested [${s.exactNameResolution.requestedTables.join(", ")}], loaded [${s.exactNameResolution.loadedTables.join(", ")}], unavailable [${s.exactNameResolution.unavailableTables.join(", ")}]`);
  console.log(`โœ“ Proposed names: ${s.proposedNameChecks.collisions.length} collisions, ${s.proposedNameChecks.missing.length} missing; foreground planning snapshot total ${t.totalDurationMs} ms`);
' "$SNAPSHOT_PATH"
echo "โœ“ Planning evidence: $EVIDENCE_PATH"

If environment resolution, token acquisition, inventory, required exact-name metadata/detail loading, parsing, or evidence rendering fails, surface the exact failure and do not treat an unreadable response as an empty inventory:

  • For every required Dataverse plan, stop planning with a visible BLOCKED: Dataverse planning metadata unavailable for exact target decisions result. Do not dispatch a snapshot-only architect and do not proceed toward Dataverse mutation. The mutation workflow does not accept an unresolved Unverified plan as an executable contract.
  • A concept-selected candidate is advisory unless it is also named by --tables. If advisory detail metadata is unsupported, abstract, or inaccessible, keep the snapshot, record it in detailLoadFailures, list it in the evidence appendix, and continue. Explicit --tables and bounded exact-name expansions remain required and fail closed.
  • --proposed-tables performs collision checks only. Missing proposed names are not required-table failures and must never be selected for detail loading solely because they were proposed.

The snapshot script emits factual DATAVERSE_SNAPSHOT_PROGRESS lines after inventory, candidate selection, and detail loading. Print them immediately; never replace them with estimates or percentages. Once the architect starts, watch <working_dir>/.tmp/data-model-planning-status.json and print each new milestone ID once with its counts and elapsed time. The first environment or snapshot milestone must be visible within 30 seconds. The foreground orchestrator owns this rendering; the architect only owns the status artifact.

For required, pass SNAPSHOT_PATH and EVIDENCE_PATH verbatim to the planner prompt and every direct data-model-architect fallback/revision. A supplied matching snapshot activates the architect's snapshot-only path: no Bash discovery and no live Dataverse calls inside the agent. For connector-only, pass the mode explicitly and state that both paths are not supplied; never provide placeholder file paths.

Benchmark method and acceptance criteria: references/dataverse-planning-benchmark.md.

Hard rule โ€” planner writes are restricted during Step 3. The planner (and any sub-agents it spawns) is permitted to write to only:

  • <working_dir>/native-app-plan.md
  • <working_dir>/_screens_section.md
  • <working_dir>/.tmp/*

All other paths in <working_dir>/ (notably app/, src/, package.json, power.config.json, tamagui.config.ts, tsconfig.json, node_modules/, memory-bank.md) are owned by the foreground setup phases. Do not mutate them during planning.

If the planner needs to record a DONE_WITH_CONCERNS from a sub-agent (data-model architect, screen-planner), add it to an in-memory queue DEFERRED_CONCERNS[] during Step 3. Do not write memory-bank.md yet. Step 6.7 must always flush DEFERRED_CONCERNS[] into memory-bank.md ## Concerns immediately after the file is created.

Resume-from-draft check. Before spawning, check if <working_dir>/native-app-plan.md already exists with content. If yes, a previous planner run (possibly in a degraded context with no Task/gate tools) already drafted sections. Read it. If it has populated ## Data Model / ## Native Capabilities / ## Connectors but the gates were never run (no ## Approvals block, or the file was authored by an agent that returned BLOCKED: tool surface missing), pass resume_from_draft: true and the existing path to the planner so it loads the draft as baseline instead of regenerating from scratch.

Planner preflight (silent). Before the full Task spawn, do a no-op Task probe for mobile-app:native-app-planner (same pattern as Step 11.0). If the probe fails with Agent type โ€ฆ not found, tool unavailable, or the host clearly cannot route nested agents, fall through to inline-gate mode (described below) without prompting. The orchestrator has the full tool surface itself โ€” it can run the gates directly. Do not retry, do not ask the user.

Announce the handoff before the Task call (so the user isn't staring at a blank screen while the planner spins up):

  • required: > "โ†’ Spawning planner agent from the verified foreground planning snapshot. Gate 1/data-model readiness is quality-first with a 10โ€“15 minute target. I will print each factual data-model-planning-status.json milestone and elapsed count as it lands."
  • connector-only: > "โ†’ Spawning planner agent in connector-only mode; a foreground planning snapshot and data-model mutation are not required."

Then spawn the mobile-app:native-app-planner agent via Task (the plugin name mobile-app: prefix is required โ€” without it Task returns Agent type not found):

Spawn agent: mobile-app:native-app-planner

Prompt:
  Plan a Power Apps mobile app.

  Requirements brief (confirmed with user):
  <requirements_brief โ€” bullet points from Step 2b>

  Design vibe opt-in: <design_vibe_opt_in โ€” always "deferred" unless `--no-design` is in $ARGUMENTS, in which case use "skip". Never invent yes/no/other values.>
  Visual companion: <visual_companion โ€” "yes" or "no">

  Original prompt: <full $ARGUMENTS verbatim>
  Wizard answers: <Step 2 answers>
  Working directory: <absolute path of <working_dir>>
  Plugin root: ${PLUGIN_ROOT}
  Dataverse planning mode: <required | connector-only>
  Dataverse planning failure reason: none
  Normalized Dataverse foreground planning snapshot: <absolute SNAPSHOT_PATH verbatim for required; otherwise NOT SUPPLIED>
  Dataverse planning evidence: <absolute EVIDENCE_PATH verbatim for required; otherwise NOT SUPPLIED>
  Structured schema contract: <absolute
  `<working_dir>/.tmp/dataverse-schema-contract.json` for required; otherwise
  NOT SUPPLIED>
  Publisher prefix (detected from env): <DETECTED_PUBLISHER_PREFIX from Step 1.7, e.g. "cr8142a" โ€” use literally as `<prefix>_<entity>` in all logical names. If empty/NOT DETECTED, fall back to `cr` placeholder and surface a `DONE_WITH_CONCERNS` note that Dataverse will normalize at create time.>

  Follow native-app-planner.md. Run all 4 approval gates. On terminal return, emit one of `DONE` / `DONE_WITH_CONCERNS:` / `NEEDS_CONTEXT:` / `BLOCKED:` as the literal first line per AGENTS.md rule #10.

The planner runs gates internally for data model โ†’ native capabilities โ†’ connectors โ†’ screen plan, and writes <working_dir>/native-app-plan.md. Wait for it to return before continuing โ€” do not proceed on a partially-approved plan. On a successful required return, require both .tmp/dataverse-schema-contract.json and .tmp/mobile-plan-status.json before continuing. If the receipt is missing, STOP as BLOCKED; this orchestrator must not synthesize it after the planner has returned.

3.0a โ€” Inline-gate fallback (planner unavailable OR returned BLOCKED: tool surface missing)

When the preflight fails OR the planner returns BLOCKED: tool surface missing <โ€ฆ>, the orchestrator runs the four gates inline. Do NOT re-spawn the planner โ€” it cannot succeed in this host. Print once:

"โ†’ Planner agent unavailable in this host โ€” running approval gates inline. (No action needed; this is automatic.)"

Then execute, in order, using your own EnterPlanMode + AskUserQuestion:

  1. If a draft native-app-plan.md exists: read it as baseline. Surface each populated section (## Data Model, ## Native Capabilities, ## Connectors) one at a time via EnterPlanMode, take user feedback inline, edit the file in place. Skip generating sections that are already populated and approved.

  2. If no draft exists: spawn mobile-app:data-model-architect directly via Task (single architect, not the orchestrator agent) to draft ## Data Model; then build ## Native Capabilities + ## Connectors inline from the brief; then spawn mobile-app:screen-planner with phase: graph and phase: specs per the two-phase Gate 4 split.

    Before each screen-planner spawn, print a one-line ETA so the user knows the agent is live and roughly how long to wait (the agent's own Bash echo progress markers โ€” see agents/screen-planner.md "Progress streaming" โ€” surface every milestone, but the orchestrator's pre-spawn line gives the wall-clock budget):

    • Before phase: graph: > "โ†’ [Gate 4a] Spawning screen-planner phase=graph (~2 min for ${N} screens)โ€ฆ"
    • Before phase: specs: > "โ†’ [Gate 4b] Spawning screen-planner phase=specs (~1 min/screen, ~${N} min for ${N} screens). Progress markers will appear inline."

MUST forward the Dataverse planning mode in the direct architect prompt. In required, also forward SNAPSHOT_PATH and EVIDENCE_PATH verbatim and do not resolve the environment or run Dataverse discovery again. In connector-only, state that both paths are not supplied; never invent placeholder artifacts.

MUST forward $DETECTED_PUBLISHER_PREFIX from Step 1.7 in the architect prompt โ€” same line as the planner prompt at Step 3 line 1034: "Publisher prefix (detected from env): <DETECTED_PUBLISHER_PREFIX> โ€” use literally as <prefix>_<entity> in all logical names. If empty/NOT DETECTED, fall back to cr placeholder and surface a DONE_WITH_CONCERNS note that Dataverse will normalize at create time." Without this, the architect defaults to cr_ and the whole plan needs a post-hoc sweep when the real prefix is something else (e.g. cr3e9).

In required, also require the direct architect to write and normalize <working_dir>/.tmp/dataverse-schema-contract.json per its agent contract. A draft Markdown section without that sidecar is not an executable Gate 1 result.

Why this works even though the planner just returned BLOCKED for tool surface: the orchestrator (this skill, running in the user's slash-command session) always has the full tool surface โ€” Task, EnterPlanMode, ExitPlanMode, AskUserQuestion, Read, Write, Bash. What's missing is the surface inside nested agent contexts (the native-app-planner agent runs in a sandbox without EnterPlanMode/AskUserQuestion, which is why its Step 0 preflight returned BLOCKED). The leaf agents data-model-architect and screen-planner only need Read/Write/Bash to draft markdown โ€” they don't need EnterPlanMode/AskUserQuestion themselves. Spawn them; the orchestrator owns the gates.

  1. Run the gates yourself โ€” use EnterPlanMode four times (data model โ†’ native caps + connectors merged โ†’ screen graph 4a โ†’ screen specs 4b). Same gate prompts as the planner agent would use. Gate 4 is a markdown screen-graph review only โ€” design picking happens unconditionally at Step 6.75 via /design-system (no separate style-picker handoff at Gate 4 even in inline mode).

  2. Write the final approved native-app-plan.md with an ## Approvals block at the bottom listing each gate, who approved (user), and a timestamp.

    HARD RULES for the plan structure (mirror the planner agent's template at agents/native-app-planner.md Step 4):

    • Top-level headings are EXACTLY: ## Overview, ## App Requirements, ## Data Model, ## Native Capabilities, ## Design Direction, ## Connectors, ## Screens, ## Approvals. Do NOT invent a ## Brief super-section that nests the data model under it.
    • ## App Requirements is the user's confirmed brief verbatim (the <requirements_brief> from Step 2b), capped at ~80 lines. No expansion, no rewriting, no embedded preview of the data model.
    • Discovery failure notes (e.g. az login on the wrong tenant, 401 from dataverse-request.js, all entities classified Create) go to <working_dir>/memory-bank.md under ## Discovery Notes, NOT into the plan. Keep at most a single one-line breadcrumb in ## Data Model like > Discovery skipped โ€” see memory-bank.md. if relevant.
    • Sample data notes, immutability plug-in notes, file-column setup notes, dispatch-block server rules go under a single ### Notes subsection in ## Data Model. Cap each at 2 sentences; link to post-deployment-tasks.md for longer write-ups instead of inlining.
  3. Record the same structured approval receipt as the planner path. At data-model acceptance, initialize <working_dir>/.tmp/mobile-plan-status.json with the exact normalized contract content/hash. After each later gate is accepted, update only that gate's approval record and the current plan hash; after Gate 4b, record the final structured service dependencies and integrity hash. Follow agents/native-app-planner.md Step 6 exactly. Never call the operation manifest builder to create or restamp this receipt. A changed approved section invalidates its record until the existing inline gate approves it again.

If the orchestrator's OWN Task tool is unavailable (rare โ€” would mean even leaf agents can't be spawned), fall further to fully-inline mode. In required, draft the data model from SNAPSHOT_PATH plus EVIDENCE_PATH with no live OData probe and write/normalize the same structured schema contract required by agents/data-model-architect.md. In connector-only, write an explicit zero-table/no-Dataverse ## Data Model section and no contract. Then draft native caps + connectors heuristically, draft the screen graph + specs against shared/references/screen-templates.md, and run the four gates against the user. This is the last-resort path โ€” functional but slower because the orchestrator does work the architects normally parallelize.

Hard rule: never silently skip a gate just because the planner couldn't run. The user MUST approve each section through EnterPlanMode before any mutation step (Step 8 onwards) executes.

3.0 โ€” Sub-agent return-status switch (canonical)

Use the plugin-wide protocol in AGENTS.md rule #10 for every Task return in this skill: planner, parallel screen-builders, and future agent spawns. Parse the literal first line and branch: DONE continues; DONE_WITH_CONCERNS: surfaces + records in memory-bank.md; NEEDS_CONTEXT: re-dispatches with missing context, capped at 2 retries; BLOCKED: stops and records under ## Blocks. Unknown first lines are malformed and must be treated as BLOCKED.

Data-model exact-name expansion: when the planner or direct architect returns exactly NEEDS_CONTEXT: detailed-dataverse-metadata:<logical names>, sort and de-duplicate those names. This signal is valid only in required mode with a validated base snapshot, whether it came from the planner or the direct architect fallback; receiving it in connector-only mode is BLOCKED. Perform one bounded foreground expansion. Reuse the existing snapshot inventory, issue at most one exact-name metadata query for requested names absent from it, and do not run another broad inventory query:

node "${PLUGIN_ROOT}/scripts/create-dataverse-snapshot.js" \
  --env-url "$ACTIVE_ENV_URL" \
  --tenant-id "$ACTIVE_TENANT_ID" \
  --base-snapshot "$SNAPSHOT_PATH" \
  --output "$SNAPSHOT_PATH" \
  --tables "<exact comma-separated logical names>"

node "${PLUGIN_ROOT}/scripts/render-dataverse-planning-evidence.js" \
  --snapshot "$SNAPSHOT_PATH" \
  --output "$EVIDENCE_PATH"

node -e '
  const s=require(process.argv[1]);
  const x=s.expansion;
  const d=s.detailLoadSummary;
  console.log(`โœ“ Expansion requested: [${x.requestedTables.join(", ")}]`);
  console.log(`โœ“ Expansion loaded: [${x.loadedTables.join(", ")}]`);
  console.log(`โœ“ Expansion unavailable: [${x.unavailableTables.join(", ")}]`);
  console.log(`โœ“ Expansion details: ${d.attemptedCandidates} attempted, ${d.loadedCandidates} loaded, ${d.failedCandidates} failed`);
  console.log(`โœ“ Expansion timing: metadata ${s.timings.inventoryRetrievalMs} ms, selection ${s.timings.candidateSelectionMs} ms, details ${s.timings.detailLoadingMs} ms, total ${s.timings.totalDurationMs} ms`);
' "$SNAPSHOT_PATH"

Print the expansion's requested/loaded/unavailable names and timings immediately, then re-dispatch the same planner or architect once with the same snapshot/evidence paths. A second detailed-metadata signal is BLOCKED; do not loop, broaden concepts, or defer exact validation to mutation.

Data-model proposed-name expansion: when the planner or direct architect returns exactly NEEDS_CONTEXT: proposed-dataverse-names:<logical names>, sort and de-duplicate those names. This signal is valid only in required mode with a validated snapshot. Perform one collision-only foreground expansion:

node "${PLUGIN_ROOT}/scripts/create-dataverse-snapshot.js" \
  --env-url "$ACTIVE_ENV_URL" \
  --tenant-id "$ACTIVE_TENANT_ID" \
  --base-snapshot "$SNAPSHOT_PATH" \
  --output "$SNAPSHOT_PATH" \
  --proposed-tables "<exact comma-separated logical names>"

node "${PLUGIN_ROOT}/scripts/render-dataverse-planning-evidence.js" \
  --snapshot "$SNAPSHOT_PATH" \
  --output "$EVIDENCE_PATH"

This expansion checks collisions only; it does not treat absent proposed names as required existing tables or load their details. Re-dispatch once. A second proposed-name signal is BLOCKED. If a collision is found and the architect needs compatibility facts, it may then use the separate one-time detailed-dataverse-metadata expansion for that existing table.

Planner-only early-return signals are handled before the status switch: INDUSTRY_CONFIRM_REQUESTED: routes to Step 3.0a; DESIGN_VIBE_REQUESTED: routes to Step 3a. After the handoff, re-spawn the planner and process its new first line through this switch.

Step 3.0a โ€” Industry confirmation handoff (orchestrator-owned)

When the planner is uncertain about which industry the app belongs to (no keyword match, ambiguous match, or wizard-aesthetic conflict), it returns early with this single line as its message:

INDUSTRY_CONFIRM_REQUESTED: <inferred-industry>|<reason-code>|<top-3-alternatives-comma-sep>

Example: INDUSTRY_CONFIRM_REQUESTED: productivity|no-keywords|field-ops,healthcare,e-commerce

This fires before Gate 1 โ€” it's not a gate, just a confidence check so the wrong industry doesn't silently lock in the design language for the entire app.

Skip this section if <design_vibe_opt_in> is yes or skip โ€” in those cases the user is either driving design explicitly (yes) or has opted out of design entirely (skip), so industry inference doesn't matter.

When you see INDUSTRY_CONFIRM_REQUESTED: and <design_vibe_opt_in> is no:

  1. Parse the three pipe-delimited fields. Map reason codes to a short user-facing explanation:

    • no-keywords โ†’ "no clear industry signal in your description"
    • ambiguous-match โ†’ "your description matches multiple industries"
    • wizard-conflict โ†’ "your aesthetic answer doesn't match the inferred industry"
  2. Map each industry slug to a one-line description for the picker (use these exactly):

    SlugDescription
    field-opsField/Ops โ€” high contrast, large targets, camera-forward (Uber Driver, ServiceTitan)
    financeFinance โ€” blue palette, conservative type, generous whitespace (banking apps)
    healthcareHealthcare โ€” warm palette, friendly type, compassionate copy (patient apps)
    educationEducation โ€” bright playful, gamification, streak/progress (Duolingo)
    productivityProductivity โ€” near-monochrome, dense layout, monospace data (Linear, Notion)
    e-commerceE-commerce โ€” brand-forward color, product imagery, frictionless CTAs (retail apps)
    tech-iotTech/IoT โ€” dark + accent gradients, data-dense cards, real-time indicators (monitoring dashboards)
  3. Ask one AskUserQuestion:

    "Quick sanity check before I build the design: I inferred this is a app, but . Confirm or pick another:

    (a) โ€” recommended (b) (c) (d) (e) Other / let me describe โ€” free text

    Which? (a / b / c / d / e โ€” default: a)"

  4. Persist the answer:

    echo "<chosen-industry-slug>" > "<working_dir>/.industry-confirmed"

    For option (e), let the user free-text a description; map it to the closest slug (or productivity as final fallback) and store that.

  5. Re-spawn the planner. Use the same prompt as Step 3, plus an extra line:

    Industry confirmed: <chosen-industry-slug>

    The planner will see this on re-spawn, skip its detection + confidence check, and lock the industry to your value. After the re-spawn, re-check the planner's return value โ€” it may now return normally, or it may still return DESIGN_VIBE_REQUESTED: (handled in Step 3a) if the user opted into the vibe picker.

Step 3a โ€” Style-picker handoff (no-op in current plugin layout)

/design-system ships with this plugin and always runs at Step 6.75, so the style-picker handoff at Gate 4 is a no-op. Behavior:

  • The planner writes a placeholder ## Design Direction: <deferred โ€” set by /design-system> block into native-app-plan.md at Gate 4 and proceeds without asking the user. Step 6.75 rewrites the placeholder with the real direction.
  • If a legacy planner output emits DESIGN_VIBE_REQUESTED: as its first line, write the placeholder block yourself (insert before ## Design, or before ## Screens if ## Design is absent), then re-spawn the planner with Design vibe opt-in: done. Do NOT run a vibe picker here โ€” Step 6.75 owns that.
  • If --no-design is in $ARGUMENTS, write the placeholder block, mark <design_vibe_opt_in> = skip, and Step 6.75 also no-ops. Screen-builders fall back to industry-inferred defaults from universal-patterns.md.

If the planner's first return is anything other than DESIGN_VIBE_REQUESTED: โ€” i.e. it ran all gates including Gate 4 normally โ€” skip directly to Step 3b.

Step 3b โ€” Open the plan preview in the user's browser (orchestrator-owned)

The planner emits a line of the form PLAN_PREVIEW_PATH: file://<abs-path>/_plan_preview.html before each Gate 4 plan-mode entry. The planner itself does NOT open the browser โ€” sub-agent shells often lose GUI context, and silent open-failures leave the user staring at the spinner with no preview. The orchestrator owns this step because it has the user's interactive session.

When to run this: every time the planner enters or re-enters Gate 4 (initial pass + each reject loop). Detection: scan the planner's most recent visible output for the PLAN_PREVIEW_PATH: token; the value after the colon is the absolute file:// URL.

What to do:

  1. Print the link in a dedicated message so the user always has the fallback (clickable in most terminals):

    "Plan-time visual preview: file:///_plan_preview.html"

  2. If <visual_companion> = no, stop here. Do not attempt to open a browser. The user explicitly opted out; the printed link is their handle. Continue immediately to the planner's Gate 4 prompt.

  3. Else attempt to open in the user's default browser via the OS-portable chain:

    open "<abs-path>/_plan_preview.html" 2>/dev/null \
      || xdg-open "<abs-path>/_plan_preview.html" 2>/dev/null \
      || powershell.exe -NoProfile -Command "Start-Process '<abs-path>\_plan_preview.html'" 2>/dev/null \
      || echo "Auto-open failed. Use the link above."
  4. Do NOT block on success. If the chain prints "Auto-open failed", the link from step 1 is the user's fallback. Continue immediately so the planner's plan-mode prompt surfaces without delay.

If the planner returns without emitting a PLAN_PREVIEW_PATH: line, that is expected โ€” the planner passes skip_preview: true to screen-planner since /design-system (always installed) renders the single visual preview at Step 6.75 after brand locks. Print:

"โ†’ Gate 4 reviewed structurally. Visual preview will appear at Step 6.75 after /design-system locks your brand tokens (~5 min from now after scaffold)."

โ€ฆand continue without attempting any browser open. Do not warn or treat this as an error โ€” it is the documented behavior.

3.9 โ€” Post-plan publisher-prefix gate

Before continuing to Step 4, verify the written native-app-plan.md actually uses $DETECTED_PUBLISHER_PREFIX from Step 1.7. Catches both the inline-fallback path missing the prefix and an architect that ignored the instruction.

if [ -n "$DETECTED_PUBLISHER_PREFIX" ]; then
  WRONG=$(grep -oE 'cr[a-z0-9]*_[a-z][a-z0-9_]*' "$WORKING_DIR/native-app-plan.md" \
    | grep -vE "^${DETECTED_PUBLISHER_PREFIX}_" | sort -u || true)
  if [ -n "$WRONG" ]; then
    echo "PLAN PREFIX MISMATCH โ€” expected ${DETECTED_PUBLISHER_PREFIX}_, found:"
    echo "$WRONG"
  fi
fi

If mismatches are reported, sweep native-app-plan.md (and any auxiliary files like .datamodel-manifest.json if already written) replacing the wrong prefix with ${DETECTED_PUBLISHER_PREFIX}_ before Step 4. Do NOT proceed to Step 5 with a wrong-prefix plan โ€” the sweep cost grows ~500 occurrences once services are generated.

For required, apply the same prefix correction to .tmp/dataverse-schema-contract.json, then normalize it. Before Step 4, require both approved artifacts:

test -f "$WORKING_DIR/native-app-plan.md"
test -f "$WORKING_DIR/.tmp/dataverse-schema-contract.json"
node "${PLUGIN_ROOT}/scripts/build-dataverse-operation-manifest.js" \
  --normalize-contract "$WORKING_DIR/.tmp/dataverse-schema-contract.json" \
  --output "$WORKING_DIR/.tmp/dataverse-schema-contract.json"

Do not fall back to parsing the Markdown ER diagram when the sidecar is missing or malformed; route through the existing planner/direct-architect revision path.

Step 4 โ€” Auth & environment selection

Telemetry checkpoint: select_app_environment

node "${PLUGIN_ROOT}/scripts/resolve-environment.js" "$ACTIVE_ENV_ID"

If the resolved environment doesn't match what the planner used in Step 3, ask the user for the intended environment ID and re-run resolve-environment.js. Capture the environment ID for Step 6.

Step 5 โ€” Prepare existing template

Telemetry checkpoint: prepare_template_files

This step is template-only and foreground-only. Do not clone/copy templates, do not run background scaffold jobs, and do not use any legacy fallback path.

Print before starting:

"โ†’ [Step 5/13] Preparing existing Expo standalone template in <working_dir> โ€ฆ"

Required checks:

cd <working_dir>
test -f package.json && test -f app.config.js && test -f auth.config.json && test -f tamagui.config.ts
test -d node_modules/expo

If any required template file is missing, STOP:

"This folder is not a fresh expo-app-standalone template. Materialize a fresh template with degit into a new folder, run npm install, then rerun /create-mobile-app --working-dir <fresh-template-dir>."

If node_modules/expo is missing, STOP:

"Dependencies are not installed. Run npm install in the template folder, then rerun /create-mobile-app --working-dir <fresh-template-dir>."

If already-created markers appear (memory-bank.md, .datamodel-manifest.json, or src/generated/services/*.ts) and Step 0 did not enter the resume path, STOP. native-app-plan.md is expected here because Step 3 writes the approved plan before template preparation:

"This folder already looks like a created app. For a new app, materialize a fresh expo-app-standalone template with degit into a new folder and rerun this skill there."

Run the deterministic preparation script once:

PREPARE_SCRIPT="${PLUGIN_ROOT}/scripts/prepare-mobile-template.js"
node - "$PREPARE_SCRIPT" <<'NODE'
const { prepareMobileTemplate } = require(process.argv[2]);

// Replace these placeholders with JSON.stringify(...) output so user-provided
// quotes and dollar signs remain data rather than becoming shell syntax.
const result = prepareMobileTemplate({
  workingDir: <JSON_STRING_OF_WORKING_DIR>,
  displayName: <JSON_STRING_OF_DISPLAY_NAME>,
  slug: <JSON_STRING_OF_SLUG>,
});
process.stdout.write(`${JSON.stringify(result)}\n`);
NODE

Capture result.writtenFiles as the exact project-relative preparation validation targets. This list contains only files created or changed by this preparation call, excluding unchanged preserved files and deletions. Keep removedPowerConfig and removedLegacyFiles as removal outcomes, not --file targets: Step 6 can recreate the same config path with a different owner. Union targets across any preparation reruns rather than replacing earlier pending changes. Do not rebuild this list from git status or a directory scan after initialization.

The script is the only owner of Step 5 mutations. It updates identity, removes only recognized legacy example hooks/query-client files, copies shared helpers only when missing, verifies that TypeScript inherits the host configuration, and structurally verifies the root provider/theme/safe-area contract. It preserves custom navigation, existing helper bytes, offlineProfile, provider props, and the template's @ts-ignore generation boundaries.

Generated ownership boundary: Step 5 must not create, reset, delete, or write anything under src/generated/. Only Power Apps schema/data-source generation commands own that directory. A generated file required later is created by its owning command, never by a placeholder barrel.

The script fails visibly for unsupported root-layout shapes or dangling legacy imports. Do not fall back to a full-file rewrite or regex patch. After it returns successfully, continue to Step 6.

Fix 1 โ€” App identity in app.config.js and package.json

Substitute the hardcoded template values with wizard answers from Step 2:

FindReplace with
`const APP_NAME = process.env.APP_DISPLAY_NAME
`const APP_SLUG = process.env.APP_SLUG
"name": "powerapps-standalone-app""name": "<slug>"

Bundle ID and scheme are left as template defaults โ€” they are fixed across all dev builds and patched by the wrap pipeline at release time.

Fix 1b โ€” Verify captured dev logging path

Manual npm run dev must remain the normal Expo entry point. The template's metro.config.js delegates to createPowerAppsMetroConfig from @microsoft/power-apps-native-host/config/metroConfig; that factory installs sanitized Metro terminal and HTTP bundle-failure logging under .powernative/metro-logs/. Manual starts and /debug-app use the same log source without a process-owning wrapper. Verify these script entries only; do not add wrapper-specific scripts:

node - "<working_dir>" <<'NODE'
const fs = require('node:fs');
const path = require('node:path');
const root = process.argv[2];
const packagePath = path.join(root, 'package.json');
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
if (!pkg.scripts || pkg.scripts.dev !== 'expo start') {
  throw new Error('Expected package.json scripts.dev to be "expo start". Do not route dev through a wrapper.');
}
if (pkg.scripts.predev !== 'npm run generate-schemas && npm run type-check') {
  throw new Error('Expected predev to run schema generation followed by type-checking.');
}
NODE

Fix 2 โ€” Remove only an empty placeholder power.config.json

The preparation script parses power.config.json and removes it only when environmentId is empty or missing. A populated file is preserved and later validated against the approved environment. Do not use an unconditional delete.

Fix 3 โ€” Remove recognized legacy examples without touching generated code

Newer snapshots do not ship the Contacts / Accounts / UserProfile example hooks or the old app-owned query client. The preparation script removes only those recognized files when present. It never traverses or mutates src/generated/; generated models, services, schemas, and barrels remain owned by Power Apps generation commands.

Do NOT overwrite app/(app)/home.tsx here. The current template ships a safe-area-aware, semantic-token starter route. The screen-builder replaces it only when the approved Screen Map assigns that route.

Keep src/hooks/ itself โ€” screen-builders write new hooks into it.

Fix 3b โ€” Scan for dangling imports referencing deleted files (back-compat only)

The preparation script scans app/ and non-generated src/ files after cleanup. Any remaining legacy example import is an explicit failure. Do not replace whole screens or layouts to make the scan pass; use a supported fresh template or repair the precise stale import before continuing.

Fix 6 โ€” Schema generation boundary

app/_layout.tsx imports schemaMap from src/generated/connectorSchemas.ts, which is generated by npm run generate-schemas (the generate-connector-schemas binary from the @microsoft/power-apps-cli devDep). Do not generate an empty schema map during initial scaffold: the template's @ts-ignore boundary lets tsc validate the scaffold without that artifact, and schema generation is more useful after a data source exists or immediately before dev/build entry points.

Do NOT hand-write a stub connectorSchemas.ts โ€” the generated output has a specific shape that downstream code depends on; a placeholder will break npx power-apps push.

Why tsc already passes post-clone (current template, PR #30): the template's app/_layout.tsx and src/playerConfig.ts carry // @ts-ignore comments above the power.config.json and connectorSchemas imports specifically so the project type-checks before power.config.json and connectorSchemas.ts exist. Never strip these @ts-ignore lines โ€” Fix 8 below preserves them when patching app/_layout.tsx to thread the project's tamaguiConfig into PowerAppsProvider, and any future Edit to either file MUST keep them. Removing them resurfaces a tsc failure against missing generated files.

Fix 7 โ€” Seed shared code only when missing

The preparation script creates the shared source directories and copies each approved sample helper only when its destination does not exist. Existing helpers are byte-for-byte preserved, so reruns cannot overwrite user or builder changes.

Fix 8 โ€” Thread the project's tamaguiConfig and active theme into the host provider

The template ships PowerAppsProvider with host-owned light and dark theme defaults. Fix 8 ensures the project tamaguiConfig and color-scheme-driven defaultTheme are present. It preserves explicit theme and darkTheme overrides when an app already has them, but does not add redundant overrides to a fresh template. Step 9b adds explicit themes only when applying generated brand tokens. Do NOT add an outer <TamaguiProvider> โ€” PowerAppsProvider composes it internally and duplicating triggers "useTheme must be used within a TamaguiProvider" warnings on hot reload.

The preparation script edits the existing root layout structurally. It adds missing imports and provider props, then wraps PowerAppsProvider with SafeAreaProvider only when needed. It does not replace the file and it does not wrap <Slot /> with SafeAreaView; each rendered route owns its content edges to avoid double insets.

Key points:

  • Do NOT remove the two // @ts-ignore lines. They keep tsc green pre-npx power-apps init.
  • Do NOT add an outer <TamaguiProvider> โ€” PowerAppsProvider composes it internally.
  • SafeAreaProvider wraps the tree so child screens can call useSafeAreaInsets() without a context error. Each route must use SafeAreaView or explicit insets for its own visible edges.
  • tamaguiConfig is imported from '../tamagui.config' (the default export of tamagui.config.ts at project root).
  • defaultTheme flips between light/dark via useColorScheme(). /design-system --add-dark-mode later wires per-token dark variants.

Fix 4 โ€” Shared TypeScript configuration

The current template extends @microsoft/power-apps-native-host/config/tsconfig. That host configuration owns the runtime package paths and the six shared-code aliases: @/components, @/hooks, @/utils, @/tokens, @/generated, and @/native. The preparation script verifies this inheritance and does not create a second template-local alias map. Expo Metro consumes the resulting effective TypeScript paths, so no Babel alias plug-in is required.

<Gradient> (used by components/index.tsx) requires expo-linear-gradient. Assume the upstream template ships it โ€” do NOT edit package.json to add it. If npm install (Step 6.5) later reveals the dep is missing, STOP and ask the user to wait for the next template release; do not work around by adding the dep here (same lockdown rule as /add-native).

Do not run npm install inside Step 5 โ€” in template-only mode dependencies must already be installed before the skill starts.

Install note (current template): The template does not read power.config.json during npm install. The Step 6 โ†’ Step 6.5 ordering is kept for predictable checkpoints, but do not run npm run generate-schemas during initial scaffold.

Step 6 โ€” Initialize

Telemetry checkpoint: initialize_power_apps_project

Print before starting:

"โ†’ [Step 6/13] Running npx power-apps init -t MobileApp to write power.config.json for environment . ~15โ€“30 seconds."

cd <working_dir>
npx power-apps init -t MobileApp --display-name "<displayName>" --environment-id "<environment-id>" --non-interactive

Substitute the approved Step 2 display name and Step 4 environment ID using shell-safe quoting; do not ask for either value again. Step 5 must leave power.config.json absent. If a populated file remains, STOP and report its environment instead of overwriting it or running init again.

Verify power.config.json exists and both its environmentId and appDisplayName match the approved Step 2/Step 4 values. If initialization fails, report the exact error and STOP. Record the successful CLI command as the config's writer. These checks are read-only; do not add this CLI-generated file to Step 5's manual validation targets or hand-edit it.

Step 6.5 โ€” Verify dependencies

This step verifies dependencies only. The user must have run npm install before invoking the skill.

[ -d "<working_dir>/node_modules/expo" ] && echo "โœ“ node_modules present" || echo "โœ— missing โ€” run npm install in the template folder and rerun"

If node_modules/expo is missing, STOP. Tell the user to run npm install in the template folder. Do not provision ADO tokens or run npm install from this skill.

Step 6.5b โ€” Root runtime contract verification

Step 5 already performs the idempotent structural update and postcondition checks. Do not mutate _layout.tsx again here. Verify only that the prepared layout still contains SafeAreaProvider, tamaguiConfig, offlineProfile, and the color-scheme-driven defaultTheme. If brand-token wiring has already run, also verify its explicit theme and darkTheme props. If any applicable contract element is missing, rerun the Step 5 preparation script and stop if it reports an unsupported layout.

Step 6.6 โ€” Scaffold TypeScript gate

Telemetry checkpoint: validate_scaffold_typescript

Print before starting:

"โ†’ [Step 6.6/13] Running scaffold tsc smoke check (~10โ€“30 seconds)."

With node_modules/ populated, run the scaffold TypeScript gate. Do not run npm run generate-schemas here just to produce an empty connectorSchemas.ts; the template is intentionally type-checkable before that file exists, and the script is already run after data-source changes and again before Step 12 starts the dev server.

npx tsc --noEmit

tsc must pass here. If it doesn't, the post-clone surgery in Step 5 (Fixes 1โ€“7) is incomplete โ€” do not proceed to data sources or screen builders. Re-read the Step 5 fixes against the current working dir contents and reapply any missed edit.

This is the Scaffold gate from the TypeScript Gate Policy. If it fails, capture the full error list once, batch-fix scaffold/template causes, and rerun this gate. Do not continue to Step 6.7 or any app-specific mutation until this gate is clean. If the only failure is a missing generated schema import, preserve the template @ts-ignore boundary rather than generating an empty schema artifact.

Step 6.7 โ€” Seed the memory bank

cp "${PLUGIN_ROOT}/shared/memory-bank.md" "<working_dir>/memory-bank.md"

Fill in the Project facts and Power Platform context sections from Steps 2 and 4. From here on, every step appends to the relevant section of <working_dir>/memory-bank.md immediately after success โ€” not at the end. This is what enables Step 0's resume on a future run.

Before leaving this step, run the shared changed-file gate on Step 5's writtenFiles, memory-bank.md, and any other pending skill/subagent-authored files or scaffold repairs, using an exact --file argument for each. Exclude files verified as CLI-generated and not manually modified afterward; power.config.json is covered by the read-only identity checks in Step 6, not this write gate. The successful TypeScript check does not replace either validation.

Immediately after creating memory-bank.md, flush any queued planner concerns from DEFERRED_CONCERNS[] into ## Concerns (append-only). This flush is unconditional: if the queue is non-empty, write it now before continuing to Step 6.75.

Also persist the Visual Companion preference so re-runs (/edit-app, /preview-screens, future /design-system runs) honor it without re-asking. Append to the Project facts section:

visual_companion: <yes|no>   # set in Step 2b โ€” controls whether browser previews open automatically

/preview-screens reads this flag when invoked from inside this project; if no, it prints the file path instead of opening. /edit-app reads it to decide whether to re-open _plan_preview.html after a re-plan. The flag is per-project and does not leak across apps.

Step 6.75 โ€” Design system

Print before starting:

"โ†’ [Step 6.75/13] Locking your design system โ€” source of truth for every screen built next. Takes 5 sec to 3 min depending on path."

Skip this step if --no-design is in $ARGUMENTS โ€” placeholder ## Design Direction: <deferred> block stays in the plan, screen-builders fall back to industry-inferred defaults from universal-patterns.md.

Otherwise, invoke /design-system (ships with this plugin):

Invoke skill: /design-system

Arguments:
  --working-dir <working_dir>

The skill detects orchestrator mode (CODE_APPS_NATIVE_ORCHESTRATING=1), collects brand inputs, presents the cost picker (a/b/c/d), runs the internal style picker, writes brand/design-system.md + brand/tokens.ts, renders brand/design-system.html, and returns with status.

Handle the return per the status protocol (AGENTS.md rule #10):

  • DONE โ†’ continue to Step 7. Record brand_path, tokens_path, direction in memory-bank.
  • DONE_WITH_CONCERNS โ†’ surface concerns, ask user, continue.
  • NEEDS_CONTEXT โ†’ surface question, re-invoke with answer.
  • BLOCKED โ†’ surface error, STOP.

If the user picked path (c) Skip in the cost picker, the skill returns immediately with DONE and no brand/ files. Screen-builders fall back to ## Design Direction โ€” same as today's behavior. But the user still needs a visual preview before code is written โ€” fall through to the "Skip path preview" block below.

After /design-system returns DONE โ€” two branches:

Branch A โ€” brand/ files exist (user picked path a, b, or d)

This is the FIRST and ONLY HTML preview the user sees in the new flow โ€” Gate 4 was a structural-only review (markdown screen-graph, no HTML). /design-system owns rendering of _plan_preview.html at its Sub-step 6.5 using the locked brand tokens. No re-spawn from the orchestrator is needed; the preview is fresh when the skill returns.

Branch B โ€” Skip path preview (user picked path c โ€” no brand/ files)

The user skipped the design system but still deserves to see their screens before code is written. Render a preview with Field/Ops defaults:

  1. Print:

    "โ†’ Design system skipped โ€” rendering screen preview with Field/Ops defaults so you can validate the layout before code is written."

  2. Render _plan_preview.html โ€” read the screen specs from `native-app-plan.md