---
name: graphql-local
description: Prepare the frontend-content app to connect to a local GraphQL router, cloning it if needed, routing one or more subgraphs to a locally running instance, or reverting subgraphs back to QA
---

This skill manages the local GraphQL development stack:
1. Ensures `graphql-router` is cloned locally
2. Asks whether to route subgraph(s) local or revert subgraph(s) to QA
3. Updates `graphql-router/config.yaml` accordingly (localhost or QA URLs)
4. Confirms `frontend-content` points to the local router
5. Optionally boots the whole stack — first ensuring the router binary + `supergraph.graphql` exist (downloading/composing them if needed), then starting subgraphs → router → frontend, **each in its own Terminal.app tab** so the user keeps control of every process

When the action is **revert**, skip the local-port / startup steps (a QA-backed subgraph needs no local process) — just update `config.yaml` and note the router will hot-reload (or needs a restart).

## Context

- The router is an Apollo Router (Rust). It federates 17 subgraphs.
- **Router repo**: `~/Repos/graphql-router` — clone URL: `git@github.com:theorchard/graphql-router.git`
- **Router config**: `config.yaml` (gitignored). Template: `config-qa.yaml`. Override subgraph URLs via `override_subgraph_url:` section.
- **Router local port**: `127.0.0.1:8087` (set in `config.yaml` `supergraph.listen`). This matches `GRAPHQL_URL=http://localhost:8087/graphql` already in `frontend-content/.env`. The template's `health_check.listen` is also `:8080`, which clashes with the frontend — disable it (`health_check.enabled: false`) or move it to a free port when creating `config.yaml`.
- **Router binary**: the precompiled `router` binary is gitignored / not committed. Download it into the repo with `curl -sSL https://router.apollo.dev/download/nix/latest | sh` (run from `~/Repos/graphql-router`). No Rust toolchain required for this path.
- **Supergraph SDL** (`supergraph.graphql`): NOT present on a fresh clone — the router cannot start without it. Compose it **locally from the subgraphs** with Rover (`rover supergraph compose --config ./scripts/supergraph.yaml > ./supergraph.graphql`), or regenerate everything with `./scripts/generate-supergraph.sh` (introspects each QA subgraph, writes a per-subgraph SDL file, rebuilds `supergraph.yaml`, then composes). **No Apollo Studio credential is required** — composition reads subgraph SDL, not Studio. The only possible interruption is a one-time Apollo **T&C acceptance** the first time `rover supergraph compose` runs (an interactive prompt, not a credential). The same supergraph works whether a subgraph is QA-backed or locally overridden, because runtime routing is driven by `config.yaml`'s `override_subgraph_url`, not by the URLs baked into the SDL.
- **Tooling**: `rover` (`~/.rover/bin/rover`) and `docker` are commonly already installed; `cargo`/`rustup` and `protoc` usually are NOT. Check before assuming a from-source build is possible. If any required tool/library is missing when you need it, don't abort — tell the user what's missing and the exact install command, offer to run it (or have them run it via `! <command>`), then continue the process from where it stalled (see the install rule below).
- **Node version (nvm)**: every TS project here — all `graphql-*` subgraphs and `frontend-content` — pins a Node version (via `.nvmrc` and/or `package.json` `engines`, often Node ≥24), but the login shell may default to an old Node (e.g. 14.x), which makes `yarn`/`yarn start` fail with `The engine "node" is incompatible`. Before ANY `yarn install` or `yarn start` in these repos, select the pinned Node with nvm. nvm is a shell function (not on `PATH`), so it must be sourced in the SAME command:
  ```bash
  export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
  cd ~/Repos/<repo> && { nvm use || nvm install; }   # reads .nvmrc; installs that version if not yet present
  ```
  `nvm use` (no arg) reads the repo's `.nvmrc`; if that version isn't installed it exits non-zero and `nvm install` then installs+uses it. If a repo has no `.nvmrc`, check `package.json` `engines.node` and `nvm install <version>` explicitly. Always run this before the `PORT=<port> yarn start` / `yarn start` commands below.
