Files
pnpm/AGENTS.md
T
Zoltan Kochan 1860c1c75e chore: move get-pnpm into the workspace and pin npm's signing keys once (#17341)
get-pnpm moves from pnpm/get.pnpm.io into pnpm/npm/get-pnpm, at 0.0.9
(get.pnpm.io be4e983, where its history stays).

- The pnpm v12 npm package depends on it with `workspace:*`, so a pnpm
  release bundles get-pnpm's source as of that release. A wrapper fix no
  longer needs a get-pnpm release, a catalog bump, and a
  minimumReleaseAge exclusion first. bundle-node-gyp builds it before
  the deploy injects its build output.
- It is released on its own: a `get-pnpm` product in bump.ts and
  create-release-pr.yml (off by default), `get-pnpm@<version>` tags,
  and build-get-pnpm/publish-get-pnpm jobs in release.yml. It is private
  in the workspace so the TypeScript release's recursive publish skips
  it; the packed manifest drops the flag.
- Its runtime dependencies stay exact, through a `get-pnpm` named
  catalog. Its lint is the root ESLint config. Its TLS tests use the
  fixture the Rust network tests use.
- The npm-wrapper CI job runs its tests on Node.js 24 and 22, beside
  the wrapper's. The forwarding proxy in its download tests forwards
  only to the servers the tests start.

npm's registry signing keys were pinned in five places: v11, v12,
get-pnpm, and get.pnpm.io's install.sh and install.ps1. Only v11's copy
was refreshed, by the release PR, and the two repositories handled a
withdrawn key in opposite ways. They now come from
.config/npm-signing-keys.json:

- .github/scripts/update-npm-signing-keys.mjs refreshes it from npm on
  every release PR, not only v11 ones. A key npm stops listing stays,
  marked withdrawn.
- The meta-updater renders it into the v11, v12, and get-pnpm copies,
  so `pnpm lint:meta` fails on a copy edited by hand. pnpm v11 and v12
  keep trusting withdrawn keys, so packages signed before a rotation
  verify. get-pnpm drops them, as get.pnpm.io's installers do.
- get.pnpm.io's update script is to read this file.
2026-10-25 17:38:04 +01:00

33 KiB

Agent Guide to pnpm Repository

This document provides context and instructions for AI agents working on the pnpm codebase.

The repository contains these products:

  • The TypeScript pnpm v11 CLI — pnpm11/.
  • The Rust pnpm v12 CLI (pacquet) — pnpm/. pnpm v12 is the target for new feature development. See pnpm/AGENTS.md for pacquet-specific rules; it adds to (and never contradicts) the conventions below.
  • The Rust pnpr registry server — pnpr/. See pnpr/AGENTS.md for pnpr-specific rules; it adds to (and never contradicts) the conventions below.
  • The get-pnpm installer — pnpm/npm/get-pnpm, the get-pnpm npm package. The pnpm v12 npm package bundles it to download its native binary. It is released apart from the CLIs.

Sections below marked "(TypeScript only)" apply to TypeScript code only; they do not apply to Rust code in pnpm/ or pnpr/. Everything else applies repo-wide unless a nested AGENTS.md specializes it.

Project philosophy

Apply PHILOSOPHY.md when implementing, triaging, or reviewing changes. It defines the core requirements and the approach to existing capabilities, abstractions, compatibility, and releases.

pnpm v12 and v11 development policy

pnpm v12, implemented in Rust under pnpm/, is the target for new development. pnpm v11, implemented in TypeScript under pnpm11/, is maintained for bug fixes.

Implement new features only in pnpm v12. Do not add them to pnpm v11. A feature that intentionally exists only in v12 is not a parity gap.

For bug fixes, first determine which versions contain the bug. If the bug is present in both v11 and v12, implement and test the fix in both stacks. Keep their observable behavior aligned for the affected functionality, including command-line flags and defaults, environment-variable handling, lockfile/manifest/state-file formats, error codes and messages, log emissions parsed by @pnpm/cli.default-reporter, store layout, and hook semantics. If the bug exists in only one version, fix only that version.

When a shared bug fix cannot be completed in both stacks in the same PR, call out the missing implementation in the PR description so it can be added before the PR lands.

The pacquet-side version policy is in pnpm/AGENTS.md.

Website documentation

