E2e route
How to use Superpowers and GStack together with Claude Code. Workflow manual, routing plugin, and CLAUDE.md generator. Use both frameworks — each in the phase where it excels.
npx -y skills add Paretofilm/superpowers-gstack --skill e2e-routeAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
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.
SKILL.md
10.8 KB, as published. Nobody here has run it
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:
If set, force committed — a CI context cannot drive a live MCP simulator session.[ -n "${CI:-}${GITHUB_ACTIONS:-}" ] && echo CI - Else infer from the user's verbs:
utforsk/sjekk/dogfood/trykk gjennom/explore/smoke/ "press through the flow" → MCP-livemå 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 samexcodegen.yml/project.pbxprojUI-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//qafor live QA reports. - Not auto-hooked. Manual
/e2e-route+ CLAUDE.md routing only — no PostToolUse/UserPromptSubmit hook (avoids hijacking existing/qa//ios-qarouting).
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
Bootedcan still show a black screen with an empty AX tree. Gate the first probe on a non-degenerate tree, not onBooted. - Never block on
bootstatus -bas 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 boundedrun_in_backgrounduntil-loop that exits the moment the level-3 precondition is true (one notification, within seconds):# 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 - 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
ScheduleWakeupfallback (or a boundedMonitortimeout) 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.