- **Frontend GRAPHQL_URL**: already set to `http://localhost:8087/graphql` in `.env` — no change needed unless the user overrides it.
- All subgraphs with their QA URLs:
  - `graphql-abacus` → `https://qa-graphql-abacus.theorchard.io/graphql`
  - `graphql-account` → `https://qa-graphql-account.theorchard.io/graphql`
  - `graphql-analytics` → `https://qa-graphql-analytics.theorchard.io/graphql`
  - `graphql-audience` → `https://qa-graphql-audience.theorchard.io/graphql`
  - `graphql-collaborator` → `https://qa-graphql-collaborator.theorchard.io/graphql`
  - `graphql-content-review` → `https://qa-graphql-content-review.theorchard.io/graphql`
  - `graphql-distribution` → `https://qa-graphql-distribution.theorchard.io/graphql` (local default port: 8081)
  - `graphql-knowledge` → `https://qa-graphql-knowledge.theorchard.io/graphql`
  - `graphql-knowledge-search` → `https://qa-graphql-knowledge-search.theorchard.io/graphql`
  - `graphql-neighbouring-rights` → `https://qa-graphql-neighbouring-rights.theorchard.io/graphql`
  - `graphql-participant` → `https://qa-graphql-participant.theorchard.io/graphql`
  - `graphql-product` → `https://qa-graphql-product.theorchard.io/graphql`
  - `graphql-publishing` → `https://qa-graphql-publishing.theorchard.io/graphql`
  - `graphql-sr-delivery` → `https://qa-graphql-sr-delivery.theorchard.io/graphql`
  - `graphql-tax-payment` → `https://qa-graphql-tax-payment.theorchard.io/graphql`
  - `graphql-user` → `https://qa-graphql-user.theorchard.io/graphql`

### Start commands

- **Subgraph** (each `~/Repos/<subgraph-name>` is a Node/TS Apollo Server): first select the pinned Node via nvm (see the Node version note above), then `yarn start`. The port comes from the `PORT` env var (loaded via `dotenv`, which does NOT override an already-set `process.env.PORT`). Subgraph `.env` files commonly default to `PORT=8080`, so you MUST start it with the chosen port inline, to match the router override. Never edit the subgraph's committed `.env`. Full command:
  ```bash
  export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
  cd ~/Repos/<subgraph-name> && { nvm use || nvm install; } && PORT=<port> yarn start
  ```
- **Router**: prefer the **precompiled Apollo Router binary**, but note it is **NOT committed** — a fresh clone has no `router` binary and no `supergraph.graphql`. Both must be obtained first (see step 6-prep below). **The precompiled binary does NOT contain the repo's custom Rust plugins (`theorchard.require_apollo_client_name` and `pde.auth_enforcement`).** If those are declared in `config.yaml` (they are in the QA template), the binary refuses to start with `Additional properties are not allowed ('theorchard.require_apollo_client_name' was unexpected)` / `('pde.auth_enforcement' was unexpected)` and exits — so they MUST be commented out of `config.yaml` for the precompiled path (see step 6c). Consequently auth/client-name enforcement is off locally; every request must still include the `apollographql-client-name` header (the health-checks already do). Once the binary and supergraph exist, and the custom plugins are commented out, start it with:
  ```bash
  ./router --dev --config config.yaml --hot-reload --supergraph supergraph.graphql
  ```
  Alternatively `make dev` (or `cargo run -- --dev --config config.yaml --hot-reload --supergraph supergraph.graphql`) compiles from source — slower, needs the Rust toolchain (`rustup`, version pinned in `rust-toolchain.toml`), `protobuf`, and sometimes `cmake`. The from-source build DOES include the custom plugins, so leave them enabled in `config.yaml` for that path. Only needed if you changed Rust plugin code. Prefer the precompiled binary.
- **Frontend**: select the pinned Node via nvm, then `yarn start` (runs `frontend start`, served on `localhost:8080` per `CLI_SERVER_PORT`):
  ```bash
  export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
  cd ~/Repos/frontend-content && { nvm use || nvm install; } && yarn start
  ```

### Launching each server in its own Terminal tab (macOS Terminal.app)