User documentation lives in pnpm/docs/ for v12, pnpm11/docs/ for v11, and pnpr/docs/ for the registry. Update the affected version's docs and sidebar in the same PR as a behavior change. See DOCUMENTATION.md for local previews and release publishing. The blog and website application remain in pnpm/pnpm.io.

Repository Structure

The pnpm codebase is a monorepo managed by pnpm itself. The root contains functional directories organized by domain:

TypeScript pnpm v11 Core Directories

  • pnpm11/pnpm/: The CLI entry point and main package.
  • pnpm11/installing/: Core package management logic (installation, linking, etc.).
  • pnpm11/bins/: Linking and removing package bins.
  • pnpm11/resolving/: Dependency resolution logic (resolvers for npm, tarballs, git, etc.).
  • pnpm11/fetching/: Package fetching logic.
  • pnpm11/store/: Store management logic (content-addressable storage).
  • pnpm11/lockfile/: Lockfile handling, parsing, and utilities.

CLI & Configuration

  • pnpm11/cli/: CLI command implementations and infrastructure.
  • pnpm11/config/: Configuration management and parsing.
  • pnpm11/hooks/: pnpm hooks (readPackage, etc.).
  • pnpm11/cli/commands/src/completion/: Shell completion support.

Other Functional Directories

  • pnpm11/network/: Network-related utilities (proxy, fetch, auth).
  • pnpm11/workspace/: Workspace-related utilities.
  • pnpm11/exec/: Execution-related commands (run, exec, dlx).
  • pnpm11/engine/runtime/commands/: Node.js environment management.
  • pnpm11/cache/: Cache-related commands and utilities.
  • pnpm11/patching/: Package patching functionality.
  • pnpm11/releasing/: Release and publishing utilities.

Shared Utilities

  • pnpm11/fs/: Filesystem utilities.
  • pnpm11/crypto/: Cryptographic utilities.
  • pnpm11/text/: Text processing utilities.

Rust Projects

  • pnpm/: The Rust pnpm v12 CLI. Self-contained sub-project with its own crates, tests, and tooling — see pnpm/AGENTS.md.
  • pnpr/: The pnpm-compatible npm registry server. Self-contained sub-project with its own crates, tests, and tooling — see pnpr/AGENTS.md.

Setup & Build (TypeScript only)

To set up the environment and build the project:

pnpm install
pnpm run compile

To compile a specific package:

pnpm --filter <package_name> run compile

Important: The TypeScript pnpm v11 CLI e2e tests (in pnpm11/pnpm/test/) use the bundled pnpm11/pnpm/dist/pnpm.mjs, not the individual package lib/ outputs. After changing any TypeScript package, you must rebuild the bundle before running e2e tests:

pnpm --filter pnpm run compile

This runs tsgo --build, linting, and pnpm run bundle (which bundles all TypeScript packages into pnpm11/pnpm/dist/pnpm.mjs). Without this step, e2e tests will use a stale bundle and your changes won't be tested.

Testing

Run the selection that covers what you changed, not the whole repository: the full suite takes a lot of time. This applies to every product here, TypeScript and Rust alike. Let CI run the rest — it runs the full Rust suite on Linux, macOS, and Windows and the full TypeScript suite for every pull request. The testing-changes skill covers how to derive that selection from the diff in each product, and pnpm/CONTRIBUTING.md has the Rust workspace's checks, including the one case that does call for a full local run: a change whose affected set cannot be named, such as the workspace manifest, the lockfile, or the toolchain.

The rest of this section is TypeScript only.

Run tests for a specific project instead:

# From the project directory
pnpm test

# From the root, filtering by package name
pnpm --filter <package_name> test

Or better yet, run tests for a specific file:

pnpm --filter <package_name> test <file_path>

Or a specific test case in a specific file:

pnpm --filter <package_name> test <file_path> -t <test_name_pattern>

Linting (TypeScript only)

To run all linting checks:

pnpm run lint

Never ignore test failures

Do not dismiss a failing test as a "pre-existing" failure that is unrelated to your changes. Every test failure must be investigated and fixed. If a test was already broken before your changes, fix it as part of your work — do not silently skip it or treat it as acceptable.

AI Review Guidance

The repository's review framework lives in review-code review guide — how changes are accepted or rejected, the security-first / performance-second priorities, the security checklist and advisory regression themes, and the test/changeset/version-coverage expectations. Apply it when reviewing pull requests. (TypeScript-specific code style and engineering conventions for the CLI are documented in the "Code Style" section of this file; pacquet and pnpr follow their own AGENTS.md and style guides.)

