diff --git a/.changeset/expo-verify-remote.md b/.changeset/expo-verify-remote.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/expo-verify-remote.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.changeset/expo-verify-skill.md b/.changeset/expo-verify-skill.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/expo-verify-skill.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.claude/skills/README.md b/.claude/skills/README.md index 0031ac9bc9c..005a6a1836b 100644 --- a/.claude/skills/README.md +++ b/.claude/skills/README.md @@ -23,9 +23,12 @@ Edits to a `SKILL.md` take effect immediately, including in already-running sess ## Scope -Skills are Claude Code specific. Cursor does not read this directory; it uses `.cursor/rules/` and -`AGENTS.md`. When a repo rule changes, update `AGENTS.md` first, then mirror the change here and in -`.cursor/rules/` where relevant. +Skills here are Claude Code specific, except `verify-clerk-expo`, the agent-neutral skill that drives +`@clerk/expo` on a simulator or emulator. Its files live here, and `.cursor/skills/verify-clerk-expo` +is a symlink to this directory so Cursor reads the same skill. Edit it here, not through the +symlink. For the other skills, Cursor uses `.cursor/rules/` and `AGENTS.md`. When a repo rule +changes, update `AGENTS.md` first, then mirror the change here and in `.cursor/rules/` where +relevant. ## Maintaining a skill @@ -41,7 +44,8 @@ Skills are Claude Code specific. Cursor does not read this directory; it uses `. ## Skills in this repo -| Skill | Use it for | -| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| `clerk-monorepo` | Day-to-day work in the monorepo: setup, build/test loops, the package map, changesets, commits, PRs, breaking-change checks. | -| `mosaic` | Mosaic flow UI: authoring machines, controllers, and views, and migrating a legacy component into the split (with parity verification). | +| Skill | Use it for | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| `clerk-monorepo` | Day-to-day work in the monorepo: setup, build/test loops, the package map, changesets, commits, PRs, breaking-change checks. | +| `mosaic` | Mosaic flow UI: authoring machines, controllers, and views, and migrating a legacy component into the split (with parity verification). | +| `verify-clerk-expo` | Proving a `@clerk/expo` change on an iOS simulator or Android emulator, local or borrowed on a CI runner, with video and app state as evidence. | diff --git a/.claude/skills/verify-clerk-expo/.gitignore b/.claude/skills/verify-clerk-expo/.gitignore new file mode 100644 index 00000000000..603ba938f6c --- /dev/null +++ b/.claude/skills/verify-clerk-expo/.gitignore @@ -0,0 +1,3 @@ +.e2e/ +.verify/ +specs/explored/ diff --git a/.claude/skills/verify-clerk-expo/SKILL.md b/.claude/skills/verify-clerk-expo/SKILL.md new file mode 100644 index 00000000000..65ef5d59b7f --- /dev/null +++ b/.claude/skills/verify-clerk-expo/SKILL.md @@ -0,0 +1,262 @@ +--- +name: verify-clerk-expo +description: Drive @clerk/expo in the expo-native fixture app (native AuthView, UserButton, UserProfileView, custom useSignIn and useSignUp flows, token cache) on an iOS simulator or Android emulator against a real Clerk development instance that the session creates and deletes, and capture video, screenshots, and app state as evidence. The device runs on this Mac, or on a CI runner when the machine cannot run it. Use it to prove any change to packages/expo or the fixture works before calling it done, to reproduce a UI bug, or to run the golden regression specs. +--- + +# verify-clerk-expo + +`.claude/skills/verify-clerk-expo/bin/control-clerk-expo` is a control CLI over [e2e](https://github.com/tester-army/e2e) 0.18.0 and `@e2e-dev/mobile` 0.10.0. It builds the `expo-native` fixture in `integration/templates/expo-native`, leases a simulator or emulator, creates one Clerk application for the worktree, seeds `+clerk_test` users, runs specs, and keeps the evidence. On a Mac the device is local, the fixture is a Debug dev client, and Metro serves your working tree to it. On a machine that cannot run the device, the CLI leases one on a GitHub Actions runner, and the runner builds your pushed commit as a Release app with the JS embedded. The verbs, specs, and evidence are the same. + +No change to `@clerk/expo` UI or auth behavior is done until a `run` on the real fixture shows the changed behavior, on each platform the change touches. + +Run every command from the repo root. In the prose below, `doctor`, `up`, `run`, `screen`, `attach`, and `down` are verbs of that CLI. Paths that begin `specs/`, `features/`, `references/`, `src/`, `test/`, or `.verify/` are inside `.claude/skills/verify-clerk-expo/`. Every verb but `attach` takes `--platform ios|android`, and iOS is the default. Every verb takes `--json` and then prints one `{ "ok": ... }` object. Exit codes are 0 for success, 1 for a failing spec, 2 for a usage error, and 3 for a failed precondition. Every error prints a `fix` line. + +CI runs none of these specs. The `Verify Skill Tests` job in `.github/workflows/ci.yml` runs the skill's unit tests and `tsc`. The device tests that gate a pull request are `integration/tests/expo-native/*.e2e.ts`, which `.github/workflows/expo-native-build.yml` runs against a Release build of the same fixture with the same `e2e` engine. This skill is the development loop, and a regression test that must run on every pull request belongs in `integration/tests/expo-native/`. + +The fixture links `@clerk/expo`, `@clerk/expo-biometrics`, and `@clerk/expo-google-signin` from the workspace. It does not install `@clerk/expo-passkeys`, so the skill cannot verify passkeys. + +## Launch + +Set up each machine once. + +1. Install Node 24, at 24.8.0 or newer. For a local iOS device, install Xcode with an iOS simulator runtime. For a local Android device, install Android Studio with the SDK, the emulator, and Java 21. The CLI looks for the SDK in `ANDROID_HOME`, `ANDROID_SDK_ROOT`, and `~/Library/Android/sdk`. +2. Create the iOS template simulator, which the CLI clones to make each simulator it drives, for example with `xcrun simctl clone "iPhone Air" "Clerk Verify Template iOS"`. If this Mac sends HTTPS through a debugging proxy, boot the template once, install and trust the proxy's CA in it, and shut it down. Android needs no template: the first `up --platform android` writes the `Clerk_Verify_Pixel` AVD. +3. Give the machine the team's Clerk Platform API key. Set `CLERK_PLATFORM_API_KEY`, or set `CLERK_PLATFORM_API_KEY_FILE` to a file that only you can read (mode 0600). To keep the key in 1Password instead, install the 1Password CLI, turn on its desktop app integration, and put the key's secret reference in `VERIFY_PLATFORM_KEY_REFERENCE` or as the one line of `~/.verify/clerk-platform-key-reference`. The reference has the shape `op:////credential`, and the team's private setup note has the real one. Never put the key or the reference in a file inside a repository. + +Then, in each worktree: + +```console +$ pnpm install # once per worktree +$ npm ci --prefix .claude/skills/verify-clerk-expo # once per worktree +$ .claude/skills/verify-clerk-expo/bin/control-clerk-expo doctor --platform ios +$ .claude/skills/verify-clerk-expo/bin/control-clerk-expo up --platform ios +backend local this Mac runs the simulator itself +instance creating verify-throwaway-until-- in org_3KHungJxbvIscuSvy8oos5MHAli +build local building... +build turbo build @clerk/expo, @clerk/expo-biometrics, @clerk/expo-google-signin +instance up in 0.7s on standard, 61 settings match src/core/instances/base.json +build expo prebuild --clean --platform ios +build xcodebuild Debug (dev client) +build local built in 110s +device verify-ios-1 cloning Clerk Verify Template iOS +install on verify-ios-1 +watch packages/expo tsdown --watch (pid ) +metro :8082 expo start (pid ) +metro :8082 bundling ios once so the first launch does not wait on Metro +device verify-ios-1 local leased by this worktree installed +``` + +The skill is outside the pnpm workspace, so `npm ci` installs its pinned `e2e` and `agent-device` from the skill's own lockfile. The sample is the first `up` in a worktree, without its `instances` and `clerk` lines. The lane is ready when `up` prints the `device` line that ends in `installed `, which is its last line. `run` does the same steps itself, so `up` only starts the slow part early. Teardown is `down` (see [Cleanup](#cleanup)). + +`up` does four things: + +- It builds the dev client when no build matches the native inputs: `turbo build` for the three packages, `expo prebuild --clean`, then `xcodebuild` or `gradlew assembleDebug`. The build overwrites the fixture's generated `package.json`, `ios/`, and `android/`. A change to anything else reuses the build and prints `build local reused`. +- It creates this worktree's Clerk application through Clerk's Platform API, in the team's verification workspace, and puts its development instance on the standard settings in `src/core/instances/base.json`. The application holds only the users that this worktree's runs create, and `down` deletes it. [Test instances](references/instances.md) has the credential lookup, the application's lifetime, and its limits. +- It leases a lane and installs the build. An iOS lane is a clone of the template named `verify-ios-`. An Android lane boots `Clerk_Verify_Pixel` read-only as `emulator-5560` or `emulator-5562`. +- It starts `tsdown --watch` in `packages/expo` (the watch build) and `expo start` on the lane's Metro port. + +`up` is idempotent. It keeps a lease that this worktree already holds. A failed `up` stops the Metro and the watch build that it started. + +On a local device a JS change reaches the app with no build. Before the specs start, `run` waits until the watch build has caught up and Metro serves the current code, and it fails with `NOT_READY` and the path of the Metro log when Metro never does. [How a change reaches the app](references/freshness.md) has the native inputs, the checks, the ports, and the logs. It also says what to do after a change to another workspace package, such as `@clerk/clerk-js` or `@clerk/shared`. While Metro runs, never run `pnpm --filter @clerk/expo build` or a build of a package that `@clerk/expo` depends on, because the build deletes the `dist` that Metro serves. + +A worktree can hold one lane of each platform. The two lanes share the watch build and the application, and each has its own Metro. Run them one after the other. The application serves one run at a time, because a run can change its settings. A `run` that starts while a run on the other platform is driving leases its device and then fails with `DEVICE_BUSY`, and its fix is to let that run finish and rerun. + +A Mac has four iOS lanes and two Android lanes, shared by every worktree on it. When all are taken, `up` and `run` fail with `POOL_FULL`, and `--wait ` on either verb waits for a lane. Never drive a simulator or emulator that the CLI did not create, the template, a physical device, or a lane that another worktree holds. [Local devices](references/devices.md) says how to find a lane's UDID or serial. + +With the key in 1Password, a command that needs it prints `wait reading the team key from 1Password; approve the request in the 1Password app within 60s`, and the 1Password app asks the person at the Mac to approve. An agent cannot approve the request, so tell the person before the first command. + +### Borrow a device on a CI runner + +A machine that is not a Mac cannot run the simulator, and a machine with no hardware virtualization cannot run the emulator. There the CLI leases a device on a GitHub Actions runner and drives it through a tunnel. That machine needs Node 24.8.0 or newer on 24, the Platform API key, and access to GitHub, and `doctor` checks each. The session builds a pushed commit, never your working tree, so commit and push before `up` or `run`. + +```console +$ git push +$ .claude/skills/verify-clerk-expo/bin/control-clerk-expo up --platform ios --backend remote +backend remote forced by --backend remote +build github-actions commit the session builds it +device remote ios starting session on macos-26 (idle stop 15 min, cap 60 min) +device remote ios tunnel up, iPhone 17 Pro on macos-26 +build github-actions built in 1432s on macos-26 +device iPhone 17 Pro on macos-26 remote leased by this worktree installed +$ .claude/skills/verify-clerk-expo/bin/control-clerk-expo down # ends the runner job +``` + +The `backend` line says which backend the CLI chose and why. This transcript is from a Mac, where `--backend remote` forced the remote backend, and it leaves out the `instance`, `clerk`, `install`, and `wait` lines and the line with the run's URL. On a machine that cannot run the device, `up` needs no flag, and the `backend` line says why the local backend is out. `--backend local` or `--backend remote` on `doctor`, `up`, or `run` forces a backend, and a worktree that holds a lease keeps its backend until `down`. `--runner