When booting the stack, DO NOT launch the servers as backgrounded Bash tasks. Instead open **each long-running server in its own Terminal.app tab** so the user keeps an interactive, controllable session per process (Ctrl-C, restart, read logs) after this skill finishes. This applies to every subgraph (`yarn start`), the router, and the frontend (`yarn start`).

Mechanism (this environment is `TERM_PROGRAM=Apple_Terminal`, Terminal.app — no iTerm): write the server's full command to a temp script (avoids AppleScript quoting issues), then use `osascript` to open a new tab (Cmd-T via System Events) and run that script in it. Run this as a normal **foreground** Bash call — `osascript` returns as soon as the tab is launched, so do NOT use `run_in_background`. Template (substitute the per-server command):
```bash
F=$(mktemp /tmp/graphql-local-<name>.XXXXXX.sh)
cat > "$F" <<'SCRIPT'
export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
cd ~/Repos/<repo> && { nvm use || nvm install; } && PORT=<port> yarn start
SCRIPT
osascript \
  -e 'tell application "Terminal" to activate' \
  -e 'tell application "System Events" to keystroke "t" using command down' \
  -e 'delay 0.4' \
  -e "tell application \"Terminal\" to do script \"source $F\" in front window"
```
Notes:
- The heredoc is single-quoted (`'SCRIPT'`) so `$HOME`, `~`, and `\.` are written literally and expand at runtime inside the tab — exactly the nvm-sourcing pattern required for these TS repos.
- The new tab stays interactive after the process exits or is Ctrl-C'd, which is the whole point — the user controls each server afterward.
- **Accessibility permission:** the System Events `keystroke` may, on first use, prompt to grant Terminal permission to control the computer (System Settings → Privacy & Security → Accessibility). If the keystroke is blocked, the new tab won't open. Tell the user to grant it, or fall back to opening a new **window** instead of a tab (omit the System Events line and the `in front window` clause — a bare `do script "source $F"` opens a new window, no accessibility needed).
- Because the server runs in a separate Terminal tab, Claude CANNOT read its stdout. Health-check each launched server with `curl` against its port (as below). If it never comes up, you can't dump a background log — tell the user to look at that server's Terminal tab for the error.

---

## Steps

### 1. Check if graphql-router is cloned

```bash
ls ~/Repos/graphql-router 2>/dev/null || echo "NOT_FOUND"
```

If `NOT_FOUND`:
```bash
git clone git@github.com:theorchard/graphql-router.git ~/Repos/graphql-router
```

Confirm the clone succeeded before continuing.

### 2. Check if config.yaml exists

```bash
ls ~/Repos/graphql-router/config.yaml 2>/dev/null || echo "NOT_FOUND"
```

If `NOT_FOUND`, create it from the QA template and change the listen address to match the frontend's expected port:
```bash
cp ~/Repos/graphql-router/config-qa.yaml ~/Repos/graphql-router/config.yaml
```
Then in `config.yaml`, replace:
```yaml
supergraph:
  listen: "0.0.0.0:8080"
```
with:
```yaml
supergraph:
  listen: "127.0.0.1:8087"
```
Also disable the health check, whose template `listen` is `0.0.0.0:8080` and would collide with the frontend on `:8080`:
```yaml
health_check:
  listen: "0.0.0.0:8088"
  enabled: false
```

If it already exists, read the current `override_subgraph_url` section to show the user the current state:
```bash
grep -A 50 'override_subgraph_url:' ~/Repos/graphql-router/config.yaml
```

### 3. Ask what to do

First show which subgraphs are currently overridden to a localhost URL (from the `grep` in step 2) so the user can see the current state. Then ask whether they want to:
- **Route a subgraph local** — point one or more subgraphs at a local instance, or
- **Revert a subgraph to QA** — restore one or more subgraphs to their QA endpoint, or
- **Revert everything to QA** — restore all subgraphs to their QA URLs (a clean reset).

**If routing local:** ask which subgraph(s) and, for each, what local port it runs on.
- For `graphql-distribution`, suggest the default port `8081` (already used in `scripts/supergraph.yaml`).
- For any other subgraph without a known default, ask the user for the port.