Security is the first review priority and performance the second. Surface only issues tied to the changed code, and explain the exploit path, impact, or hot path affected. See the guide's Security and Performance review sections for the full checklist.

Code Reuse and Avoiding Duplication

Before writing new code, always analyze the existing codebase for similar functionality. This is a large monorepo with many shared utilities — duplication is a real risk.

  • Search before you write. Before implementing any non-trivial logic, search the codebase for existing functions, utilities, or patterns that do the same or similar thing. Check pnpm11/fs/, pnpm11/crypto/, pnpm11/text/, pnpm11/object/, and other shared directories first.
  • Extract shared code. If you find that the logic you need already exists in another package but is not exported or reusable, refactor it into a shared package rather than duplicating it. If you are adding new code that is similar to code that already exists elsewhere in the repo, move the common parts into a shared package that both locations can use.
  • Prefer open source packages over custom implementations. Do not reimplement functionality that is already available as a well-maintained open source package. Use established libraries for common tasks (e.g., path manipulation, string utilities, data structures, schema validation). Only write custom code when no suitable package exists or when the existing packages are too heavy or unmaintained.
  • Keep the dependency on the right level. When adding a new open source dependency, add it to the most specific package that needs it, not to the root or to a shared package unless multiple packages depend on it.

Commit Messages

Follow the Conventional Commits specification.

  • feat: a new feature
  • fix: a bug fix
  • docs: documentation only changes
  • style: formatting, missing semi-colons, etc.
  • refactor: code change that neither fixes a bug nor adds a feature
  • perf: a code change that improves performance
  • test: adding missing tests
  • chore: changes to build process or auxiliary tools

Install the git hooks before committing

The git hooks in .husky/ (including the commit-msg check described below) only run once husky has wired them into git. A fresh clone does not have them active until installed. Before making any commit, ensure the hooks are installed by running one of:

pnpm install      # runs the "prepare": "husky" script as part of install
# or, if dependencies are already installed, register the hooks on their own:
pnpm exec husky

