diff --git a/.github/workflows/skill-routing-coverage.yml b/.github/workflows/skill-routing-coverage.yml
new file mode 100644
index 0000000..efb3b0e
--- /dev/null
+++ b/.github/workflows/skill-routing-coverage.yml
@@ -0,0 +1,84 @@
+name: skill routing coverage
+
+on:
+ pull_request:
+ branches:
+ - main
+ merge_group:
+ branches:
+ - main
+
+concurrency:
+ group: skill-routing-coverage-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+ issues: write
+
+jobs:
+ check:
+ name: check expo-overview routing
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+
+ - name: Set up Bun
+ uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
+
+ - name: Check expo-overview routing coverage
+ id: routing-check
+ continue-on-error: true
+ env:
+ ROUTING_CHECK_SUMMARY_PATH: .context/skill-routing-check-summary.md
+ run: bun scripts/check-overview-routing.ts
+
+ - name: Add workflow summary
+ if: always() && hashFiles('.context/skill-routing-check-summary.md') != ''
+ run: cat .context/skill-routing-check-summary.md >> "$GITHUB_STEP_SUMMARY"
+
+ - name: Comment on pull request
+ if: always() && github.event_name == 'pull_request' && hashFiles('.context/skill-routing-check-summary.md') != ''
+ continue-on-error: true
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ const fs = require("node:fs");
+
+ const marker = "";
+ const summary = fs.readFileSync(".context/skill-routing-check-summary.md", "utf8");
+ const body = `${marker}\n${summary}`;
+ const issue_number = context.payload.pull_request.number;
+ const { owner, repo } = context.repo;
+
+ const comments = await github.paginate(github.rest.issues.listComments, {
+ owner,
+ repo,
+ issue_number,
+ per_page: 100,
+ });
+ const existingComment = comments.find(
+ (comment) => comment.user?.type === "Bot" && comment.body?.includes(marker)
+ );
+
+ if (existingComment) {
+ await github.rest.issues.updateComment({
+ owner,
+ repo,
+ comment_id: existingComment.id,
+ body,
+ });
+ } else {
+ await github.rest.issues.createComment({
+ owner,
+ repo,
+ issue_number,
+ body,
+ });
+ }
+
+ - name: Fail on missing routing
+ if: steps.routing-check.outcome == 'failure'
+ run: exit 1
diff --git a/AGENTS.md b/AGENTS.md
index 04e596e..e33a5db 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -191,8 +191,9 @@ When changing Claude Code marketplace aliases, preserve backward compatibility u
1. Create `plugins/expo/skills/my-skill/SKILL.md`.
2. Add focused reference files under `plugins/expo/skills/my-skill/references/` when the skill needs more detail than belongs in the main `SKILL.md`.
3. Add scripts under `plugins/expo/skills/my-skill/scripts/` only for reusable logic.
-4. Update `plugins/expo/README.md` or the root `README.md` only when the user-facing installation or usage story changes.
-5. Keep the skill under the existing `expo` plugin unless there is a clear distribution reason to create a new plugin.
+4. Add a one-line entry for the skill to the `expo-overview` Skill Map (`plugins/expo/skills/expo-overview/SKILL.md`) so the router can dispatch to it. This is enforced by the `skill routing coverage` workflow (`bun scripts/check-overview-routing.ts`).
+5. Update `plugins/expo/README.md` or the root `README.md` only when the user-facing installation or usage story changes.
+6. Keep the skill under the existing `expo` plugin unless there is a clear distribution reason to create a new plugin.
## Testing Plugins
diff --git a/plugins/expo/README.md b/plugins/expo/README.md
index 8e071b4..fa4ff6c 100644
--- a/plugins/expo/README.md
+++ b/plugins/expo/README.md
@@ -62,6 +62,10 @@ Official AI agent skills from the Expo team for building, deploying, upgrading,
## Skills Included
+### Start Here
+
+- **expo-overview** — Router and shared setup rules; the entry point when a request is vague or the user hasn't named a specific Expo tool
+
### App Design
- **add-app-clip** — Add an iOS App Clip target to an Expo app (AASA, associated domains, TestFlight)
@@ -69,6 +73,8 @@ Official AI agent skills from the Expo team for building, deploying, upgrading,
- **expo-api-routes** — Create API routes in Expo Router with EAS Hosting
- **expo-brownfield** — Integrate Expo and React Native into existing native iOS or Android apps
- **expo-dev-client** — Build and distribute Expo development clients locally or via TestFlight
+- **expo-examples** — Canonical, version-matched patterns from the official expo/examples repo (Stripe, Clerk, Supabase, and more)
+- **expo-module** — Write Expo native modules and views with the Expo Modules API (Swift, Kotlin, TypeScript)
- **expo-tailwind-setup** — Set up Tailwind CSS v4 in Expo with NativeWind v5
- **expo-ui** — Native UI with @expo/ui: universal cross-platform components first, with SwiftUI and Jetpack Compose for platform-specific needs
- **native-data-fetching** — Network requests, API calls, caching, and offline support
@@ -79,6 +85,7 @@ Official AI agent skills from the Expo team for building, deploying, upgrading,
- **eas-update-insights** — Check EAS Update health, crash rates, adoption, and payload size
- **expo-deployment** — Deploy to iOS App Store, Android Play Store, and web hosting
- **expo-cicd-workflows** — EAS workflow YAML files for CI/CD pipelines
+- **expo-observe** — Measure startup, launch, and TTI performance with EAS Observe
### Upgrading
diff --git a/plugins/expo/skills/building-native-ui/SKILL.md b/plugins/expo/skills/building-native-ui/SKILL.md
index 4d302ca..4fd2b90 100644
--- a/plugins/expo/skills/building-native-ui/SKILL.md
+++ b/plugins/expo/skills/building-native-ui/SKILL.md
@@ -7,6 +7,8 @@ license: MIT
# Expo UI Guidelines
+> **Before picking any UI component, check `expo-ui` first.** `@expo/ui` provides native equivalents — BottomSheet, Button, Picker, Slider, Menu, Section, Switch, SegmentedControl, and more — rendered as real SwiftUI on iOS and Jetpack Compose on Android, available in Expo Go on SDK 56+ with no custom build. Load the **`expo-ui`** skill to find the right component before falling back to React Native built-ins or community libraries. This skill (`building-native-ui`) covers the surrounding structure: Expo Router navigation, layout, styling, and animations.
+
## References
Consult these resources as needed:
@@ -78,6 +80,7 @@ See `./references/route-structure.md` for detailed route conventions.
## Library Preferences
+- **For any sheet, picker, slider, toggle, menu, or grouped-form section: use `@expo/ui` (see `expo-ui` skill) before reaching for a React Native built-in or community library** — it renders native SwiftUI/Compose and works in Expo Go on SDK 56+. For grouped/settings-style rows (short, fixed-length), use `@expo/ui`'s `List` + `ListItem`. For large or unknown-length scrolling lists (feeds, search results, catalogs), use `FlatList` or `FlashList` — `@expo/ui`'s `List` is not virtualized.
- Never use modules removed from React Native such as Picker, WebView, SafeAreaView, or AsyncStorage
- Never use legacy expo-permissions
- `expo-audio` not `expo-av`
@@ -105,7 +108,7 @@ See `./references/route-structure.md` for detailed route conventions.
- Use views with built-in haptics like `` from React Native and `@react-native-community/datetimepicker`
- When a route belongs to a Stack, its first child should almost always be a ScrollView with `contentInsetAdjustmentBehavior="automatic"` set
- When adding a `ScrollView` to the page it should almost always be the first component inside the route component
-- Prefer `headerSearchBarOptions` in Stack.Screen options to add a search bar
+- Search bars belong in the Stack header via `headerSearchBarOptions` — **never** as a floating `View` pinned above content or above the tab bar. See `./references/search.md`.
- Use the `` prop on text containing data that could be copied
- Consider formatting large numbers like 1.4M or 38k
- Never use intrinsic elements like 'img' or 'div' unless in a webview or Expo DOM component
diff --git a/plugins/expo/skills/building-native-ui/references/controls.md b/plugins/expo/skills/building-native-ui/references/controls.md
index 762fe20..0b0257d 100644
--- a/plugins/expo/skills/building-native-ui/references/controls.md
+++ b/plugins/expo/skills/building-native-ui/references/controls.md
@@ -2,6 +2,8 @@
Native iOS controls provide built-in haptics, accessibility, and platform-appropriate styling.
+> **Prefer `@expo/ui` for these controls.** Its `Slider`, `Switch`, `DateTimePicker`, `Menu`, and segmented pickers render as real SwiftUI / Jetpack Compose and follow platform design conventions automatically — recommended over the React Native controls below. See the **`expo-ui`** skill; use the React Native controls below as a fallback when you're not using `@expo/ui`.
+
## Switch
Use for binary on/off settings. Has built-in haptics.
diff --git a/plugins/expo/skills/expo-overview/SKILL.md b/plugins/expo/skills/expo-overview/SKILL.md
new file mode 100644
index 0000000..b723cbc
--- /dev/null
+++ b/plugins/expo/skills/expo-overview/SKILL.md
@@ -0,0 +1,95 @@
+---
+name: expo-overview
+description: >
+ Entry point and router for any Expo or EAS task — load this skill first, before writing
+ any code, whenever the task involves building, extending, or modifying a React Native /
+ Expo app. Triggers on: any multi-screen app spec or design to implement (tabs, stacks,
+ maps, lists, navigation); building from a screenshot or design reference; adding screens
+ or features to an existing Expo project; or when the request mentions Expo, EAS, React
+ Native, or any expo-* package. Handles phrasings like "implement a mobile app", "build
+ an app from this screenshot", "create an app that looks like this design", "make my app
+ look native", "build a beautiful app", "align with iOS / SwiftUI best practices", "build
+ with native iOS patterns", "add navigation", "fetch some data", "upgrade my SDK", "add
+ Expo to my existing native app", "ship to the App Store", "push a fix without
+ resubmitting", or "I'm new to Expo, where do I start". Detects the real goal, routes to
+ the right expo-* / eas-* skill, and owns the shared environment and setup rules the
+ other Expo skills rely on.
+version: 1.0.0
+license: MIT
+---
+
+# `expo-overview` — router & shared rules for Expo / EAS
+
+## Start Here — read before doing anything
+
+**Do not guess the skill from project files alone.** Many Expo goals look similar from
+the filesystem but need different skills.
+
+1. **Read the user's goal** — what outcome do they want, in plain terms?
+2. **Classify it** using the Skill Map below, translating casual phrasing to a goal.
+3. **Confirm intent** if ambiguous ("Sounds like you want to ship to the stores — that's
+ `expo-deployment`. Right?"), then load that skill's `SKILL.md` and follow it.
+4. **Trust the leaf skill** — it has its own detection logic and steps. Don't improvise.
+
+## Skill Map (by goal)
+
+Match the goal to a category, then the skill, then load that leaf's `SKILL.md`.
+
+**Build the app**
+- `building-native-ui` — screens, navigation (tabs / stacks / modals), styling, animations, layout
+- `expo-ui` — native UI components via `@expo/ui`: BottomSheet, Picker, Slider, Button, Menu, Section, and more — real SwiftUI on iOS, Jetpack Compose on Android, available in Expo Go on SDK 56+; also drop-in replacements for `@gorhom/bottom-sheet`, `datetimepicker`, etc.
+- `expo-tailwind-setup` — Tailwind / NativeWind styling
+- `native-data-fetching` — network requests, React Query / SWR, caching, offline, route loaders
+- `expo-api-routes` — server endpoints / API routes with EAS Hosting
+- `use-dom` — run web code or reuse a web library inside native
+
+> **Component selection rule:** whenever you need a UI component (list rows, bottom sheets, pickers, sliders, menus, buttons, segmented controls, toggles), **consult `expo-ui` first** to check whether `@expo/ui` has a native equivalent before reaching for a React Native built-in or a community library. Native `@expo/ui` components give the best platform fit with zero extra install steps on SDK 56+. Load `expo-ui` alongside `building-native-ui` for any app that renders lists, detail sheets, or form controls.
+
+**Ship & operate**
+- `expo-deployment` — build, submit to the App Store / Play Store / TestFlight, and push OTA updates
+- `expo-cicd-workflows` — EAS Workflow YAML and CI/CD pipelines
+- `expo-dev-client` — custom development builds
+- `eas-update-insights` — OTA update health: crash rate, adoption, payload size
+- `expo-observe` — startup / launch / TTI performance with EAS Observe
+
+**Extend natively**
+- `expo-module` — native modules and views (Swift / Kotlin) with the Expo Modules API
+- `expo-brownfield` — embed Expo / React Native in an existing native app
+- `add-app-clip` — iOS App Clip target (AASA, smart app banner)
+
+**Maintain & learn**
+- `upgrading-expo` — upgrade the Expo SDK and fix dependency conflicts
+- `expo-examples` — canonical, version-matched integration examples (Stripe, Clerk, Supabase, …)
+
+### Translating vague asks
+
+Some everyday phrasings don't obviously map to a skill name — translate before routing:
+
+- "Make it look native" → grouped controls / settings forms = `expo-ui`; screens, navigation, animations = `building-native-ui`.
+- "Ship it" / "get an .ipa or .apk" / "push a fix without resubmitting" → all `expo-deployment` (it owns the whole release path, including OTA updates).
+- "I'm new / where do I start" → scaffold first (see First Run), then route by goal.
+
+## First Run / shared rules
+
+These apply across every Expo skill, so handle them here once instead of repeating them
+in each leaf.
+
+- **No Expo project yet?** Start one the standard way before routing to a feature skill:
+ `npx create-expo-app@latest`. Then classify the user's goal and route.
+- **Detect the SDK version** before giving version-specific advice: read the `expo`
+ version in `package.json` (and `app.json` / `app.config.{js,ts}`). Many APIs and
+ defaults differ by SDK.
+- **Managed vs. bare/prebuild**: the presence of committed `ios/` and `android/`
+ directories means native projects exist (prebuild or bare). Config-plugin and
+ native-setup steps differ — note which one the project is in.
+- **Install packages with `npx expo install `**, not raw `npm`/`yarn`/`pnpm add`,
+ so versions stay compatible with the project's SDK.
+- **EAS auth & linking** (only needed for build/submit/update/observe/workflows): check
+ login with `eas whoami`, log in with `eas login`. A project is linked when
+ `extra.eas.projectId` exists in the app config; create it with `eas init` if missing.
+
+## When NOT to use this skill
+
+- The user already named a specific Expo workflow or tool → go straight to that skill.
+- A more specific `expo-*` / `eas-*` skill obviously fits the request → use it and skip
+ the router hop.
diff --git a/plugins/expo/skills/expo-ui/SKILL.md b/plugins/expo/skills/expo-ui/SKILL.md
index fd59c8c..517b894 100644
--- a/plugins/expo/skills/expo-ui/SKILL.md
+++ b/plugins/expo/skills/expo-ui/SKILL.md
@@ -1,6 +1,6 @@
---
name: expo-ui
-description: "Build native UI with the @expo/ui package: real SwiftUI on iOS and Jetpack Compose on Android rendered from React in an Expo or React Native app. Covers universal cross-platform components (Host, Column, Row, Button, Text, List, and more imported from @expo/ui), drop-in replacements for popular React Native community libraries (BottomSheet, DateTimePicker, Slider, Menu, etc.), and platform-specific SwiftUI (@expo/ui/swift-ui, iOS only) and Jetpack Compose (@expo/ui/jetpack-compose, Android only) trees and modifiers. Use when adding or reviewing @expo/ui Host/RNHostView trees, building native-feeling UI where standard React Native components fall short (grouped settings forms with toggles, sections, menus, sheets, pickers, sliders), choosing between universal and platform-specific components, or replacing an RN community UI library with a native @expo/ui equivalent. Not for custom native modules, Expo Router navigation, Reanimated, or data fetching."
+description: "Build native UI with the @expo/ui package: real SwiftUI on iOS and Jetpack Compose on Android. Default to @expo/ui for sheets (BottomSheet), pickers, sliders, toggles, menus, and grouped-form sections — do NOT reach for Reanimated, @gorhom/bottom-sheet, or RN built-in Picker/Switch; use @expo/ui instead. Fall back to RN built-ins only when @expo/ui is missing the component. NOTE: @expo/ui List renders native grouped rows like an iOS Settings screen — it is NOT a virtualized list; use FlatList/FlashList for large datasets. Covers universal components (Host, Column, Row, Button, Text, List, BottomSheet, FieldGroup, Switch, Slider, Picker, Menu), drop-in replacements for RN community libraries, and platform-specific SwiftUI/Jetpack Compose trees. Not for Expo Router navigation, Reanimated, or data fetching."
version: 1.0.0
license: MIT
allowed-tools: "Bash(node *expo-ui/scripts/list-components.js *)"
@@ -8,9 +8,9 @@ allowed-tools: "Bash(node *expo-ui/scripts/list-components.js *)"
# Expo UI (`@expo/ui`)
-`@expo/ui` renders real native UI from React: SwiftUI on iOS, Jetpack Compose on Android. Start with its universal components (one tree for iOS, Android, and web) and drop to platform-specific SwiftUI/Jetpack Compose only when the universal layer falls short. It also ships drop-in replacements for migrating off RN community UI libraries.
+`@expo/ui` renders real native UI from React: SwiftUI on iOS, Jetpack Compose on Android. It also ships drop-in replacements for migrating off RN community UI libraries.
-> These instructions track the latest Expo SDK. The **universal** layer requires **SDK 56+**. Drop-in replacements and the platform-specific layers also exist on SDK 55. For component details on a specific SDK, refer to the Expo UI docs for that version.
+> These instructions track the latest Expo SDK. The **universal** layer requires **SDK 56+** and works in Expo Go — no custom build needed. Drop-in replacements and the platform-specific layers also exist on SDK 55. For component details on a specific SDK, refer to the Expo UI docs for that version.
## Installation
@@ -18,11 +18,57 @@ allowed-tools: "Bash(node *expo-ui/scripts/list-components.js *)"
npx expo install @expo/ui
```
-On SDK 56, `@expo/ui` works in Expo Go, so `npx expo start` runs it directly — no custom build required. On older SDKs, build a dev client first (`npx expo run:ios` / `npx expo run:android`).
-
Every `@expo/ui` tree — universal or platform-specific — must be wrapped in `Host`.
-## Choosing an approach (read this first)
+## Use @expo/ui by default — don't reach for RN alternatives first
+
+**Before using Reanimated, `@gorhom/bottom-sheet`, React Native's built-in `Switch`/`Picker`, or any community UI library for the items below, use `@expo/ui` instead.** Only fall back to RN built-ins when `@expo/ui` is missing the component.
+
+| Need | Use |
+|------|-----|
+| Slide-up sheet / bottom sheet | `BottomSheet` from `@expo/ui` — **not** Reanimated or `@gorhom/bottom-sheet` |
+| Grouped native list rows (settings/form-style) | `List` + `ListItem` from `@expo/ui` — **not** `FlatList` (see note below) |
+| Toggle | `Switch` from `@expo/ui` |
+| Slider | `Slider` from `@expo/ui` |
+| Date/time picker | `@expo/ui/community/datetimepicker` |
+| Menu | `Menu` from `@expo/ui` |
+| Form section with label | `FieldGroup` from `@expo/ui` |
+| Collapsible section | `Collapsible` from `@expo/ui` |
+
+> **`List` is NOT a virtualized scrolling list.** It renders native grouped table rows — the visual look of an iOS Settings screen or a form section, with disclosure indicators and native row styling. Each `ListItem` is a native node on the JS thread; rows are not recycled. For any list with large or unknown-length data (feeds, search results, catalogs), use **`FlatList`** or **`FlashList`** instead. `List` is the right choice for short, fixed-length groups: a settings screen, a detail panel's rows, a fixed menu.
+
+**`BottomSheet` example** (use this for map pin details, action sheets, detail panels — not Reanimated):
+
+```tsx
+import { Host, BottomSheet, Column, Text } from '@expo/ui';
+import { useState } from 'react';
+
+export default function MapScreen() {
+ const [isOpen, setIsOpen] = useState(false);
+
+ return (
+
+ setIsOpen(true)} />
+
+ setIsOpen(false)}
+ snapPoints={['half', 'full']}
+ >
+
+ Café name
+ Address
+
+
+
+
+ );
+}
+```
+
+`BottomSheet` uses `isPresented`/`onDismiss` — **not** `isOpened`, `isOpen`, `onIsOpenedChange`, or `onChange` (those are `@gorhom/bottom-sheet` props and will silently do nothing). `snapPoints` accepts `'half'`, `'full'`, `{ fraction: 0.5 }`, or `{ height: 400 }` and is optional (auto-sizes to content when omitted).
+
+## Choosing an approach
Work down this list and stop at the first layer that meets the need:
diff --git a/plugins/expo/skills/expo-ui/references/universal.md b/plugins/expo/skills/expo-ui/references/universal.md
index fb6170a..e71931c 100644
--- a/plugins/expo/skills/expo-ui/references/universal.md
+++ b/plugins/expo/skills/expo-ui/references/universal.md
@@ -61,6 +61,33 @@ export default function MyInput() {
Docs — https://docs.expo.dev/versions/latest/sdk/ui/universal/textinput/index.md
+## BottomSheet
+
+`BottomSheet` is a universal slide-up sheet. Use `isOpened` / `onIsOpenedChange` to control open state — **not** `isPresented`, `onDismiss`, or `snapPoints` (those are `@gorhom/bottom-sheet` props; the universal `BottomSheet` does not accept them).
+
+```tsx
+import { Host, BottomSheet, Column, Button, Text } from '@expo/ui';
+import { useState } from 'react';
+
+export default function MapScreen() {
+ const [isOpen, setIsOpen] = useState(false);
+
+ return (
+
+
+
+
+ Sheet content
+
+
+
+
+ );
+}
+```
+
+> **Don't confuse the universal `BottomSheet` with the drop-in `@gorhom/bottom-sheet` replacement** (`@expo/ui/community/bottom-sheet`). They are different components with different APIs. Use the universal one when building new UI; use the drop-in only when migrating an existing `@gorhom/bottom-sheet` integration. See `./drop-in-replacements.md`.
+
## Confirming the API
`@expo/ui` is versioned with the Expo SDK (e.g. `56.0.x` for SDK 56) and its API can change between SDK versions, so the **installed package's TypeScript types (`.d.ts`) are the most reliable source of truth** — they match the version in your project, while the docs track latest. Read the relevant component's `.d.ts` from the installed `@expo/ui` package in `node_modules`. Use the docs as the human-readable reference:
diff --git a/scripts/check-overview-routing.ts b/scripts/check-overview-routing.ts
new file mode 100644
index 0000000..5ccb18c
--- /dev/null
+++ b/scripts/check-overview-routing.ts
@@ -0,0 +1,59 @@
+#!/usr/bin/env bun
+
+import { mkdirSync, readFileSync, readdirSync, writeFileSync, existsSync } from "node:fs";
+import { dirname, join } from "node:path";
+
+const SKILLS_DIR = "plugins/expo/skills";
+const ROUTER_SKILL = "expo-overview";
+const ROUTER_PATH = join(SKILLS_DIR, ROUTER_SKILL, "SKILL.md");
+
+function listSkillNames(): string[] {
+ return readdirSync(SKILLS_DIR, { withFileTypes: true })
+ .filter((entry) => entry.isDirectory())
+ .map((entry) => entry.name)
+ .filter((name) => name !== ROUTER_SKILL)
+ .filter((name) => existsSync(join(SKILLS_DIR, name, "SKILL.md")))
+ .sort();
+}
+
+function writeSummary(markdown: string) {
+ const summaryPath = process.env.ROUTING_CHECK_SUMMARY_PATH;
+ if (!summaryPath) {
+ return;
+ }
+
+ mkdirSync(dirname(summaryPath), { recursive: true });
+ writeFileSync(summaryPath, `${markdown}\n`);
+}
+
+function complete(success: boolean, markdown: string): never {
+ writeSummary(markdown);
+ console.log(markdown);
+ process.exit(success ? 0 : 1);
+}
+
+const skillNames = listSkillNames();
+const router = readFileSync(ROUTER_PATH, "utf8");
+
+// A skill is "routed" when it appears as an inline-code token: `skill-name`.
+const missing = skillNames.filter((name) => !router.includes(`\`${name}\``));
+
+const markdown = [
+ "## Expo overview routing check",
+ "",
+ missing.length === 0
+ ? `Passed. All ${skillNames.length} skills are referenced in \`${ROUTER_PATH}\`.`
+ : "Failed. New skills must be added to the `expo-overview` Skill Map so the router can dispatch to them.",
+ ...(missing.length === 0
+ ? []
+ : [
+ "",
+ "Skills missing from the router:",
+ "",
+ ...missing.map((name) => `- \`${name}\``),
+ "",
+ `Add each one as an inline-code entry (\`\`${"skill-name"}\`\`) under a category in \`${ROUTER_PATH}\`.`,
+ ]),
+].join("\n");
+
+complete(missing.length === 0, markdown);