**If reverting:** offer the currently-overridden (localhost) subgraphs as the candidates to revert, since those are the only ones not already on QA. Then go to step 4b.

If args were passed to this skill, parse them and skip asking:
- `/graphql-local graphql-sr-delivery:4001` → route `graphql-sr-delivery` local on port 4001 (`<subgraph-name>:<port>` pairs).
- `/graphql-local revert graphql-sr-delivery` → revert that subgraph to QA.
- `/graphql-local revert all` (or just `revert`) → revert all subgraphs to QA.

### 4. Update config.yaml override_subgraph_url

For each selected subgraph, update its entry in the `override_subgraph_url:` section of `~/Repos/graphql-router/config.yaml` to `http://localhost:<port>/graphql`.

Example — if the user selected `graphql-sr-delivery` on port `4001`:
```yaml
override_subgraph_url:
  ...
  graphql-sr-delivery: "http://localhost:4001/graphql"
  ...
```

All other subgraphs remain pointing to their QA URLs.

Use the Edit tool to make these changes precisely — do not rewrite the entire file.

**Port alignment:** the port in this override MUST equal the port the subgraph actually listens on. Subgraph `.env` files often default to `PORT=8080`, so the subgraph must be launched with `PORT=<chosen-port>` (see step 6). Note any clashes — the router listens on `8087` and the frontend on `8080`, so do not assign those to a subgraph.

### 4b. Revert subgraph(s) to QA

For each subgraph being reverted, set its `override_subgraph_url:` entry back to its QA URL from the Context list (`https://qa-graphql-<name>.theorchard.io/graphql`).

Example — reverting `graphql-product`:
```yaml
  graphql-product: "https://qa-graphql-product.theorchard.io/graphql"
```

