| 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)
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:
- not a Swift project, or
- 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
- 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:
- 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.
- 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):
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
fi
sleep 1
done
- 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.
- 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.