You can confirm the hooks are active with git config core.hooksPath (it should point at husky's directory) and by checking that .husky/_/ exists. Do not commit with hooks uninstalled — that silently skips every check, including the bare #NNN rejection below.

Never use bare #NNN issue/PR references

Do not write a bare #NNN (a # followed by digits) anywhere in a commit message. A commit-msg hook (.husky/reject-bare-issue-refs.mjs) rejects them.

GitHub turns any #NNN into a link to issue/PR NNN of this repo, which is almost never what a bare reference means. This is a frequent AI mistake, in these forms:

  • Using #1, #2, #3, and so on to enumerate items in a list. GitHub instead links them to unrelated issues #1, #2, #3 of this repo. Fix: don't use # for enumeration — write item 1, (1), 1., or rephrase.
  • Referring to issue #NNN of a different repository. GitHub instead links it to issue NNN of this repo. Fix: use qualified syntax owner/repo#NNN or an absolute URL https://github.com/owner/repo/issues/NNN.

For references to issues/PRs in this repo, also use the qualified form pnpm/pnpm#NNN or the absolute URL https://github.com/pnpm/pnpm/issues/NNN. Qualified syntax and absolute URLs are always unambiguous, so this rule is applied to every #NNN without exception.

Address the root cause when the hook fires. Rewrite the reference into the correct unambiguous form. Never bypass the check with git commit --no-verify, by editing or deleting the hook, or with any suppression file.

Never use a bare @mention

Do not write a bare @name (an @ followed by a username-like token) anywhere in a commit message. A commit-msg hook (.husky/reject-bare-mentions.mjs) rejects them.

GitHub turns any @name into a mention of that user/org/team, which is wrong either way it is meant:

  • If it is code (a scoped package like @pnpm/core, a handle, a path), GitHub should not treat it as a mention.
  • If it really is a person, every push, force-push, and rebase that carries the commit re-notifies them — noise nobody asked for.

Fix: wrap the reference in backticks so GitHub renders it as code and sends no notification — e.g. `@pnpm/core` or `@foo` — or remove it if it is not needed. Never bypass the check with git commit --no-verify, by editing or deleting the hook, or with any suppression file.

Changesets

Bug fixes for features that have not yet been released do not need a changeset. This exemption applies to both TypeScript and Rust products. Check whether the affected feature has shipped before adding a changeset for a bug fix.

Except for the unreleased-feature bug fixes described above, if your changes affect published packages, you MUST create a changeset file in the .changeset directory (pnpm change records one interactively; pnpm change status shows the pending release plan). The file describes the change and specifies the affected packages with their pending version bump types: patch, minor, or major. Write the description for pnpm users and keep it concise — it becomes a release note. Implementation rationale belongs in the commit message, not the changeset. The bare pnpm version -r consumes the pending changesets at release time; there is no separate @changesets/cli dependency.

IMPORTANT: For changes to the TypeScript pnpm v11 CLI, always explicitly include "pnpm" in the changeset with a patch bump. The changeset description will appear on the release notes page. For pnpm v12 changes, follow the Rust-product rules below and target pacquet instead.

Example:

---
"@pnpm/installing.deps-installer": patch
"pnpm": patch
"pacquet": patch
---

Fixed `pnpm install` failing when a workspace dependency uses an aliased catalog entry.

The package targets already place each changeset on the correct release page. Do not name a pnpm release line or implementation in the release-note text, such as "pnpm v11", "pnpm v12", or "pacquet", merely to distinguish its targets. Describe the behavior of the published pnpm command.

Use one changeset for several release lines only when the same release-note text applies to every target. If the user-visible behavior differs, create separate changesets with the targets and wording for each behavior.

The TypeScript pnpm v11 CLI is maintenance-only. Its changesets use patch bumps for bug fixes and internal maintenance. Do not implement new features or breaking changes in v11.

Changeset style

A changeset is a release note. Someone skimming the changelog for the one entry that affects them should read it once and know what changed for them. Release blog posts are assembled from these entries, so their wording is the wording users see.

  • Lead with what the user sees. The first sentence names the command, setting, or behavior that changed and how. If the mechanism matters to a user, give it a second sentence of its own. If it does not, leave it out. It belongs in the commit body.
  • One idea per sentence. Split any sentence a reader would have to read twice. Do not chain several changes with colons, semicolons, or a parenthetical. A performance changeset says what got faster and for whom, not which caches and hash functions changed.
  • End with the issue link, not with the old behavior. If the reader needs the old behavior to recognize the bug, describe it in its own past-tense sentence. Do not append "instead of ..." or "rather than ..." to every sentence.
  • Plain punctuation. No em dashes or en dashes. End the sentence or use a comma. A colon only introduces a list or an example. No bold or italics for emphasis. Straight quotes.
  • Plain words. "Use", not "leverage". "Many", not "numerous". "If", not "in the event that". Name the actor when it matters: "pnpm now reads the file", not "the file is now read". Replace "significantly faster" with the number, or drop the adverb.
  • No reasoning. A changeset states what pnpm does now. Design justification, "so that ..." chains, "note that", and hedging go in the commit body or the PR description.

Before committing, reread the entry and ask what makes it read as generated. The usual answers are an em dash, an "instead of" tail, and a colon-joined list of internals.

Before:

Sped up installs in large workspaces: the fast lockfile-update check no longer compares every project against every lockfile entry (or copies the whole lockfile before discovering a change needs the resolver), project ordering uses faster hashing, and the version-preference table builds in parallel [#14352](https://github.com/pnpm/pnpm/issues/14352).

After:

Sped up installs in large workspaces. The check that decides whether the lockfile needs updating no longer compares every project against every lockfile entry [#14352](https://github.com/pnpm/pnpm/issues/14352).

Before:

Fixed `pnpm run`, `pnpm exec`, `pnpm rebuild`, and the script shortcuts not loading the pnpmfile, so an `updateConfig` hook never applied to them [#14433](https://github.com/pnpm/pnpm/issues/14433). A hook's settings — `extraEnv` and `extraBinPaths` among them — now reach the scripts and commands these spawn, as they do on pnpm 11.

After:

`pnpm run`, `pnpm exec`, `pnpm rebuild`, and the script shortcuts such as `pnpm test` now load the pnpmfile, so `updateConfig` hook settings such as `extraEnv` and `extraBinPaths` reach the scripts they spawn [#14433](https://github.com/pnpm/pnpm/issues/14433).

Changesets for the Rust products

The Rust products are released through the same native flow. Their npm wrapper packages are workspace packages with committed versions, so a user-visible change to a Rust product needs a changeset too, unless it fixes a bug in an unreleased feature. Target:

  • pacquet — the Rust pnpm v12 CLI (published to npm as pnpm and @pnpm/exe; named pacquet in-repo so its name can't collide with the TypeScript CLI). @pnpm/napi is a versioning.fixed group with it and bumps with it automatically.
  • @pnpm/napi — the Node.js addon bindings for the Rust engine.
  • @pnpm/pnpr — the pnpr registry server (published as @pnpm/pnpr and its platform packages, plus the ghcr.io/pnpm/pnpr Docker image).
  • get-pnpm — the installer package. A change to it that the pnpm v12 npm package ships needs a pacquet entry as well, since the wrapper bundles get-pnpm's source.

pnpm v12 and its NAPI addon release as stable versions on the main lane. pnpr releases on the alpha prerelease lane configured in pnpm-workspace.yaml; pnpm lane main --filter … graduates it to a stable version.

Do not add "pnpm" to a Rust-only changeset: in changesets, pnpm always means the TypeScript v11 CLI package. A changeset for a pnpm v12 feature or v12-only bug fix targets pacquet and omits "pnpm". A shared bug fix that lands in both versions may use one changeset naming both the affected TypeScript packages (plus "pnpm") and the Rust wrappers only when the same release-note text applies to both.

Use pacquet as the changeset package name, but use pnpm in its release-note prose and command examples (pnpm add, not pacquet add). The published Rust CLI's executable is pnpm; pacquet is only its in-repo package identifier.

Comments

These conventions apply to the TypeScript pnpm CLI, pacquet, and pnpr. Product-specific AGENTS.md files may add language-specific rules, but they do not weaken this baseline.

Write code that explains itself. A reader should understand what a function does from its name, parameters, and types — not from prose above the call site.

Defaults:

  • Do not write a comment that restates what the code already says. If renaming a variable, splitting a helper, or moving a check to a more obvious place would carry the information, do that instead.
  • Do not repeat documentation at call sites that already lives on the callee. If the function has JSDoc, a Rust doc comment, or equivalent API documentation, the call site shouldn't re-explain what calling it does. Update the documentation once; let every call site benefit.
  • Put a shared why in one place. When the same rationale underlies several related functions — peers that delegate to a common helper, or a type and its methods — document it once at that common home and reference it from the rest, instead of re-deriving it in each. This is the call-site rule applied sideways across peers, not just upward to a callee.
  • Documentation comments are for the item's contract — preconditions, postconditions, edge cases, why the item exists. Not for re-narrating the body.
  • Do not record past implementation shape, refactor history, or "the previous code did X" framing. That's what git log and git blame are for. Describe the current contract — what the code is and what it guarantees — not what it replaced. Phrasings like "used to", "previously", "the original X", or a parenthetical naming a removed type belong in the commit message, not in the source.

Write a comment only when:

  • The reason for the code is non-obvious from reading it (a hidden invariant, a workaround for a known bug, a deliberate exception to the surrounding pattern).
  • The right name doesn't fit — e.g., a temporary technical constraint that's worth flagging but doesn't justify a new symbol.

Before adding a comment, ask: "Could I rename, restructure, or extract instead?" If yes, do that. The bar for prose-in-code is high; the bar for prose-that-restates-code is "don't."

Register

The readers are engineers. Write for them:

  • Concise. State each fact once, in the shortest form that is still precise. Text that repeats information stated elsewhere links to it instead. Length is not thoroughness.
  • Structured. Prefer a list, a table, or a labelled item over prose. Do not write long paragraphs. A doc block of several paragraphs is cut to the contract.
  • One word per concept. A synonym signals a distinction, and a reader who meets one goes looking for it. Spell a value the way the code or the message spells it.

Prose that cannot go stale

A sentence that repeats a fact the code states has no test. Write the form that cannot go stale:

  • No count over a list that can grow ("three products"). Name the list, not its size.
  • A predicate a reader can grep for ("every crate that depends on pnpm-lockfile") instead of a hand-maintained roster. A roster that helps is marked "for example".
  • No verbatim quotation of another file's heading or prose. Link the section by anchor; in Rust, link the item.
  • One home per fact. A limit, a default, a path, or a setting name is stated once and linked from everywhere else. A default is documented on the field that has it, never on the field's type or on an enum variant.
  • Fake names in guide examples. Code copied from a live item drifts when the item changes.
  • A statement about behaviour is a claim. Verify it with a test, a compiler experiment, or a measurement, or leave it out. Say in the pull request what could not be verified.
  • A deliberate exception to the surrounding pattern gets a one-line comment at the site, so the next reader does not take it for an oversight. A parameter every caller passes with the same value either says why or goes.
  • A document that describes planned work states its status at the top and is deleted when the work lands. Grep for its name to find the references that would dangle.

User-facing text

Most documentation in this repository is internal. The user-facing text is:

  • the website documentation under pnpm/docs/, pnpm11/docs/, and pnpr/docs/;
  • changesets;
  • doc comments on clap commands and arguments, which are the --help output;
  • the @pnpm/napi type declarations in pnpm/npm/napi/index.d.ts;
  • diagnostics and log messages;
  • the READMEs of published npm packages.

User-facing text names only what the user can reach: their project, their config, the pnpm command, npm, Node.js. It does not name a crate, a module, a Rust or TypeScript item, the other implementation, the in-repo package name pacquet, a workflow, or a just recipe. It states a limitation by what the user sees, not by the mechanism behind it. Everything else is contributor text and says what helps a contributor.

Reviewing documentation and comments

  • A review does not increase verbosity. For a nitpick, prefer removal over addition.
  • A fix for a nitpick follows the same rule: remove rather than add.

Prose linting

Vale enforces the rules above with the style in .vale/styles/pnpm. .vale.ini sets which rules apply to which files. pnpm lint:prose runs it on the Markdown, the comments in Rust and TypeScript, and the changesets added since origin/main. Only errors fail the run. Pass --minAlertLevel=warning to also see the warnings.

Vale flags patterns, not meaning. After a prose rewrite, check whether the opening answers the reader's question, conditions are easy to find, and punctuation makes the sentence easier to read. If behavior is uncertain, name the condition or source of uncertainty instead of removing it.

Fix an alert by rewriting the sentence. Turn a rule off with a <!-- vale <rule> = NO --> comment only where the text quotes the words the rule rejects.

Code Style (TypeScript only)

This repository uses Standard Style with a few modifications:

  • Trailing commas are used.
  • Functions are preferred over classes.
  • Functions are declared after they are used (hoisting is relied upon).
  • Functions should have no more than two or three arguments. If a function needs more parameters, use a single options object instead.
  • Import Order:
    1. Standard libraries (e.g., fs, path).
    2. External dependencies (sorted alphabetically).
    3. Relative imports.

To ensure your code adheres to the style guide, run:

pnpm run lint

Size and shape limits

@pnpm/eslint-config ports the perfectionist rules that the Rust workspace enforces through dylint.toml, with the limits stated in the pacquet style guide. It also enforces:

  • Variables, parameters, and type parameters have descriptive names, not single letters.
  • Every eslint-disable directive gives a reason after --.

Tests are exempt from the length, local-name, and chain limits, as they are in Rust. Meet a limit by refactoring, not by disabling the rule: extract a helper named for what it does, return early, or name a predicate. Packages that do not pass the size limits yet are listed in PENDING_SIZE_AND_SHAPE_REFACTOR in eslint.config.mjs. When you refactor a package to pass them, remove it from that list.

Conventions

Recurring engineering conventions in this codebase — the rules reviewers most often enforce:

  • Errors. Throw PnpmError (from @pnpm/error) for user-reachable errors — they are part of the UX and carry a stable code. Programmer-error, type-guard, and unreachable-branch errors stay plain Error. Never swallow errors; catch only the specific expected code (not "any error" when you meant ENOENT). Throw on impossible states rather than continuing. Error messages must carry context, e.g. the offending path.
  • Naming. Functions are verbs; types and fields are specific, not generic. Reuse existing terminology rather than inventing synonyms. File names follow the existing convention; rename a concept everywhere it appears.
  • Reuse repo libraries. Don't add a dependency, or hand-roll logic, for a job an existing repo utility or an already-present library does — search for it first. Deduplicate copy-pasted logic into a shared function or package.
  • String parsing. Prefer plain string operations over a custom regular expression. When the input needs structured parsing with backtracking, use the existing parser-combinator pattern (object/property-path).
  • Dependency placement. Shared infrastructure (the logger, etc.) is a peer dependency. (The narrowest-package rule is covered under "Code Reuse and Avoiding Duplication" above.)
  • Config and layering. Configurable values flow through @pnpm/config.reader and reach commands via options — don't hardcode them (CLI options are camelCased automatically). Command handlers return data and let the CLI print it, which keeps them unit-testable. Don't add a wrapper function that adds nothing.
  • Async and loops. Prefer async fs and async/await; run independent work with Promise.all/Promise.any and await what must complete; hoist invariant work out of loops.

Common Gotchas

Error Type Checking (TypeScript only)

When checking if a caught value is an Error, use isError() from @pnpm/error. Do not use instanceof Error or util.types.isNativeError() alone. Jest runs tests in a VM context where instanceof fails across realms. StackBlitz WebContainers reject async fs calls with errors that util.types.isNativeError() does not recognize. isError() accepts both.

import { isError } from '@pnpm/error'

try {
  // ... some operation
} catch (err: unknown) {
  // ❌ Wrong - fails in Jest
  if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
    return null
  }

  // ❌ Wrong - fails in WebContainers
  if (util.types.isNativeError(err) && 'code' in err && err.code === 'ENOENT') {
    return null
  }

  // ✅ Correct
  if (isError(err) && 'code' in err && err.code === 'ENOENT') {
    return null
  }
  throw err
}

Working with GitHub PRs, Issues, and Comments

The pull-requests skill covers taking a change through a pull request: opening it from the template, waiting for the checks, and working the review rounds. These rules hold whether or not it is loaded:

  • Open the PR as a draft. CI runs on a draft in this repository and the reviewers do not, so the checks and your own pass over the diff happen before the first round of review. gh pr ready <pr> once the checks are green and that pass is clean.

  • A push is not the end of the task. Every push re-triggers CI and the review bots. Wait for the checks and the new review round, investigate every failure, verify each finding before acting on it, and push the fixes. Repeat until the checks are green and a round produces nothing to act on. Handing back a PR that has an unread round or a red check on it is unfinished work.

  • File tasks in pnpm/tasks, not in pnpm/pnpm. An issue in pnpm/pnpm is a bug report against released pnpm behavior. Planned work goes to pnpm/tasks (gh issue create -R pnpm/tasks): follow-ups left by a PR, refactors, performance work, v11/v12 parity gaps, CI, benchmark and test-harness work, and docs to update after a release. Feature ideas go to Discussions. Report a security vulnerability the way SECURITY.md directs, not as a task.

  • Sign all agent-authored content. When posting a comment, creating an issue, or opening a PR, append a footer to the message indicating that it was written by an agent. The footer must include the name of the agent and the name of the model used. Example:

    ---
    Written by an agent (Claude Code, claude-opus-4-7).
    

Resolving Conflicts in GitHub PRs

Use .agents/skills/pull-requests/scripts/resolve-pr-conflicts.sh to resolve PR conflicts:

./.agents/skills/pull-requests/scripts/resolve-pr-conflicts.sh <PR_NUMBER>

The script force-fetches the base branch (avoiding stale refs), rebases, auto-resolves pnpm-lock.yaml conflicts via pnpm install, force-pushes, and verifies GitHub sees the PR as mergeable. For non-lockfile conflicts it will pause and list the files that need manual resolution; finish those with <PR_NUMBER> --continue, which resumes that paused rebase in place. Pass --no-push to stop after the rebase and print the push command instead of running it.

Agent Skills

The repository's skills live in .agents/skills/<name>/SKILL.md, one directory per skill.

Codex reads that directory as-is. Claude Code only looks in .claude/skills, so .claude/skills is a symlink to ../.agents/skills. Git stores the symlink, and .gitignore keeps ignoring everything else under .claude, so a local settings.local.json stays untracked.

Add a new skill under .agents/skills; nothing else needs to change.

Git only writes a real symlink on Windows when the clone has core.symlinks=true, which needs Developer Mode or an elevated shell. Without it .claude/skills is checked out as a text file holding the target path, and Claude Code finds no skills. CLAUDE.md is a symlink to AGENTS.md, so such a clone loses the project instructions the same way.

Key Configuration Files

  • pnpm-workspace.yaml: Defines the workspace structure.
  • package.json (root): Root scripts and devDependencies.
  • CONTRIBUTING.md: Detailed contribution guidelines.