For **revert everything**, restore every entry to the QA URLs listed in Context. The easiest reliable way is to copy the override block fresh from the committed template:
```bash
grep -A 20 'override_subgraph_url:' ~/Repos/graphql-router/config-qa.yaml
```
and replace the corresponding block in `config.yaml` with those QA values (keep `config.yaml`'s other settings — the local `supergraph.listen: 127.0.0.1:8087`, disabled health_check, dropped telemetry, etc. — untouched).

Use the Edit tool to change only the affected lines. After reverting, if the router is running with `--hot-reload` it will pick up the config change automatically; otherwise note that the router must be restarted. A reverted subgraph no longer needs its local process — mention the user can stop it.

### 5. Confirm frontend-content GRAPHQL_URL

```bash
grep GRAPHQL_URL ~/Repos/frontend-content/.env ~/Repos/frontend-content/.env.local 2>/dev/null
```

- If `.env` already has `GRAPHQL_URL=http://localhost:8087/graphql` and no `.env.local` override exists → no change needed. Report: "frontend-content already points to the local router."
- If `.env.local` overrides it to something else → warn the user that `.env.local` is overriding the value and ask if they want to reset it.
- Never modify `.env` — it is committed.

### 6. Offer to boot the whole stack

Ask the user: **"Start the whole stack now?"** (yes / no). If no, skip to step 7 and just print the manual commands. If yes, launch the three tiers IN ORDER (subgraphs → router → frontend), each in its own Terminal.app tab (via `osascript`), health-checking each before starting the next.

**Determine the full set of local subgraphs to start.** The federated router needs EVERY locally-overridden subgraph running, not just the one(s) selected in this invocation — a subgraph left overridden to `localhost` with no process behind it will break the router. So before booting, re-read `config.yaml` and collect ALL subgraphs whose `override_subgraph_url` points to a `localhost`/`127.0.0.1` URL, parsing the port from each:
```bash
grep -E 'http://(localhost|127\.0\.0\.1):[0-9]+' ~/Repos/graphql-router/config.yaml
```
Start every subgraph in that set (this includes both subgraphs newly routed local in this run AND any that were already overridden from a previous run), each on the exact port its override specifies. If a subgraph is overridden to localhost but the user does NOT want it running, tell them to revert it to QA first (step 4b) — don't leave a dangling local override.

Each such subgraph repo must exist at `~/Repos/<subgraph-name>`. If one is missing, tell the user and offer to clone it (`git@github.com:theorchard/<subgraph-name>.git`); also run `yarn install` if `node_modules` is absent. Select the pinned Node via nvm (`nvm use || nvm install`) BEFORE any `yarn install` too, or it will fail with an incompatible-engine error.

**6a. Ensure router prerequisites (binary + supergraph).** The router cannot start without both. Check and obtain whatever is missing BEFORE launching anything:
```bash
ls ~/Repos/graphql-router/router ~/Repos/graphql-router/supergraph.graphql 2>&1
```
- **Missing `router` binary** → download the precompiled binary (no Rust needed):
  ```bash
  cd ~/Repos/graphql-router && curl -sSL https://router.apollo.dev/download/nix/latest | sh
  ```
- **Missing `supergraph.graphql`** → **compose it locally from the subgraphs**. No Apollo Studio credential is needed — composition reads subgraph SDL, and the helper script obtains that SDL by introspecting the QA subgraphs.

  **Primary — compose from the existing config:**
  ```bash
  cd ~/Repos/graphql-router && rover supergraph compose --config ./scripts/supergraph.yaml > ./supergraph.graphql
  ```
  If you haven't accepted the Apollo **T&C** yet, the command blocks on an interactive prompt and the redirect would capture a half-written file. Run it once **without** the `>` redirect so the T&C can be accepted, then re-run with the redirect to write the file:
  ```bash
  cd ~/Repos/graphql-router && rover supergraph compose --config ./scripts/supergraph.yaml
  ```
  The T&C acceptance is interactive — if it surfaces in a background/non-interactive run, hand it to the user (e.g. `! rover supergraph compose --config ./scripts/supergraph.yaml`), then re-run with the redirect once accepted.

  **Alternative — regenerate everything with the helper script** (use if `scripts/supergraph.yaml` or the per-subgraph SDL files are missing/stale):
  ```bash
  cd ~/Repos/graphql-router && ./scripts/generate-supergraph.sh
  ```
  This introspects each QA subgraph, writes a GraphQL schema file per subgraph, regenerates the `supergraph.yaml` that `rover` needs, and composes the supergraph SDL with `rover compose`.

  **Checkpoint:** `~/Repos/graphql-router/supergraph.graphql` exists at the repo root. Do not start the router until it does.

**6b. Start every local subgraph** detected in step 6, **each in its own Terminal tab** (see "Launching each server in its own Terminal tab" above — do NOT use `run_in_background`), selecting the pinned Node first, then forcing the port from its override:
```bash
F=$(mktemp /tmp/graphql-local-<subgraph-name>.XXXXXX.sh)
cat > "$F" <<'SCRIPT'
export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
cd ~/Repos/<subgraph-name> && { nvm use || nvm install; } && PORT=<port> yarn start
SCRIPT
osascript \
  -e 'tell application "Terminal" to activate' \
  -e 'tell application "System Events" to keystroke "t" using command down' \
  -e 'delay 0.4' \
  -e "tell application \"Terminal\" to do script \"source $F\" in front window"
```
Open one tab per locally-overridden subgraph (e.g. if both `graphql-product:8083` and `graphql-sr-delivery:8082` are overridden to localhost, open both). (Skipping the nvm step is the most common boot failure — an old default Node aborts with `The engine "node" is incompatible`.) **Use the brace group `{ nvm use || nvm install; }`, NOT a subshell `( … )`** — a subshell selects the Node only inside the fork, so `yarn start` in the parent still runs the old Node and fails the engine check even though nvm reported the right version. (Braces are needed rather than a bare `nvm use || nvm install` so `&&`/`||` precedence doesn't skip `yarn start` when `nvm use` succeeds.) Then poll until it answers (give it up to ~60s — TS compile is slow on first boot):
```bash
curl -s -o /dev/null -w "%{http_code}" -X POST http://localhost:<port>/graphql \
  -H "Content-Type: application/json" -H "apollographql-client-name: local" \
  -d '{"query":"{ __typename }"}'
```
A `200` means it is up. If it never comes up, tell the user to check that subgraph's Terminal tab for the error and stop — do not start the router against a dead subgraph. (Claude can't read the tab's output; the error is visible to the user in that tab.)

**6c-prep. Disable the custom Rust plugins in `config.yaml` (precompiled-binary path only).** The precompiled binary does not contain `theorchard.require_apollo_client_name` or `pde.auth_enforcement`, and will refuse to start if they're declared (`Additional properties are not allowed (...) was unexpected` → `no valid configuration was supplied`, exit 1). Check whether the `plugins:` block still has them active:
```bash
grep -nE '^\s*(theorchard\.require_apollo_client_name|pde\.auth_enforcement):' ~/Repos/graphql-router/config.yaml
```
If either is present and uncommented, comment out the whole `theorchard.require_apollo_client_name:` and `pde.auth_enforcement:` blocks (including their nested `enabled:`/`rules:` lines) in `config.yaml` with the Edit tool — `config.yaml` is gitignored, so this is safe. Keep the built-in `experimental.expose_query_plan: true` enabled. Add a comment noting they're disabled for the precompiled binary and should only be re-enabled for a from-source (`make dev`) build. (If building from source, SKIP this step and leave the plugins enabled.)

**6c-prep-2. Strip `Authorization` for each locally-routed subgraph (avoid the JWKS rate-limit error).** A locally-running subgraph validates the frontend's JWT against the live Auth0 JWKS endpoint via `@theorchard/jwt-service-handler` → `jwks-rsa`, which is hardcoded with `rateLimit: true` (10 requests/minute). On a cold start the subgraph's JWKS cache is empty, and the frontend's burst of concurrent queries from a single dev-machine IP trips that limit, surfacing in the browser as:

```
HTTP fetch failed from '<subgraph>': 500: Internal Server Error
Context creation failed: error in secret or public key callback: Too many requests to the JWKS endpoint
```

Note: `ENABLE_JWT_SERVICE_CACHE` does **not** fix this — it caches the JWT-enabled-services list, a different cache, not the JWKS signing keys. Do not rely on it for this error.

The fix is config-only. The subgraph only calls the JWKS endpoint when an `Authorization: Bearer` header is present; the JWT is decoded merely to derive `brand` (falls back to a default) and to override the identity UUID. The actual identity (`profileId`, `profileType`, `profileUUID`, `identityId`) is read from the `Orchard-*` headers — which `frontend-content` already sends on every request alongside the JWT. So removing the `Authorization` header for the locally-routed subgraph makes it build identity purely from those headers and skip JWKS entirely, with no rate limit. All other (QA) subgraphs keep the JWT and validate normally against QA infra.

For **each** subgraph routed to localhost, add a per-subgraph header rule under `headers.subgraphs` in `config.yaml` (gitignored, safe to edit) with the Edit tool:
```yaml
headers:
  all:
    request:
      - propagate:
          matching: ".*"
      - remove:
          matching: ^x-datadog-.*$
  subgraphs:
    graphql-publishing:        # repeat one block per locally-routed subgraph
      request:
        - remove:
            named: "authorization"
```
The router picks this up via `--hot-reload` (no restart needed) — re-health-check `:8087` afterward. When a subgraph is later reverted to QA (step 4b), also remove its `headers.subgraphs.<name>` block so QA receives the JWT again. (Trade-off: with the JWT stripped, `context.authorization` is unset and `brand` defaults — fine for local dev viewing data; if the subgraph itself needs the bearer token for downstream service calls, leave the JWT and instead let the JWKS cache warm by reloading once after a ~60s cooldown.)

**6c. Start the router** in its own Terminal tab (NOT `run_in_background`), using the precompiled binary:
```bash
F=$(mktemp /tmp/graphql-local-router.XXXXXX.sh)
cat > "$F" <<'SCRIPT'
cd ~/Repos/graphql-router && ./router --dev --config config.yaml --hot-reload --supergraph supergraph.graphql
SCRIPT
osascript \
  -e 'tell application "Terminal" to activate' \
  -e 'tell application "System Events" to keystroke "t" using command down' \
  -e 'delay 0.4' \
  -e "tell application \"Terminal\" to do script \"source $F\" in front window"
```
Poll the GraphQL endpoint until it answers:
```bash
curl -s -o /dev/null -w "%{http_code}" -X POST http://localhost:8087/graphql \
  -H "Content-Type: application/json" -H "apollographql-client-name: local" \
  -d '{"query":"{ __typename }"}'
```

**6d. Start the frontend** in its own Terminal tab (NOT `run_in_background`), selecting the pinned Node first:
```bash
F=$(mktemp /tmp/graphql-local-frontend.XXXXXX.sh)
cat > "$F" <<'SCRIPT'
export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
cd ~/Repos/frontend-content && { nvm use || nvm install; } && yarn start
SCRIPT
osascript \
  -e 'tell application "Terminal" to activate' \
  -e 'tell application "System Events" to keystroke "t" using command down' \
  -e 'delay 0.4' \
  -e "tell application \"Terminal\" to do script \"source $F\" in front window"
```

After each launch, confirm to the user whether it came up (via the `curl` health-check). Each server runs in its own Terminal tab, so the user controls them directly afterward — tell them which tab is which (subgraph(s) → router → frontend) and that they can Ctrl-C a tab to stop that server. There are no background task IDs to report since nothing is backgrounded.

### 7. Print a final setup summary

```
Local GraphQL stack
-------------------
Router repo     : ~/Repos/graphql-router  (already existed / just cloned)
Router config   : ~/Repos/graphql-router/config.yaml  (already existed / created from config-qa.yaml)
Router listen   : http://127.0.0.1:8087/graphql
Frontend URL    : http://localhost:8087/graphql  (already correct in .env)

Local subgraph overrides (ALL must be running):
  graphql-sr-delivery → http://localhost:8082/graphql
  graphql-product     → http://localhost:8083/graphql
  (all other subgraphs → QA)

Stack status (if started — each in its own Terminal tab; Ctrl-C a tab to stop it):
  graphql-sr-delivery : running (:8082)  [Terminal tab]
  graphql-product     : running (:8083)  [Terminal tab]
  graphql-router      : running (:8087)  [Terminal tab]
  frontend-content    : running (:8080)  [Terminal tab]

Open http://localhost:8080 — traffic flows: frontend → local router → local subgraph(s) + QA.

If NOT started, run manually in order (TS projects need the pinned Node — source nvm first):
  0. (one-time, if missing) cd ~/Repos/graphql-router
     - download binary:  curl -sSL https://router.apollo.dev/download/nix/latest | sh
     - compose schema:   rover supergraph compose --config ./scripts/supergraph.yaml > ./supergraph.graphql   (local compose; accept Apollo T&C once if prompted)
     - disable custom plugins for the precompiled binary: comment out the `theorchard.require_apollo_client_name` and `pde.auth_enforcement` blocks in config.yaml (else the binary errors with "Additional properties are not allowed")
  pre. export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"   # then `nvm use || nvm install` inside each TS repo
  1. start EVERY locally-overridden subgraph, each on its override port, e.g.:
     cd ~/Repos/graphql-sr-delivery && { nvm use || nvm install; } && PORT=8082 yarn start
     cd ~/Repos/graphql-product     && { nvm use || nvm install; } && PORT=8083 yarn start
  2. cd ~/Repos/graphql-router && ./router --dev --config config.yaml --hot-reload --supergraph supergraph.graphql
  3. cd ~/Repos/frontend-content && { nvm use || nvm install; } && yarn start
```

---

## Rules

- Never modify `frontend-content/.env`, any subgraph's committed `.env`, or `graphql-router/config-qa.yaml` — all are committed. Force the subgraph port inline (`PORT=<port> yarn start`) instead of editing its `.env`.
- `config.yaml` in graphql-router is gitignored — safe to edit freely.
- Never print or log secrets, tokens, or credentials found in any config files.
- If the user selects a subgraph not in the known list, still add the override entry — don't reject it.
- If args are passed in `subgraph:port` format, skip the interactive questions for those.
- Launch every server (each subgraph, the router, the frontend) in **its own Terminal.app tab** via the `osascript` pattern in "Launching each server in its own Terminal tab" — NOT as a backgrounded Bash task. This gives the user an interactive, controllable session per process afterward. These `osascript` calls are foreground Bash calls (they return immediately once the tab opens). Start the servers bottom-up (subgraphs → router → frontend) and health-check each with `curl` before starting the next; never start the router against a subgraph that failed to come up. Claude can't read a tab's stdout, so on failure point the user to the relevant tab rather than dumping a log.
- When booting the stack, start ALL subgraphs currently overridden to a `localhost`/`127.0.0.1` URL in `config.yaml` — not only the one(s) selected in the current invocation. The router federates them all, so any local override without a running process behind it breaks the system. If a subgraph shouldn't run, revert it to QA first (step 4b) rather than leaving a dangling local override.
- Booting the stack is a high-impact action — always confirm with the user (step 6) before starting any server.
- A fresh clone has NO `router` binary and NO `supergraph.graphql`. Never assume they exist — check in step 6a and obtain whatever is missing before starting the router.
- The precompiled binary does NOT include the custom Rust plugins (`theorchard.require_apollo_client_name`, `pde.auth_enforcement`). If they're declared in `config.yaml` the binary fails to start (`Additional properties are not allowed (...) was unexpected` → `no valid configuration was supplied`). Comment those blocks out of `config.yaml` (gitignored, safe) before starting the precompiled binary — step 6c-prep — keeping `experimental.expose_query_plan`. Re-enable them only for a from-source (`make dev`) build, which does compile them in.
- A locally-routed subgraph that validates the frontend's JWT will trip `jwks-rsa`'s hardcoded 10-req/min rate limit on its cold-start query burst → browser error `Too many requests to the JWKS endpoint`. Fix it config-only (step 6c-prep-2): add a `headers.subgraphs.<name>.request` rule to `config.yaml` that does `remove: named: "authorization"` for each locally-routed subgraph, so it builds identity from the `Orchard-*` headers the frontend already sends and skips JWKS. NEVER patch `node_modules` (e.g. `@theorchard/jwt-service-handler`'s `rateLimit` flag) to work around this — it's non-reproducible and wiped on reinstall. `ENABLE_JWT_SERVICE_CACHE` is unrelated and does not fix it. Remove the `headers.subgraphs.<name>` block when the subgraph is reverted to QA.
- Compose `supergraph.graphql` **locally from the subgraphs** (`rover supergraph compose --config ./scripts/supergraph.yaml > ./supergraph.graphql`, or `./scripts/generate-supergraph.sh`). No Apollo Studio credential is needed. The only interactive step is a possible one-time Apollo T&C acceptance — if `rover supergraph compose` blocks on it, run the command once without the `>` redirect so it can be accepted, then re-run with the redirect. Never fetch, prompt for, log, or set any Apollo key yourself.
- Don't install heavy toolchains (Rust/`rustup`, `protobuf`, `cmake`) unprompted. The precompiled-binary path needs none of them; only suggest a from-source build if the user must change Rust plugin code, and confirm first.
- **Missing library/tool → install, then continue (don't abort).** If a step needs a CLI or library that isn't installed (e.g. `rover`, `docker`, `node`/`nvm`, `yarn`, `protoc`), do NOT silently fail or stop the whole flow. Instead: (1) name what's missing and why this step needs it, (2) give the exact install command and offer to run it — for an interactive/auth or sudo install, hand it to the user via `! <command>`, otherwise run it yourself after confirming, (3) once it's installed, resume the process from the exact step that stalled rather than restarting from scratch. Keep the safety boundaries above: still confirm before installing heavy toolchains, and never install or fetch credentials/keys yourself.
- Before ANY `yarn install` / `yarn start` in a TS project (`graphql-*` subgraph or `frontend-content`), source nvm and run `nvm use || nvm install` to select the repo's pinned Node. Skipping this is the most common boot failure (`The engine "node" is incompatible`). nvm is a shell function, so source it (`\. "$NVM_DIR/nvm.sh"`) in the same command — it is not on `PATH`.
