Pure dispatcher: picks the right E2E executor for a Swift test request from context (platform × intent × verification kind) and hands off. Routes to the scaffold/MCP-sim/QA/design-review skills.
Scanned 9/5/2026
Install to Claude Code
npx -y skills add Paretofilm/superpowers-gstack --skill e2e-route --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of E2e Route?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/paretofilm-e2e-route)More formats (shields.io, HTML) on the badges page.
---
name: e2e-route
description: |
Pure dispatcher: picks the right E2E executor for a Swift test request from
context (platform × intent × verification kind) and hands off. Routes to the
scaffold/MCP-sim/QA/design-review skills.
---
# e2e-route
A **pure dispatcher** for Swift E2E testing. Given a test request it reads context
deterministically, chooses the right E2E executor, and dispatches — it does **not** own
execution. It is the thin routing layer above the scaffold skills and the MCP-live
tools, analogous to how `pitfall-verification` orchestrates lenses without
reimplementing them.
Invoke with: `/superpowers-gstack:e2e-route`
## Phase 0 — Self-check
| Check | Detect via | Refuse-message |
|---|---|---|
| Swift project | `*.xcodeproj` or `Package.swift` in cwd | "Not a Swift project. /e2e-route needs a Swift app to route tests for." |
| Detectable target | a scheme/target is discoverable (see Platform below) | If none: ask the user once which scheme to target; else refuse "No scheme/target detected — cannot route." |
## Routing inputs (read in order)
### 1. Intent — exploratory (MCP-live) vs committed (XCUITest)
- **CI forces committed.** Detect CI by running Bash:
```bash
[ -n "${CI:-}${GITHUB_ACTIONS:-}" ] && echo CI
```
If set, force *committed* — a CI context cannot drive a live MCP simulator session.
- **Else infer from the user's verbs:**
- `utforsk` / `sjekk` / `dogfood` / `trykk gjennom` / `explore` / `smoke` / "press through the flow" → **MCP-live**
- `må aldri knekke` / `regresjon` / `regression` / `CI` / "lock this down" → **committed**
- **Ambiguous** (no verb signal, not in CI) → ask the user once:
"Exploratory live run, or committed regression test?"
### 2. Platform — iOS vs macOS
Detect via `mcp__XcodeBuildMCP__show_build_settings` / `list_schemes` (read
`SDKROOT` / `SUPPORTED_PLATFORMS`); if MCP unavailable, fall back to
`grep -E 'SDKROOT|SUPPORTED_PLATFORMS' *.xcodeproj/project.pbxproj` or read
`.gstack/track`. Ask only if undetectable.
**Multiplatform tiebreak — REQUIRED (the user's premise is "both platforms").** If the
detected target supports BOTH iOS and macOS (`SUPPORTED_PLATFORMS` lists both
`iphoneos` and `macosx`, or `.gstack/track` = `both`, or two schemes one per
platform), the platform is NOT uniquely determined. Resolve in order:
- (a) If the test request names a platform ("test the iPhone flow", "the macOS menu
bar") → use it.
- (b) Else ask the user once: "This app targets both iOS and macOS — route this test
to iOS, macOS, or both?"
- (c) `both` → emit **two** decision blocks (one iOS, one macOS), each routing to its
platform's executor. **Run them SEQUENTIALLY.** The scaffolds handle coexistence via
their shared TARGET_DIR convention: on a multiplatform target each uses a
platform-suffixed test directory — `<App>iOSUITests/` for /ios-e2e-scaffold,
`<App>macOSUITests/` for /macos-e2e-scaffold — and each scaffold's "already
scaffolded" Phase-0 check globs `*UITests/` name-agnostically but EXCLUDES the
sibling platform's suffixed directory (`*macOSUITests/` ignored by ios,
`*iOSUITests/` ignored by macos). So whichever runs second is not blocked by the
first's files, and the two never collide on the same directory or the same
`xcodegen.yml`/`project.pbxproj` UI-test target. Each decision block's "Next action"
must name its platform-specific target dir.
### 3. Verification kind (optional refinement)
Functional / accessibility-assertion vs visual. A request about layout, spacing,
colour, dark mode, or "does it look right" → the visual-regression row.
## Routing table (the oracle)
| Intent | Platform | Executor |
|---|---|---|
| Committed regression | macOS | `/macos-e2e-scaffold` + its xcresult runner |
| Committed regression | iOS | `/ios-e2e-scaffold` |
| Exploratory / live | macOS | `XcodeBuildMCP` UI-automation (`snapshot_ui` → tap → screenshot) |
| Exploratory / live | iOS | `ios-simulator` MCP (`ui_find_element` / `ui_tap`) or `/ios-qa` |
| Visual regression | iOS | screenshot/vision diff + `/ios-design-review` |
| Visual regression | macOS | screenshot/vision diff + `/design-review` (generic designer's-eye QA — no macOS-specific reviewer exists) |
| Visual exploration (Tier-2 escalation) | iOS/iPadOS | `/superpowers-gstack:ios-visual-explore` — Gemini computer-use drives the app visually. Route here when the accessibility tree is insufficient (visual landmarks, layout regressions XCUITest can't assert, open-ended "find visual issues" missions). Not a first resort; paid Gemini API per run. |
## Fallback
Degrade to the MCP-live row for the platform **only when the chosen scaffold's Phase 0
actually refuses** — i.e. one of the scaffold's three refuse-conditions fires:
1. not a Swift project, or
2. no SwiftUI app for the routed platform detected — no SwiftUI scene (e.g. a
UIKit-/AppKit-only app) or no platform-discriminating signal (e.g. a pure-iOS app
routed to /macos-e2e-scaffold, or vice versa), or
3. a UI-test target already exists.
Emit an explicit note naming the unmet precondition. No false promise; always a way
forward.
**SPM-only is NOT a fallback trigger.** The scaffold skills accept `Package.swift`
projects and proceed — they generate files under `Tests/<TARGET_DIR>/` (`<App>UITests`,
platform-suffixed on multiplatform targets) with a "SwiftPM can't host a UI-test
bundle; add an .xcodeproj" warning. So for SPM-only iOS/macOS apps
the dispatcher still routes to the scaffold skill; it does NOT degrade to MCP-live.
## Output — routing-decision block
Emit **one block per resolved platform** — normally exactly one; **two when the
multiplatform tiebreak resolved to `both`** (one iOS block + one macOS block) — then
stop. Do not build/tap/assert; hand control back after emitting.
```
## /e2e-route decision
Detected: platform=<iOS|macOS>, intent=<committed|exploratory|visual>, source=<scheme|.gstack/track|asked>
Chosen executor: <skill or MCP sequence>
Why: <one line tying context → routing cell>
Next action: <exact /skill to invoke OR exact MCP call sequence>
```
## What this skill is NOT
- **Not an executor.** It does not build, tap, assert, or run tests itself — it names
the executor and the exact next action, then hands off.
- **Not a scaffolder.** It does not modify app files or generate test stubs — the
scaffold skills (`/ios-e2e-scaffold`, `/macos-e2e-scaffold`) do that.
- **Not a QA-report writer.** Use `/ios-qa` / `/qa` for live QA reports.
- **Not auto-hooked.** Manual `/e2e-route` + CLAUDE.md routing only — no
PostToolUse/UserPromptSubmit hook (avoids hijacking existing `/qa` / `/ios-qa`
routing).
## Shared foundation
Both executors locate controls via the accessibility tree the same way, using the
`<ViewName>_<ControlType>_<Purpose>` identifier convention (snake_case) **owned by the
scaffold skills** — `e2e-route` points at it but does not own or apply identifiers.
This is why the routing layer stays thin: element-lookup is identical whether the
executor is `ui_find_element("PlanListView_Button_GeneratePlan")` (MCP-live) or
`app.buttons["PlanListView_Button_GeneratePlan"].tap()` (XCUITest).
## Simulator readiness ladder (MCP-live only)
When the routed executor is **MCP-live** (`ios-simulator` / `XcodeBuildMCP` / raw
`idb`+`simctl`), readiness is not a single boolean. "Booted" exists at three levels that
lag each other unpredictably — sometimes by **minutes** on a cold iOS 26 boot:
| Level | Signal | Reality |
|---|---|---|
| 1 | `xcrun simctl list devices booted` shows `Booted` | CoreSimulator state — earliest, a diagnostic not a gate |
| 2 | `xcrun simctl bootstatus <udid> -b` returns | launchd/system services — neither sufficient nor necessary for automation; a diagnostic not a gate |
| 3 | `describe-all` / `snapshot_ui` returns a non-degenerate tree | SpringBoard rendered, AX bridge live — **the only readiness gate** |
These are **not a clean monotonic ladder.** In the 2026-06-27 incident `bootstatus -b`
(level 2) was *still blocking* after the UI (level 3) was already up. Treat level 3 as the
only readiness gate; levels 1–2 are diagnostics you read, never conditions you wait on.
**Rules — these prevent stranded passive waits:**
1. **Poll the precondition you actually need (level 3), not a proxy.** A device that
reports `Booted` can still show a black screen with an empty AX tree. Gate the first
probe on a non-degenerate tree, not on `Booted`.
2. **Never block on `bootstatus -b` as your wakeup signal.** It can block far longer than
the device takes to become usable — or indefinitely, if its boot-completion condition
is never met — so it always needs an external timeout. Instead run a bounded
`run_in_background` until-loop that *exits* the moment the level-3 precondition is true
(one notification, within seconds):
```bash
# Ready ≠ "Booted". A cold-boot AX tree can contain elements whose frames are
# all null/0 (degenerate) — so gate on REAL geometry, not just on nodes existing.
# REPLACE the ready() body with a check against YOUR tool's actual output (an
# element with a NON-ZERO frame width — a label alone does not prove geometry).
# Confirm field names once against a real dump. Note: idb roles are bare ("Button",
# "Application"), NOT AX-prefixed — do not grep for "AXButton". Until you replace it,
# ready() fails loudly, so the loop can never falsely report success.
ready() {
echo "readiness predicate not implemented — replace ready()" >&2
return 1
}
start=$SECONDS
until ready; do
if [ $((SECONDS - start)) -gt 120 ]; then
echo "TIMEOUT: AX tree still degenerate" >&2
exit 1 # fail loudly — a wrapping background task must NOT notify "ready"
fi
sleep 1
done
```
3. **Always pair "I'll be notified when the task finishes" with a fallback wakeup.** The
harness re-invokes on task *completion*; a hung task strands you forever. Set a
`ScheduleWakeup` fallback (or a bounded `Monitor` timeout) so a hang can't cost 10
idle minutes. If your only plan is "I'll be notified automatically," you have no plan
for the notification not arriving.
4. **Never emit a bare "waiting."** Either you are actively polling (a background
until-loop that exits) or you hand control back. Idle-waiting on an opaque blocking
command is the anti-pattern that wasted real time on a Fase-2 iPad spike (2026-06-27).
## Relationship to other skills
| Skill | Layer | Asks |
|---|---|---|
| **`e2e-route`** | **routing** | **Which executor for this test?** |
| `ios-e2e-scaffold` / `macos-e2e-scaffold` | project | Is this E2E-tested? |
| `ios-qa` | live | Does the running app behave? |
| `ios-design-review` | visual | Does it look right on device? |
| `pitfall-verification` | artifact | Will this work? |
`e2e-route` sits above the project-layer scaffold skills and the MCP-live tools. It
decides the *executor*; the executor decides *how*.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!