diff --git a/README.md b/README.md
index b94fbe3a..8a237019 100644
--- a/README.md
+++ b/README.md
@@ -125,7 +125,7 @@ domains//
scripts/ # optional helper scripts
adapters/ # optional runtime payloads used by scripts
repos/.md # optional repo-specific overlay
- knowledge/ # optional shared domain reference
+ knowledge/ # optional shared domain reference, installed beside each domain skill
tools/
install # core writer (mms- prefix, multi-operator output)
sync # Flow 2: `yarn skills` wrapper for engineers
diff --git a/domains/perps/knowledge/review-antipatterns.md b/domains/perps/knowledge/review-antipatterns.md
index d0266453..ddab98b7 100644
--- a/domains/perps/knowledge/review-antipatterns.md
+++ b/domains/perps/knowledge/review-antipatterns.md
@@ -105,3 +105,11 @@ PRs that touch UI components must include testIDs so agentic recipes and E2E tes
- **testID not in `Perps.testIds.ts`** — testIDs defined as inline strings instead of exported constants from `app/components/UI/Perps/Perps.testIds.ts`. All testIDs must be centralized so recipes can reference them by constant name.
- **testID missing from the element that holds the value** — adding testID to a wrapper View instead of the `TextInput` or Text that actually contains the value. CDP fiber-walk reads `value` from the React element with the matching testID — the testID must be on the element that owns the state.
- **TP/SL price inputs without testID** — the trigger price `TextInput` components in `PerpsTPSLView` (and similar order-form screens) frequently lack testIDs, making it impossible to assert the accepted decimal precision agentically. Any PR touching these screens must add `testID` to both the Take Profit and Stop Loss price inputs.
+
+## Component View Test Coverage
+
+Perps page/view tests should use the component-view test framework when they are exercising screen behavior through rendered UI and app state. Broad unit tests that render a page and mock hooks/selectors are a review smell.
+
+- **Component-view behavior tested as a unit test** — files such as `ui/pages/perps/**/index.test.tsx` or `app/components/UI/Perps/**/*.test.tsx` that render a whole page/view and assert UI behavior should be converted to `*.view.test.tsx` using the component-view test framework/skill. Keep simple unit tests only for focused pure logic, local helper functions, or narrow component contracts.
+- **Hook/selector mocking in a page behavior test** — mocking selectors, hooks, or service modules to force page state bypasses the real state wiring. Drive behavior through framework state presets/renderers instead. If the framework cannot cover the case yet, keep the unit test focused and link a follow-up for the missing framework support.
+- **Coverage drops during conversion** — converting to component-view tests must preserve the same coverage intent. If a scenario cannot move to component-view, document why and retain the smallest focused unit test needed to keep coverage.
diff --git a/domains/perps/skills/fix-perps-bug/skill.md b/domains/perps/skills/fix-perps-bug/skill.md
index e37ae018..ab5ce99f 100644
--- a/domains/perps/skills/fix-perps-bug/skill.md
+++ b/domains/perps/skills/fix-perps-bug/skill.md
@@ -22,7 +22,7 @@ maturity: stable
- Check if the bug involves a duplicated utility (see shared-package-analysis) -- if so, check both codebases
3. **Check formatting.** If the bug involves number display:
- - Read formatting-rules knowledge
+ - Read installed `knowledge/formatting-rules.md`
- Fix must follow the sig-dig rules, not hardcode decimals
4. **Check stream hooks.** If the bug involves stale/missing data:
diff --git a/domains/perps/skills/review-perps-pr/repos/metamask-extension.md b/domains/perps/skills/review-perps-pr/repos/metamask-extension.md
index 4e363fbf..a9d5ecfd 100644
--- a/domains/perps/skills/review-perps-pr/repos/metamask-extension.md
+++ b/domains/perps/skills/review-perps-pr/repos/metamask-extension.md
@@ -30,6 +30,8 @@ ui/hooks/perps/usePerpsOrderForm.ts -- formatCu
**Hook consolidation** — extension merges several mobile hooks into one. When reviewing hook changes, check that the consolidated hook still covers all cases the separate mobile hooks handle.
+**Component-view test fit** — Perps page tests under `ui/pages/perps/**/*.test.tsx` that render full pages or exercise UI behavior should be reviewed as component-view test candidates. Ask for conversion to the component-view test framework/skill while preserving coverage. Keep ordinary unit tests only for pure helpers, narrow rendering contracts, or cases the framework cannot cover yet; require that exception to be stated.
+
**Missing screens** — close-all, cancel-all, withdraw, order book, order details. Don't block PRs for these, but note if a PR introduces partial implementations that conflict with future full implementations.
## Validation
diff --git a/domains/perps/skills/review-perps-pr/skill.md b/domains/perps/skills/review-perps-pr/skill.md
index 7966c339..cf0c4b06 100644
--- a/domains/perps/skills/review-perps-pr/skill.md
+++ b/domains/perps/skills/review-perps-pr/skill.md
@@ -56,18 +56,21 @@ loop, run each CLI as a live tmux pane instead of one-shot, and `/clear` between
## Perps standard (reviewers must load and enforce as blockers)
-From the perps `knowledge/` dir: **review-antipatterns** (core checklist), architecture,
+From the installed `knowledge/` dir: **review-antipatterns** (core checklist), architecture,
connection-architecture, caching-architecture, formatting-rules, mobile-extension-map,
shared-package-analysis, feature-flags, screens. Check both repos when a shared util/screen
-changes.
+changes. For page/view test changes, also enforce the component-view test guidance: broad
+rendered UI behavior tests belong in the component-view framework/skill unless a focused unit
+test is explicitly justified.
## Reviewer prompt (force a fresh full review every round)
```
Fresh full review of perps changes in at vs . No prior context.
-Load perps knowledge (review-antipatterns + the rest). You gate this before any human sees it.
+Load installed perps knowledge (`knowledge/`, review-antipatterns + the rest). You gate this before any human sees it.
Every perps anti-pattern AND every nit (naming, magic number, missing testID, weak test,
-.toFixed, fallback-display vs 0) = BLOCKER. APPROVE only if nothing is left to improve.
+component-view behavior left as broad unit tests, .toFixed, fallback-display vs 0) = BLOCKER.
+APPROVE only if nothing is left to improve.
Return:
VERDICT: APPROVE | REQUEST_CHANGES
COMMIT:
diff --git a/domains/perps/skills/validate-perps-multiproject/scripts/perps-validate.sh b/domains/perps/skills/validate-perps-multiproject/scripts/perps-validate.sh
new file mode 100755
index 00000000..ebc834a3
--- /dev/null
+++ b/domains/perps/skills/validate-perps-multiproject/scripts/perps-validate.sh
@@ -0,0 +1,262 @@
+#!/usr/bin/env bash
+#
+# perps-validate.sh — deterministic helper for validating a Core
+# @metamask/perps-controller change inside a client checkout (Mobile or
+# Extension) via yalc.
+#
+# Direction is always: a CLIENT validates a CORE controller change.
+# owner = the Core checkout that holds the perps-controller change
+# client = the Mobile/Extension checkout that consumes it
+#
+# Run subcommands in this order:
+# 1. prestate [client-dir...] # snapshot before touching anything
+# 2. build [--full] # build the package (freshness gate)
+# 3. push [...] # yalc publish + push into clients
+# 4. verify # confirm version + new symbols landed
+# 5. restore [client-dir...] # put the client back to its snapshot
+#
+# Helper subcommands:
+# resolve-yalc # print the resolved yalc invocation
+# doctor # quick environment sanity check
+#
+# Design goals:
+# - No assumption about the Node manager (asdf / nvm / volta / brew / none).
+# - No hardcoded paths. Everything is derived or passed in.
+# - Pre-state aware: if a client was ALREADY on a yalc link, restore brings
+# that exact link back instead of nuking the user's dev setup.
+#
+# Env overrides:
+# YALC_BIN explicit yalc invocation (e.g. "/opt/homebrew/bin/yalc"
+# or "node /path/to/yalc/src/yalc.js"). Skips auto-resolution.
+# PKG package name (default: @metamask/perps-controller)
+# STATE_DIR where snapshots live (default: /tmp/.perps-validate)
+
+set -euo pipefail
+
+PKG="${PKG:-@metamask/perps-controller}"
+# perps-controller lives at packages/ in the Core monorepo.
+PKG_LEAF="${PKG##*/}" # perps-controller
+PKG_SCOPE="${PKG%/*}" # @metamask
+
+# ---------------------------------------------------------------------------
+# yalc resolution — the single most fragile thing across machines.
+#
+# A version-manager shim (notably asdf) can SUCCEED with exit 0 yet do nothing,
+# printing "No version is set for command yalc". So we never trust exit code
+# alone: a working yalc must print a semver to stdout. If the shim is broken we
+# fall back to locating yalc's own yalc.js and running it through a real node.
+# ---------------------------------------------------------------------------
+_looks_like_version() { printf '%s' "$1" | grep -Eq '^[0-9]+\.[0-9]+'; }
+
+resolve_yalc() {
+ if [ -n "${YALC_BIN:-}" ]; then printf '%s' "$YALC_BIN"; return 0; fi
+
+ # 1. plain yalc on PATH, but only if it actually reports a version.
+ # Use `command yalc` so we hit the real binary, never the run_yalc wrapper.
+ if command -v yalc >/dev/null 2>&1; then
+ local v; v="$(command yalc --version 2>/dev/null || true)"
+ if _looks_like_version "$v"; then printf 'yalc'; return 0; fi
+ fi
+
+ # 2. a real node to run yalc.js with (any working node is fine).
+ local node_bin; node_bin="$(command -v node || true)"
+ [ -z "$node_bin" ] && { echo "ERROR: no node on PATH to run yalc" >&2; return 1; }
+
+ # 3. hunt for yalc's entrypoint across the common install layouts.
+ local cand
+ for cand in \
+ "$(npm root -g 2>/dev/null)/yalc/src/yalc.js" \
+ "$(npm root -g 2>/dev/null)/yalc/yalc.js" \
+ "$HOME"/.asdf/installs/nodejs/*/lib/node_modules/yalc/src/yalc.js \
+ "$HOME"/.nvm/versions/node/*/lib/node_modules/yalc/src/yalc.js \
+ "$HOME"/.volta/tools/image/packages/yalc/lib/node_modules/yalc/src/yalc.js \
+ /opt/homebrew/lib/node_modules/yalc/src/yalc.js \
+ /usr/local/lib/node_modules/yalc/src/yalc.js ; do
+ [ -f "$cand" ] && { printf '%s %s' "$node_bin" "$cand"; return 0; }
+ done
+
+ echo "ERROR: could not resolve yalc. Install it (npm i -g yalc) or set YALC_BIN." >&2
+ return 1
+}
+
+# Run yalc regardless of how it resolved (binary or "node yalc.js").
+# NOT named `yalc` on purpose — a function named `yalc` would shadow the real
+# binary and make resolve_yalc recurse forever.
+run_yalc() { local y; y="$(resolve_yalc)" || return 1; eval "$y \"\$@\""; }
+
+# ---------------------------------------------------------------------------
+state_dir() { printf '%s/tmp/.perps-validate' "$1"; }
+
+# ===========================================================================
+cmd_resolve_yalc() {
+ local y; y="$(resolve_yalc)" || exit 1
+ echo "yalc => $y"
+ eval "$y --version"
+}
+
+# ---------------------------------------------------------------------------
+cmd_prestate() {
+ [ "$#" -ge 1 ] || { echo "usage: prestate [client-dir...]" >&2; exit 2; }
+ for client in "$@"; do
+ client="$(cd "$client" && pwd)"
+ local sd; sd="$(state_dir "$client")"; mkdir -p "$sd"
+ echo "=== prestate: $client ==="
+ git -C "$client" status --short --branch | tee "$sd/git-status.txt" >/dev/null
+ cp "$client/package.json" "$sd/package.json.bak" 2>/dev/null || true
+ cp "$client/yalc.lock" "$sd/yalc.lock.bak" 2>/dev/null || true
+
+ local linkdir="$client/.yalc/$PKG"
+ if [ -d "$linkdir" ]; then
+ # Client was ALREADY on a yalc link — back it up byte-for-byte so restore
+ # reproduces the exact pre-existing dev setup, not a clean registry state.
+ echo "mode=PREEXISTING_YALC" > "$sd/mode"
+ tar -czf "$sd/yalc-pkg.tgz" -C "$client/.yalc/$PKG_SCOPE" "$PKG_LEAF"
+ cat "$linkdir/yalc.sig" 2>/dev/null > "$sd/yalc.sig" || true
+ echo " was already yalc-linked: version=$(node -p "require('$linkdir/package.json').version" 2>/dev/null) sig=$(cat "$sd/yalc.sig" 2>/dev/null)"
+ else
+ echo "mode=REGISTRY" > "$sd/mode"
+ echo " no prior yalc link (registry baseline)"
+ fi
+ echo " snapshot -> $sd"
+ done
+}
+
+# ---------------------------------------------------------------------------
+# Build the controller package. The package CANNOT build standalone in a fresh
+# Core checkout: its referenced packages have no dist yet and tsc fails with
+# TS6305. That is expected — the supported fix is a full monorepo build first.
+cmd_build() {
+ local core="${1:?usage: build [--full]}"; shift || true
+ local full=0; [ "${1:-}" = "--full" ] && full=1
+ core="$(cd "$core" && pwd)"
+ local log="$core/tmp/perps-build.log"; mkdir -p "$core/tmp"
+
+ if [ "$full" -eq 1 ]; then
+ echo "=== full monorepo build (nice) — builds all referenced dists ==="
+ ( cd "$core" && nice -n 10 yarn build ) 2>&1 | tee "$log"
+ else
+ echo "=== package build: yarn workspace $PKG build ==="
+ if ! ( cd "$core" && yarn workspace "$PKG" build ) 2>&1 | tee "$log"; then
+ :
+ fi
+ if grep -q "TS6305" "$log"; then
+ echo ""
+ echo "BLOCKED: TS6305 — referenced package dists are missing (fresh checkout)."
+ echo "Do NOT use 'workspaces foreach -R' (cycle can delete dist)."
+ echo "Re-run with --full to build the whole monorepo first:"
+ echo " perps-validate.sh build $core --full"
+ exit 3
+ fi
+ fi
+
+ # Freshness gate: prove the built dist actually carries this change.
+ local dist="$core/packages/$PKG_LEAF/dist"
+ [ -f "$dist/index.cjs" ] || { echo "ERROR: no dist/index.cjs produced" >&2; exit 3; }
+ echo ""
+ echo "built version: $(node -p "require('$core/packages/$PKG_LEAF/package.json').version")"
+ echo "dist OK -> $dist"
+}
+
+# ---------------------------------------------------------------------------
+cmd_push() {
+ local core="${1:?usage: push [client-dir...]}"; shift
+ [ "$#" -ge 1 ] || { echo "need at least one client dir" >&2; exit 2; }
+ core="$(cd "$core" && pwd)"
+ local pkgdir="$core/packages/$PKG_LEAF"
+
+ echo "=== yalc publish $PKG from $pkgdir ==="
+ ( cd "$pkgdir" && run_yalc publish --private )
+
+ for client in "$@"; do
+ client="$(cd "$client" && pwd)"
+ echo "=== push into client: $client ==="
+ if [ -d "$client/.yalc/$PKG" ] || grep -q "$PKG" "$client/yalc.lock" 2>/dev/null; then
+ ( cd "$client" && run_yalc update "$PKG" ) # advance an existing link
+ else
+ ( cd "$client" && run_yalc add "$PKG" && yarn install --mode=skip-build )
+ fi
+ done
+}
+
+# ---------------------------------------------------------------------------
+cmd_verify() {
+ local client="${1:?usage: verify }"
+ client="$(cd "$client" && pwd)"
+ local inst="$client/node_modules/$PKG"
+ echo "=== verify $PKG in $client ==="
+ echo "installed version: $(node -p "require('$inst/package.json').version" 2>/dev/null || echo MISSING)"
+ echo "yalc link version: $(node -p "require('$client/.yalc/$PKG/package.json').version" 2>/dev/null || echo none)"
+ echo "yalc sig: $(cat "$client/.yalc/$PKG/yalc.sig" 2>/dev/null || echo none)"
+ echo "--- next: run the client's own proof (type-check + the affected tests) ---"
+}
+
+# ---------------------------------------------------------------------------
+# Restore is pre-state aware. PREEXISTING_YALC clients get their exact link
+# back; REGISTRY clients are fully un-yalc'd.
+cmd_restore() {
+ [ "$#" -ge 1 ] || { echo "usage: restore [client-dir...]" >&2; exit 2; }
+ for client in "$@"; do
+ client="$(cd "$client" && pwd)"
+ local sd; sd="$(state_dir "$client")"
+ local mode; mode="$(cat "$sd/mode" 2>/dev/null | cut -d= -f2 || echo UNKNOWN)"
+ echo "=== restore: $client (mode=$mode) ==="
+ case "$mode" in
+ PREEXISTING_YALC)
+ rm -rf "$client/.yalc/$PKG"
+ tar -xzf "$sd/yalc-pkg.tgz" -C "$client/.yalc/$PKG_SCOPE"
+ cp "$sd/yalc.lock.bak" "$client/yalc.lock" 2>/dev/null || true
+ cp "$sd/package.json.bak" "$client/package.json" 2>/dev/null || true
+ echo " restored prior link: sig now=$(cat "$client/.yalc/$PKG/yalc.sig" 2>/dev/null) expected=$(cat "$sd/yalc.sig" 2>/dev/null)"
+ echo " run 'yarn install --mode=skip-build' if node_modules needs to match"
+ ;;
+ REGISTRY)
+ ( cd "$client" && run_yalc remove "$PKG" || true )
+ git -C "$client" checkout -- package.json yarn.lock 2>/dev/null || true
+ rm -rf "$client/.yalc" "$client/yalc.lock"
+ echo " un-yalc'd; package.json/yarn.lock reverted"
+ ;;
+ *)
+ echo " no snapshot found ($sd) — nothing to restore. Run prestate next time." ;;
+ esac
+ git -C "$client" status --short --branch | sed 's/^/ /'
+ done
+}
+
+# ---------------------------------------------------------------------------
+cmd_doctor() {
+ local core="${1:?usage: doctor }"; local client="${2:?need client dir}"
+ echo "=== doctor ==="
+ echo "node: $(command -v node) $(node -v 2>/dev/null)"
+ echo -n "yalc: "; cmd_resolve_yalc || true
+ echo "core: $core ($(git -C "$core" rev-parse --abbrev-ref HEAD 2>/dev/null))"
+ echo "client: $client ($(git -C "$client" rev-parse --abbrev-ref HEAD 2>/dev/null))"
+ echo "package: $PKG ($core/packages/$PKG_LEAF $( [ -d "$core/packages/$PKG_LEAF" ] && echo found || echo MISSING ))"
+ echo "client links pkg already: $( [ -d "$client/.yalc/$PKG" ] && echo yes || echo no )"
+}
+
+# ===========================================================================
+sub="${1:-}"; shift || true
+case "$sub" in
+ resolve-yalc) cmd_resolve_yalc "$@" ;;
+ prestate) cmd_prestate "$@" ;;
+ build) cmd_build "$@" ;;
+ push) cmd_push "$@" ;;
+ verify) cmd_verify "$@" ;;
+ restore) cmd_restore "$@" ;;
+ doctor) cmd_doctor "$@" ;;
+ *) cat >&2 < [client-dir...]
+ perps-validate.sh build [--full]
+ perps-validate.sh push [client-dir...]
+ perps-validate.sh verify
+ perps-validate.sh restore [client-dir...]
+ perps-validate.sh resolve-yalc
+ perps-validate.sh doctor
+
+Env: YALC_BIN, PKG (default @metamask/perps-controller), STATE_DIR
+EOF
+ exit 2 ;;
+esac
diff --git a/domains/perps/skills/validate-perps-multiproject/skill.md b/domains/perps/skills/validate-perps-multiproject/skill.md
new file mode 100644
index 00000000..cea2ebcd
--- /dev/null
+++ b/domains/perps/skills/validate-perps-multiproject/skill.md
@@ -0,0 +1,246 @@
+---
+name: validate-perps-multiproject
+description: Interactively validate perps changes across local MetaMask Core, Mobile, and Extension checkouts. Use when a Core @metamask/perps-controller change must be checked in Mobile/Extension, or when a Mobile/Extension perps change needs parity validation in the other client. Defaults to current checkout as owner, yalc for Core package transport, read-only validation targets, and the smallest meaningful proof; asks the user only when required folders or proof level cannot be resolved.
+maturity: stable
+---
+
+# Validate Perps Multiproject
+
+Validate one perps change across multiple local MetaMask repo checkouts.
+
+**Canonical direction: a CLIENT validates a CORE controller change.** The
+`@metamask/perps-controller` package lives in Core; Mobile and Extension consume
+it. The owner is wherever the controller change is; the targets are the clients
+that must not break. (Client↔client parity is a secondary mode — see below.)
+
+## Deterministic layer — use `scripts/perps-validate.sh`
+
+Do **not** re-derive the per-folder commands each run. `scripts/perps-validate.sh`
+encodes them and is portable across machines and Node managers. Run it in order:
+
+```bash
+SC="$(dirname "$0")/scripts/perps-validate.sh" # or the skill's scripts/ path
+
+perps-validate.sh doctor # sanity: branches, yalc, links
+perps-validate.sh prestate [client-dir...] # snapshot BEFORE touching anything
+perps-validate.sh build [--full] # build pkg; freshness gate
+perps-validate.sh push [...] # yalc publish + push into clients
+perps-validate.sh verify # version + new symbols landed?
+# -> then run the client's own proof (type-check + the affected tests)
+perps-validate.sh restore [client-dir...] # pre-state-aware restore
+```
+
+The script makes no assumption about the Node manager and resolves `yalc`
+robustly (see "yalc resolution" below). Everything else in this doc explains the
+*why* so you can adapt when a step deviates.
+
+## Defaults
+
+- **Owner checkout**: current repo/cwd unless a path is provided.
+- **Folder layout**: assume Core/controller, Mobile, and Extension are sibling folders under one workspace.
+- **Targets**:
+ - Core/controller owner -> validate in Mobile and Extension when available;
+ - Mobile owner -> validate parity in Extension;
+ - Extension owner -> validate parity in Mobile.
+- **Transport**: `yalc` for `@metamask/perps-controller`; none for client parity.
+- **Proof**: smallest meaningful proof. For a client validating a Core change,
+ the high-value proof is the **client type-check** (catches removed/renamed/
+ changed exports) plus **the client tests that exercise the changed surface**.
+ Add recipe/e2e or real UI flow only when runtime behavior changes.
+- **Target edits**: forbidden unless explicitly allowed.
+- **Cleanup**: required, and **pre-state aware** — a client that was already on a
+ yalc link must be restored to that exact link, not wiped to a registry baseline.
+
+## Step 0 — resolve folders or ask
+
+Do not guess missing folders. Discover likely local checkouts, then ask only for unresolved choices.
+
+```bash
+HERE=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
+PARENT=$(dirname "$HERE")
+
+# Prefer sibling folders in the same workspace.
+for name in core metamask-core controller metamask-mobile mobile metamask-extension extension; do
+ [ -d "$PARENT/$name" ] && echo "$PARENT/$name"
+done
+
+# Optional fallback if repos are not siblings.
+ROOT="${METAMASK_REPOS_DIR:-$HOME/dev/metamask}"
+find "$ROOT" -maxdepth 1 -type d \
+ \( -name 'core*' -o -name 'controller*' -o -name 'metamask-mobile*' -o -name 'mobile*' -o -name 'metamask-extension*' -o -name 'extension*' \) -print 2>/dev/null
+```
+
+Note: a workspace may hold many numbered clones (`core-1..core-6`,
+`metamask-mobile-1..6`). Confirm WHICH core holds the change and WHICH client to
+validate — do not assume the first match.
+
+If needed, ask one concise question using the runtime's interactive question tool when available:
+
+```text
+I need the validation folders:
+- Owner checkout (Core with the change):
+- Validation target client(s):
+- Proof level: transport-only | type/import | recipe/e2e | real UI flow
+- May validation targets be edited? default no
+```
+
+Echo the resolved contract before changing anything.
+
+## Worker context to inject
+
+```md
+## Perps multiproject validation
+
+Owner: `` on ``
+Targets:
+- `` — — purpose:
+
+Rules:
+1. Capture `git status --short --branch` in every checkout first (`prestate`).
+2. Edit only the owner checkout unless a target is explicitly editable.
+3. For Core package validation, build first, then publish via yalc; never publish stale `dist/`.
+4. Prove the built `dist/` actually carries the change (version bump + new symbols) before publishing.
+5. Run the smallest proof that reaches the changed perps behavior.
+6. Restore each client to its snapshot; a pre-existing yalc link must come back byte-for-byte.
+```
+
+## Core -> clients via yalc
+
+`scripts/perps-validate.sh` runs all of this. The manual equivalents below are
+for when you must deviate.
+
+### yalc resolution (do not assume asdf/nvm/brew)
+
+`yalc` is the single most fragile dependency across machines. A version-manager
+**shim can exit 0 yet do nothing** — e.g. asdf prints `No version is set for
+command yalc` and returns success, so `yalc publish` silently no-ops. Never trust
+the exit code alone: a working `yalc` prints a semver to stdout.
+
+Resolution order (what the script does):
+1. `YALC_BIN` env override, if set.
+2. `command yalc --version` — accept only if it prints a semver.
+3. Otherwise locate yalc's own `yalc.js` and run it through any working `node`.
+ Searched layouts: `$(npm root -g)/yalc`, `~/.asdf/installs/nodejs/*/lib/...`,
+ `~/.nvm/versions/node/*/lib/...`, `~/.volta/...`, `/opt/homebrew/lib/...`,
+ `/usr/local/lib/...`.
+
+If all else fails: `npm i -g yalc`, or pass an explicit
+`YALC_BIN="node /abs/path/to/yalc/src/yalc.js"`.
+
+```bash
+# Pre-state for every checkout (the script's `prestate` does this + a byte-for-byte
+# backup of any existing .yalc link).
+for repo in /path/to/core /path/to/client; do
+ echo "--- $repo"; git -C "$repo" status --short --branch
+ (cd "$repo" && printf 'node=%s yarn=%s\n' "$(node -v)" "$(yarn -v)")
+done
+```
+
+### Building the package — expect a full monorepo build
+
+The perps-controller package **cannot build standalone in a fresh Core checkout.**
+Its `tsconfig.build.json` uses project references, so `yarn workspace
+@metamask/perps-controller build` fails with `TS6305: Output file ... has not been
+built from source file` for every dependency package whose `dist/` is missing
+(network-controller, transaction-controller, controller-utils,
+profile-sync-controller, remote-feature-flag-controller, messenger, …). This is
+the normal state of a fresh checkout, **not** a perps-controller bug.
+
+The supported fix is a full monorepo build first:
+
+```bash
+cd /path/to/core
+nice -n 10 yarn build # builds all referenced dists in dependency order
+```
+
+This takes several minutes (40+ packages) but is clean and leaves the checkout
+buildable for later runs. Do **not** use `yarn workspaces foreach --from
+@metamask/perps-controller -R ...` — it can fail on the
+account-tree/multichain/perps/snap cycle and delete `dist/` on the way out.
+`perps-validate.sh build --full` wraps `nice -n 10 yarn build`; the
+plain `build` form detects TS6305 and tells you to re-run with `--full`.
+
+### Freshness gate
+
+`yalc publish` success is meaningful only when the **current** run produced a
+fresh `packages/perps-controller/dist`, and that dist carries the change. Verify
+before trusting it:
+
+```bash
+D=/path/to/core/packages/perps-controller
+node -p "require('$D/package.json').version" # expected bump
+ls "$D"/dist/index.cjs # dist exists
+grep -l "" "$D"/dist/index.d.cts # new surface present
+```
+
+### Publish + push (advancing an existing link)
+
+A client may **already** be on a yalc link (e.g. mid-feature dev). In that case
+`yalc add` is wrong — use `yalc update` to advance the existing link in place.
+`perps-validate.sh push` picks the right one automatically.
+
+```bash
+cd /path/to/core/packages/perps-controller && yalc publish --private --push
+# `--push` propagates to every linked project. WATCH THE OUTPUT: it will touch
+# *other* repos that link the same package (e.g. a sibling clone), not just your
+# target. That is a side effect to note, not a failure.
+```
+
+### Run the proof, then restore
+
+Client proof for "controller won't break":
+
+```bash
+cd /path/to/client
+# 1. type-check — the strongest signal for export/type regressions.
+NODE_OPTIONS='--max-old-space-size=8192' npx tsc --noEmit --incremental \
+ --tsBuildInfoFile .tsbuildinfo --project ./tsconfig.json
+# 2. the client tests that exercise the changed surface (NOT the whole suite,
+# NOT --findRelatedTests).
+yarn jest --no-coverage
+```
+
+### Cleanup — pre-state aware (critical)
+
+The old "rm -rf .yalc yalc.lock && git checkout package.json yarn.lock" recipe is
+**destructive** when the client was already on a yalc link before you started: it
+wipes the user's active dev setup. Restore to the snapshot taken by `prestate`:
+
+- **Client was already yalc-linked** → restore the backed-up `.yalc/`
+ byte-for-byte, restore `yalc.lock`/`package.json`, re-run
+ `yarn install --mode=skip-build` if node_modules must match.
+- **Client had no prior link (registry baseline)** → `yalc remove `,
+ `git checkout -- package.json yarn.lock`, `rm -rf .yalc yalc.lock`.
+
+`perps-validate.sh restore ` does the right one based on the recorded
+mode. If you advanced a client deliberately and the user wants to keep the new
+version, say so explicitly and keep the snapshot for a later restore.
+
+## Client parity
+
+For Mobile <-> Extension checks:
+
+1. Load relevant installed perps knowledge from `knowledge/`: `mobile-extension-map`, `screens`, `shared-package-analysis`, architecture docs as needed.
+2. Find the equivalent screen/hook/flow in the other client.
+3. Validate with real flow evidence; do not inject UI state.
+4. Report semantic differences separately from regressions.
+
+## Stop conditions
+
+Stop and report when the owner package cannot build (after `--full`), a target
+checkout has unexpected dirt, cleanup would delete user work, target source edits
+are needed but not allowed, or required device/browser/credential context is
+missing.
+
+## Final answer
+
+```md
+Validated against .
+- Owner: @, status
+- Targets:
+- Transport: (built via full monorepo build / package build)
+- Proof: =>
+- Artifacts:
+- Cleanup:
+- Follow-ups/blockers:
+```
diff --git a/tools/install b/tools/install
index 22bb83b6..fcd91dc1 100755
--- a/tools/install
+++ b/tools/install
@@ -360,17 +360,46 @@ copy_bundle_dirs() {
done
}
+copy_domain_knowledge() {
+ local skill_dir="$1" dest_dir="$2" label="$3"
+ local domain_dir; domain_dir=$(cd "$skill_dir/../.." && pwd)
+ local knowledge_dir="$domain_dir/knowledge"
+
+ if [[ -d "$knowledge_dir" ]]; then
+ action "$label/knowledge/"
+ $DRY_RUN && return
+ mkdir -p "$dest_dir"
+ rm -rf "$dest_dir/knowledge"
+ cp -R "$knowledge_dir" "$dest_dir/knowledge"
+ else
+ if $DRY_RUN; then
+ [[ -e "$dest_dir/knowledge" ]] && action "$label/knowledge/ (remove stale)"
+ return
+ fi
+ rm -rf "$dest_dir/knowledge"
+ fi
+}
+
copy_project_bundles() {
local skill_dir="$1" out_name="$2"
copy_bundle_dirs "$skill_dir" "$CLAUDE_DIR/$out_name" ".claude/skills/$out_name"
+ copy_domain_knowledge "$skill_dir" "$CLAUDE_DIR/$out_name" ".claude/skills/$out_name"
copy_bundle_dirs "$skill_dir" "$CURSOR_DIR/$out_name" ".cursor/rules/$out_name"
+ copy_domain_knowledge "$skill_dir" "$CURSOR_DIR/$out_name" ".cursor/rules/$out_name"
copy_bundle_dirs "$skill_dir" "$AGENTS_DIR/$out_name" ".agents/skills/$out_name"
+ copy_domain_knowledge "$skill_dir" "$AGENTS_DIR/$out_name" ".agents/skills/$out_name"
}
copy_user_bundles() {
local skill_dir="$1" out_name="$2"
- [[ -d "$HOME/.claude" ]] && copy_bundle_dirs "$skill_dir" "$USER_CLAUDE_DIR/$out_name" "~/.claude/skills/$out_name"
- [[ -d "$HOME/.codex" ]] && copy_bundle_dirs "$skill_dir" "$USER_CODEX_DIR/$out_name" "~/.codex/skills/$out_name"
+ if [[ -d "$HOME/.claude" ]]; then
+ copy_bundle_dirs "$skill_dir" "$USER_CLAUDE_DIR/$out_name" "~/.claude/skills/$out_name"
+ copy_domain_knowledge "$skill_dir" "$USER_CLAUDE_DIR/$out_name" "~/.claude/skills/$out_name"
+ fi
+ if [[ -d "$HOME/.codex" ]]; then
+ copy_bundle_dirs "$skill_dir" "$USER_CODEX_DIR/$out_name" "~/.codex/skills/$out_name"
+ copy_domain_knowledge "$skill_dir" "$USER_CODEX_DIR/$out_name" "~/.codex/skills/$out_name"
+ fi
}
process_skill() {