Repository navigation
feat: install a base skill set by default - #135
Conversation
Rename the `mandatory` frontmatter flag to `base` and turn it on for the
ten skills every metamask-mobile engineer needs, so a fresh clone lands
exactly those from `yarn install` with no flags and nothing to read first.
The flag was already implemented end to end but no skill had ever set it,
and the default domain filter was permissive enough that it never applied.
- `SKILLS_DOMAINS` gains `none`/`all` sentinels; the zero-config default is
now base-only rather than every domain
- `SKILLS_AUTO_UPDATE` defaults on, and the cache refresh moved behind that
gate so opted-out engineers no longer pay a network fetch per install
- suppress git credential prompts and bound every spawn, so enabling the
above cannot hang `yarn install` on a private-source fetch
- reject `base: true` alongside `maturity: experimental`, and warn when a
base description is too thin to be selected
- rewrite six base descriptions that could never have triggered
("General coding guidelines" was 25 characters)
Base set: mobile-testing, coding-guidelines, controller-integration,
ui-development, component-scaffold, performance, pr-guidelines, create-pr,
pr-description, pr-codeowners.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`yarn skills` reads as "give me the skills", so having it install 10 instead of 36 was a surprising regression for a command an engineer typed on purpose. Split the two paths: an automatic install nobody asked for gets the minimum that works, an explicit run still gets every domain. `postinstall` now exports SKILLS_DEFAULT_SCOPE=base when it delegates to sync, and sync branches its *fallback* on that. Everything above the fallback in the resolution order is untouched — a --domain flag, SKILLS_DOMAINS, the picker and a saved selection all still win. That last part is the point: passing `--domain none` from postinstall would have outranked a saved selection, so an engineer who opted into perps would have silently lost it on their next `yarn install`. | path | domains | installed | | --- | --- | --- | | `yarn skills`, no config | all | 36 | | `yarn install`, no config | none | 10 | | `yarn install`, SKILLS_DOMAINS=perps | perps | 12 | | `yarn skills --domain none` | none | 10 | Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
NicolasMassart
left a comment
There was a problem hiding this comment.
Blocking on the delegated postinstall path still being able to wait indefinitely.
run() bounds its own spawns, but delegate() called spawnSync directly with no timeout. That is the subprocess launching tools/sync — including its git work — from the automatic postinstall path, so a stalled child could still hang `yarn install` indefinitely despite the "every spawn is capped" guarantee. Three details beyond adding the flag: - DELEGATE_TIMEOUT_MS is double SPAWN_TIMEOUT_MS, not equal to it. delegate() wraps a script that itself makes several already-capped calls; with the same budget the outer timer would fire first, after the wrapper's own startup cost, and kill a legitimately slow clone from the outside. That drops the engineer to the stale bundled snapshot — the exact outcome the existing cap is written to avoid. - killSignal is SIGKILL rather than the default SIGTERM. The child is Bash, which does not reliably forward a term signal to an in-flight git, so SIGTERM can leave an orphan holding the index lock and break the next install too. - spawnSync reports status === null on timeout, so `result.status ?? 1` returned a bare 1, indistinguishable from an ordinary failure. It now detects ETIMEDOUT explicitly and says what happened and what state the engineer is left in, which is what makes the failure reportable rather than merely bounded. `...options` is spread after `timeout`, matching run(), so an explicit override still wins. Verified: node --check passes, yarn lint clean, 65/65 tests pass. Confirmed empirically that spawnSync with timeout + SIGKILL yields error.code ETIMEDOUT and status null, so the new branch is the one that fires. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
NicolasMassart
left a comment
There was a problem hiding this comment.
Looks good to me.
|
Reviewed in depth — the diagnosis in the PR body is genuinely good (three gaps, one problem), and every number in it checks out against a clone at Blocker: legacy saved "all" selections are silently dropped to base-only. This breaks the PR's own "a saved selection still wins" guarantee — and since pruning is off by default, the other 26 skills stay on disk and silently go stale rather than visibly disappearing. Small fix in load_saved() {
[[ -f "$CONFIG_FILE" ]] || return 0
grep -q '^SKILLS_DOMAINS=' "$CONFIG_FILE" || return 0
local v; v=$(awk -F= '/^SKILLS_DOMAINS=/{sub(/^SKILLS_DOMAINS=/,""); print; exit}' "$CONFIG_FILE")
echo "${v:-all}"
}SSH prompts can still hang Smaller items:
|
Review pass over the base-skill work. Each item is a path where the new
default-on install either failed silently or failed harder than it should.
- .skills.local readers disagreed on inline comments. The JS side strips a
` # comment` tail via stripInlineComment(); Bash kept it, so the
`SKILLS_DOMAINS=perps # my choice` shape that .skills.local.example teaches
reached the new domain validation as `perps#mychoice` and aborted the run.
read_config_value() now strips consistently, for every saved key.
- `--select` with no valid entries returned "", which resolves to `all`. An
engineer who ran the picker specifically to narrow their set, then mistyped
the numbers, installed every domain instead. Blank already means `none`; an
all-invalid selection now means the same thing.
- An explicitly empty `SKILLS_AUTO_UPDATE=` is a choice, not an absence. Under
0.2.0 isTruthy('') was false, so engineers wrote exactly that to turn syncing
off, and reading it as unset would silently re-enable it. Only a genuinely
absent key defaults to on — the same rule load_saved() applies to an empty
SKILLS_DOMAINS.
- GIT_SSH_COMMAND is appended to rather than replaced. Overwriting breaks a
custom key, an agent or a ProxyCommand on the private overlay clone these
guards exist to protect, and buildDelegatedEnv() is shared, so it would hit
the explicit `yarn skills` path too.
- killSignal is back to the default SIGTERM. spawnSync's timeout signals only
the direct child, so neither signal reaches an in-flight git grandchild;
SIGKILL bought nothing and removed Bash's ability to run a trap. The orphan
holding .git/index.lock is a real gap, now documented as one instead of
claimed as fixed.
- tools/install accepts the pre-rename `mandatory` key. Executable code ships
in the pinned package while domains/ content can come from the origin/main
cache or a private overlay on its own cadence, so the two can legitimately be
out of step; without the alias that skew installs zero base skills and exits
0. Recognised as DEPRECATED_FRONTMATTER so the linter warns rather than
reporting an unknown key.
- Linter truthiness matches the other three implementations. It alone accepted
`on`/`off`, so `base: on` linted clean while the installer skipped the skill.
The base description floor is an error, not a warning: only errors set a
non-zero exit, so as a warning it never failed a job.
- Restored the `sync --save` changelog entry, dropped when the new Fixed block
replaced it.
Verified: yarn lint:changelog passes, 69/69 tests pass, skill linter reports 0
errors across 52 skills. Confirmed empirically that a saved
`SKILLS_DOMAINS=perps # my choice` previously exited 1 with
"unknown domain(s): perps#mychoice".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
All of this is addressed in c30fa37. Going through it in order — and two of your items led to finding a further bug each, which is noted below. Blocker — legacy saved "all" dropped to base-only. Fixed, and your reproduction was exactly right. I took your That refactor surfaced a second bug in the same readers: the JS side strips a trailing So the validation and the comment-stripping had to land together; separately, the first would have broken configs the docs recommend. SSH prompts. Fixed in both places — with one deviation: appended, not replaced. return /(^|\s)-o\s*BatchMode=/u.test(base) ? base : `${base} -oBatchMode=yes`;Hard-setting
CHANGELOG / 0.3.0. Added, including two Tests. Added to Typo'd domain silently installing base-only. Fixed — validated against This one also surfaced a sibling bug in Supply chain (raised on the mobile PR, but the fix belongs here). Verified on c30fa37: Thanks — the two blockers were both real, and chasing them turned up two more. |
NicolasMassart
left a comment
There was a problem hiding this comment.
Looks good. The previous blocking issues have been addressed, and I found no new blockers in the latest changes.
The domain-resolution logic would benefit from additional regression tests, but I don't consider that blocking.
Turns on a **base skill set** that installs for every engineer on a fresh clone, with no flags and nothing to read first. Mobile gets 10 skills; everything else stays opt-in. The `mandatory` frontmatter flag is renamed to `base` and actually used. It was already implemented end to end — `tools/install`, `tools/sync`, the schema, the CLI, even a `★` marker in the interactive picker — but **no skill had ever set it**, and the default domain filter was permissive enough that the bypass had nothing to bypass. Three things had to be true together for "base skills by default" to work. Only the flag existed. | Problem | Fix | |---|---| | Empty `SKILLS_DOMAINS` meant *all* domains, so `base` never applied | `none`/`all` sentinels | | `SKILLS_AUTO_UPDATE` was off by default, so `postinstall` refreshed the cache and installed nothing | Defaults on. The cache refresh moved *behind* the gate, so opted-out engineers no longer pay a network fetch per install | | Six base descriptions were too thin to ever trigger | Rewritten. `coding-guidelines` was literally `"General coding guidelines"` — 25 characters | That last one is the subtle one: a skill's description is its **entire trigger surface**. A 25-character description never fires on the work it governs, while appearing to be covered. `yarn skills` reads as "give me the skills", so having it install 10 instead of 36 would be a surprising regression for a command someone typed on purpose. The two paths differ: | Path | Domains | Installed | |---|---|---| | `yarn skills`, no config | all | **36** — unchanged from today | | `yarn install` → `postinstall`, no config | none | **10** | | `yarn install` **with `SKILLS_DOMAINS=perps`** | perps | **12** = 10 base + 2 perps | | `yarn skills --domain none` | none | 10 | `postinstall` exports `SKILLS_DEFAULT_SCOPE=base` and `tools/sync` branches its **fallback** on that. Everything above the fallback in the resolution order is untouched — a `--domain` flag, `SKILLS_DOMAINS`, the picker, and a saved selection all still win. That last property is the point of doing it this way. Passing `--domain none` from `postinstall` would have outranked a saved selection, so an engineer who opted into `perps` would have silently lost it on their next `yarn install`. `mobile-testing` · `coding-guidelines` · `controller-integration` · `ui-development` · `component-scaffold` · `performance` · `pr-guidelines` · `create-pr` · `pr-description` · `pr-codeowners` `pr-description` and `pr-codeowners` are included because `create-pr` delegates its main output to them with "if available" phrasing — without them it silently runs in a reduced mode. Always-on cost: **~1,130 tokens** per session. The set is deliberately small: when the agent's skill listing overflows its budget, descriptions get truncated starting with the least-invoked skills, so a bloated base set makes *every* skill harder to trigger. Enabling auto-update puts git operations on every engineer's install path, so this also: - suppresses git credential prompts (`GIT_TERMINAL_PROMPT=0`, empty `GIT_ASKPASS`) in both `run()` and the delegated environment — `tools/sync` pulls the **private** Consensys overlay, which could otherwise block on stdin during `yarn install` with no output - bounds every spawn: 300s for network git, 10s for local `bash --version` probes - `base: true` + `maturity: experimental` is now a **lint error** — contradictory intent; installing unfinished guidance for everyone is the bad outcome - warns when a base skill's description is under 120 characters | Check | Result | |---|---| | Automatic path (`SKILLS_DEFAULT_SCOPE=base`) | exactly 10, in `.claude/skills` and `.agents/skills` | | Explicit `yarn skills` | 36 | | Automatic + `SKILLS_DOMAINS=perps` | 12 — saved selection respected | | `SKILLS_AUTO_UPDATE=0` / `CI=1` | silent, exit 0 | | Full `postinstall` → `sync` → `install` | exit 0 | | Overlay merge intact | `coding-guidelines` 71-byte source stub → 3,990 bytes installed | | `node --test` | 65/65 | | `lint-skill-entry.mjs` | 52 skills, 0 errors | Existing engineers are **not** dropped to 10. Their current installs stay, because pruning is off by default and their next `yarn install` only adds the base set. `yarn skills --prune-stale` once gives a clean state. This does reach **external contributors**: the skills package is a devDependency of public repos, so a drive-by contributor's `yarn install` now clones this repo into `.skills-cache/` and writes 10 gitignored skill files. Public content, gitignored output, one clone — flagging it rather than letting it be discovered later. - Extension's base set: stacked PR on this branch - Consumer side: MetaMask/metamask-mobile#35263 (draft — needs a published release of this) Known gaps documented separately: no standardized PR-review skill, platform APIs at 2 of ~11, and five deprecated testing skills that still compete with `mobile-testing`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
##⚠️ Merge after #135 Stacked on `feat/base-skills-mobile`. The `base` flag this PR sets is introduced there — **merge #135 first**, then this retargets to `main` automatically. ## What Marks the 10 skills every `metamask-extension` engineer needs as `base`, so a fresh clone lands exactly those. Four are already shared with Mobile and land in #135 — `coding-guidelines`, `ui-development`, `controller-integration`, `pr-guidelines`. This PR adds the six Extension-only ones: | Skill | Why base | |---|---| | `extension-testing` | Testing entrypoint — unit, integration (stub), Selenium E2E | | `controller-guidelines` | Authoring a controller: BaseController choice, state metadata, messenger typing | | `perf-rendering` | Lists, virtualization, memoization, code splitting | | `perf-hooks-effects` | `useEffect` misuse, dependency arrays, cascading effects | | `perf-react-compiler` | What the compiler optimizes, where it bails out, when manual memoization remains | | `perf-state-management` | Redux reducer purity, selector identity, normalization | Always-on cost: **3,257 characters ≈ 814 tokens** per session across all ten. ## Descriptions Both caveats from the original version are addressed. **Descriptions now state trigger conditions, not topics.** They were 34–39 characters — `"Rendering performance optimization"` says what a skill is about but never when to reach for it, and nobody phrases the problem that way; they say the list stutters when they scroll. Each now leads with concrete coverage and ends with a `Use when …` clause in symptom vocabulary. The original mitigation here — "CI warns on each until rewritten, so the TODO cannot rot" — **was false**, as @Qbandev demonstrated: the linter only sets a non-zero exit on errors, so a warning sat invisibly inside a green check. #135 promotes the base-description floor to an error, which is what makes this PR's descriptions load-bearing rather than optional. **The four `perf-*` skills now route to each other.** They were mutually indistinguishable — "my component re-renders too much" matched all four equally, so selection was arbitrary and the model could pull ~20KB scattershot. Each now names its siblings, so whichever fires hands off rather than competing. This mitigates the four-slots-for-one-concern problem; it does not solve it. Consolidating to a single entrypoint with on-demand references, as Mobile's `performance` does, remains the structural fix and is still worth doing separately. Nothing here is measured. The argument is mechanical — the description is the selection surface, symptoms were absent, siblings were undifferentiated — not the result of a triggering experiment. ## Scope Proposed from the catalogue rather than from working in `metamask-extension`, so **it still needs owner review** on: - whether `extension-testing` is genuinely the right single testing entrypoint Resolved since the original: @MajorLift confirmed `controller-guidelines` and `controller-integration` should **both** be base, with `controller-integration` the more valuable for client work since integrating controllers is routine and authoring them is not. They now cross-reference each other, and `controller-integration`'s Extension reference gains a section pointing at the authoring guidance. ## Known Extension gaps (not addressed here) | Gap | Notes | |---|---| | **Almost no PR automation** | 9 of 10 `pr-workflow` skills are Mobile-only, including `create-pr`. Extension gets PR *standards* but no PR *automation*. Overlay work on existing skills — likely the cheapest high-value win. | | No `component-scaffold` equivalent | Design-system-first intent has no enforcement at generation time | | No component-view test layer | The "steer agents toward component view tests" goal is Mobile-only today | | Platform APIs | 1 of ~11 areas covered | --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Summary `yarn install` installs **zero base skills** for anyone with `CONSENSYS_SKILLS_DIR` set, and reports `Install complete.` while doing it. Found while testing 0.3.0 end to end from the published package in metamask-mobile. This is a regression from #135 — my own change. ## Root cause `tools/sync` picked its installer with an executable test: ```bash if [[ -x "$SCRIPT_DIR/install" ]]; then INSTALL_BIN="$SCRIPT_DIR/install" else for src in "${SOURCES[@]}"; do ... done # keeps the LAST match fi ``` npm and Yarn do not preserve the executable bit when unpacking a tarball, so every `tools/*` file arrives `0644` in a consumer install: ``` node_modules/@metamask/skills/tools/ -rw-r--r-- install .skills-cache/metamask-skills/tools/ -rwxr-xr-x install ``` So the guard is **always false in the published package** and always true in a git checkout — which is the only place it was ever exercised. The pinned installer was never selected. The fallback loop ran instead, and since it keeps the last match while `SOURCES` appends `CONSENSYS_SKILLS_DIR` after `METAMASK_SKILLS_DIR`, the private overlay's `tools/install` won every install. An overlay predating the `base:` key has no notion of base skills, so it skipped all 16 as `excluded by --domain` and exited 0. Deterministic on a real checkout — identical command either side: ``` chmod +x tools/install → base-included: 16, .claude/skills: 10 chmod -x tools/install → base-included: 0, .claude/skills: 0 ``` ## Why it survived review Two conditions had to line up, and #135 supplied both: - Preferring the overlay in the fallback was **already** wrong, but harmless — the overlay's installer understood every flag in use, so output was identical. - `base:` is the first flag it doesn't implement, turning a silent misroute into zero skills. Before #135, sync preferred `$METAMASK_SKILLS_DIR/tools/install`, which the CLI defaults to the `.skills-cache` clone — a git checkout, so the bit survived and the first branch always won. #135 was right to stop executing code from that unpinned clone, but swapped in a condition that cannot hold in the artifact it was protecting. It cannot reproduce from a source checkout, so neither CI nor local dev would ever see it. ## Changes | File | Change | |---|---| | `tools/sync` | test `-f`, `exec bash "$INSTALL_BIN"` | | `tools/bootstrap`, `tools/deploy` | same treatment — both exec a sibling installer directly | | `test/cli.test.mjs` | regression test | The test builds the fixture that reproduces it — package installer at `0644` as npm ships it, overlay at `0755` as git checks it out — and asserts the pinned one runs. Reverting only `tools/sync` fails it with `STALE-OVERLAY`. ## Testing - `yarn test` — 70/70 (69 + the new one) - `yarn lint`, `yarn lint:changelog` — clean - Verified against the published `0.3.0` in metamask-mobile: `chmod +x` on the package tools restores all 16 base skills ## Follow-up Needs a **0.3.1** — 0.3.0 cannot deliver base skills to overlay users. metamask-mobile#35263 is pinned at `^0.3.0` and picks the patch up automatically. Also worth noting: the supply-chain property #135 claimed has never actually held for a consumer. Until this lands, every install executes `tools/install` from the git cache or the overlay rather than the lockfile-pinned package. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
> [!NOTE] > **Unblocked.** MetaMask/skills#135 and MetaMask/skills#154 are merged and `@metamask/skills@0.3.1` is published. Ready to review. ## **Description** Agent skills currently require running `yarn skills` and picking domains by hand. This makes the base set arrive from a plain `yarn install` instead, so a fresh clone lands 10 skills with no flags and nothing to read first. Everything else stays opt-in via `yarn skills --select`. | File | Change | |---|---| | `package.json` | `@metamask/skills` bumped to `^0.3.1`; `postinstall` can no longer fail `yarn install` | | `yarn.lock` | resolves `@metamask/skills@npm:0.3.1` | | `scripts/setup.mjs` | `yarn setup` reports which skills are on disk | | `.skills.local.example` | documents the base-only default and the opt-outs | | `README.md`, `AGENTS.md`, `.gitignore` | all three described `yarn skills` as the only way skills arrive | **Why 0.3.1 and not 0.3.0.** The base-skill default shipped in 0.3.0, but 0.3.0 cannot deliver it to anyone with `CONSENSYS_SKILLS_DIR` set: `tools/sync` chose its installer with an `-x` test, npm strips the executable bit when unpacking, so it fell through to the overlay's copy of `tools/install` — which predates `base:` and installed **zero** skills while exiting 0. Found by testing this branch against the published package; fixed in MetaMask/skills#154 and released as 0.3.1. Verified against the published 0.3.1 with the exec bits exactly as npm delivers them (`tools/install` at `0644`) and the overlay configured: ``` yarn skills:postinstall claude=10 cursor=10 agents=10 yarn skills claude=39 cursor=39 agents=39 ``` Same setup returned 0/0/0 on 0.3.0. **Why the postinstall guard is `|| true`.** Unguarded, an unresolvable `@metamask/skills` (production-only install, partial `node_modules`) exits 127 and **`yarn install` exits 1** — measured. That contradicts `README.md`, which promises *"Skipping `yarn skills` is fine — it only affects agent tooling, not the app build."* It was `|| echo '...' >&2` first. Measured with yarn 4.14.1 in a scratch workspace, failing postinstall either way: | postinstall | install #1 | install #2 | |---|---|---| | bare | `YN0009` + `build.log` path | retries | | `\|\| echo …` | `YN0007` only, message printed nowhere | **never retried** | Yarn surfaces build output only for builds it considers *failed*, so the guard discarded its own message **and** marked the build successful, which stopped Yarn retrying. `|| true` is the honest form; the README now says plainly that nothing is printed either way. **Why the setup report.** Yarn swallows all postinstall output into a build log, so the base set arrived silently and nobody learned that team skills are opt-in. `yarn setup` runs in the foreground and is run often. The task only reads `.claude/skills/`; it never runs a sync, so `yarn setup` does not install twice. Skipped in CI and `--node` mode, where `postinstall` already skips installing. Output: ``` .... Found 10 agent skill(s) in .claude/skills. The base set installs automatically; yarn skills adds every domain: 🔎 Pick specific domains: yarn skills --select 📖 Inspect one: yarn metamask-skills describe <domain>/<skill> 🧹 Drop ones no longer managed: yarn skills --prune-stale (add --domain none for the base set only) .... ``` "Found", not "installed": `postinstall` returns 0 even when the sync fails, so this is a directory listing rather than a report of what this run did. It names only Claude Code because it only reads `.claude/skills` — the three targets drift, and captioning it "and Cursor" reported 10 on a checkout where Cursor was loading 39. The prune hint cannot be one command. With a domain saved, plain `--prune-stale` keeps it (12 → 12) and `--domain none` silently discards it (12 → 10); with nothing saved the fallback is `all`, so plain `--prune-stale` installs every domain first (10 → 37). Both measured. **One oddity worth knowing.** The trailing blank line comes from a `` constant, not whitespace. Listr trims each captured `console.log` chunk, so a trailing newline, space, tab, NBSP, or an extra `console.log('')` is all removed and the next task's output butts against this one. A zero-width space is not in JavaScript's whitespace set, so it survives the trim while rendering as nothing. Written as an escape with a comment rather than a literal, so no invisible byte sits in the file for a formatter to strip. **Behaviour change.** Engineers without `SKILLS_DOMAINS` set go from ~36 skills to 10 on their next dependency change. Nothing is pruned automatically, so existing installs accumulate — `yarn skills --prune-stale` cleans up once, and `SKILLS_DOMAINS=all` restores previous behaviour. Opt out entirely with `SKILLS_AUTO_UPDATE=0` or `SKILLS_SKIP_POSTINSTALL=1`. This also reaches **external contributors**: `@metamask/skills` is a devDependency of a public repo, so a drive-by contributor's `yarn install` will clone the public skills repo into `.skills-cache/` and write 10 gitignored skill files. Public content, gitignored output, one clone — flagging it rather than letting it be discovered later. ## **Changelog** CHANGELOG entry: null Developer tooling only — no user-facing change. ## **Related issues** Refs: AIENG-38 Depended on: MetaMask/skills#135 and MetaMask/skills#154 (both merged, released as `@metamask/skills@0.3.1`) ## **Manual testing steps** ```gherkin Feature: base agent skills install by default Scenario: engineer runs setup and sees which skills are available Given a checkout with @metamask/skills at a version that supports `base` When the engineer runs `yarn setup:expo` Then the "Report agent skills" task lists the number of available skills And it points at `yarn skills --select` for opt-in domains And the EXPO "Setup complete!" banner remains the final message Scenario: a broken skills CLI does not fail the install Given @metamask/skills is not resolvable When the engineer runs `yarn install` Then the install exits 0 And `yarn skills:postinstall` run by hand still exits non-zero and reports why Scenario: CI does not run the report Given CI is set When setup runs Then the "Report agent skills" task is skipped ``` ## **Screenshots/Recordings** ### **Before** N/A — no UI change. Before this PR, `yarn setup` printed nothing about agent skills. ### **After** Terminal output shown in the Description above. ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [x] I've tested on Android - [x] I've tested with a power user scenario - [x] I've instrumented key operations with Sentry traces for production performance metrics N/A for all three — this changes a setup script and a package.json script, with no app-runtime code path. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Developer-only install and setup scripting; no app runtime or production dependency paths change beyond a resilient postinstall hook. > > **Overview** > **Fresh clones get a default agent skill set without running `yarn skills` first.** The PR bumps `@metamask/skills` to `^0.3.1` so `postinstall` syncs only **base** skills during `yarn install` / setup; `yarn skills` still installs every domain. Docs (README, AGENTS.md, `.skills.local.example`, `.gitignore`) are updated to match that split and to document `SKILLS_DOMAINS`, opt-outs (`SKILLS_AUTO_UPDATE=0`), and when Yarn actually re-runs postinstall. > > **`yarn install` no longer fails if the skills hook breaks:** `postinstall` is now `yarn skills:postinstall || true`, aligning with the promise that agent tooling is optional for the app build. > > **`yarn setup` surfaces what’s on disk:** a new **Report agent skills** Listr task (local only, not CI/`--node`) lists `mms-*` folders under `.claude/skills` and points people at `yarn install` for the base set or `yarn skills` for all domains, without triggering a second sync. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 51dd8ea. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
What
Turns on a base skill set that installs for every engineer on a fresh clone, with no flags and nothing to read first. Mobile gets 10 skills; everything else stays opt-in.
The
mandatoryfrontmatter flag is renamed tobaseand actually used. It was already implemented end to end —tools/install,tools/sync, the schema, the CLI, even a★marker in the interactive picker — but no skill had ever set it, and the default domain filter was permissive enough that the bypass had nothing to bypass.Why
Three things had to be true together for "base skills by default" to work. Only the flag existed.
SKILLS_DOMAINSmeant all domains, sobasenever appliednone/allsentinelsSKILLS_AUTO_UPDATEwas off by default, sopostinstallrefreshed the cache and installed nothingcoding-guidelineswas literally"General coding guidelines"— 25 charactersThat last one is the subtle one: a skill's description is its entire trigger surface. A 25-character description never fires on the work it governs, while appearing to be covered.
Automatic vs explicit
yarn skillsreads as "give me the skills", so having it install 10 instead of 36 would be a surprising regression for a command someone typed on purpose. The two paths differ:yarn skills, no configyarn install→postinstall, no configyarn installwithSKILLS_DOMAINS=perpsyarn skills --domain nonepostinstallexportsSKILLS_DEFAULT_SCOPE=baseandtools/syncbranches its fallback on that. Everything above the fallback in the resolution order is untouched — a--domainflag,SKILLS_DOMAINS, the picker, and a saved selection all still win.That last property is the point of doing it this way. Passing
--domain nonefrompostinstallwould have outranked a saved selection, so an engineer who opted intoperpswould have silently lost it on their nextyarn install.The base set (10)
mobile-testing·coding-guidelines·controller-integration·ui-development·component-scaffold·performance·pr-guidelines·create-pr·pr-description·pr-codeownerspr-descriptionandpr-codeownersare included becausecreate-prdelegates its main output to them with "if available" phrasing — without them it silently runs in a reduced mode.Always-on cost: ~1,130 tokens per session. The set is deliberately small: when the agent's skill listing overflows its budget, descriptions get truncated starting with the least-invoked skills, so a bloated base set makes every skill harder to trigger.
Safety
Enabling auto-update puts git operations on every engineer's install path, so this also:
GIT_TERMINAL_PROMPT=0, emptyGIT_ASKPASS) in bothrun()and the delegated environment —tools/syncpulls the private Consensys overlay, which could otherwise block on stdin duringyarn installwith no outputbash --versionprobesGuardrails added
base: true+maturity: experimentalis now a lint error — contradictory intent; installing unfinished guidance for everyone is the bad outcomeVerified
SKILLS_DEFAULT_SCOPE=base).claude/skillsand.agents/skillsyarn skillsSKILLS_DOMAINS=perpsSKILLS_AUTO_UPDATE=0/CI=1postinstall→sync→installcoding-guidelines71-byte source stub → 3,990 bytes installednode --testlint-skill-entry.mjsMigration note
Existing engineers are not dropped to 10. Their current installs stay, because pruning is off by default and their next
yarn installonly adds the base set.yarn skills --prune-staleonce gives a clean state.This does reach external contributors: the skills package is a devDependency of public repos, so a drive-by contributor's
yarn installnow clones this repo into.skills-cache/and writes 10 gitignored skill files. Public content, gitignored output, one clone — flagging it rather than letting it be discovered later.Related
Known gaps documented separately: no standardized PR-review skill, platform APIs at 2 of ~11, and five deprecated testing skills that still compete with
mobile-testing.🤖 Generated with Claude Code