RuleStack

Configs

Stacks

Compare

Diff

RuleStack

Configs

Stacks

Compare

Diff

Read API

RuleStack

Configs

Stacks

Compare

Diff

Read API

Diff/pnpm-pnpm-agents ↔ pnpm-pnpm-pnpm-agents

Comparison

A · AGENTS.md · pnpm/pnpmB · AGENTS.md · pnpm/pnpm
What each file covers, counted
DimensionSharedOnly in AOnly in BOverlap
Sections229194%
Commands126312%
Section tags66340%

What each file covers

Sections

2 shared · 29 only in A · 19 only in B
  • − Agent Guide to pnpm Repository
  • − Keep pnpm and pacquet in sync
  • − Repository Structure
  • − Core Directories
  • − CLI & Configuration
  • − Other Functional Directories
  • − Shared Utilities
  • − Rust Projects
  • − Setup & Build (TypeScript only)
  • − Testing (TypeScript only)
  • − From the project directory
  • − From the root, filtering by package name
  • − Linting (TypeScript only)
  • − Never ignore test failures
  • − AI Review Guidance
  • − Code Reuse and Avoiding Duplication
  • − Install the git hooks before committing
  • − or, if dependencies are already installed, register the hooks on their own:
  • − Never use bare `#NNN` issue/PR references
  • − Never use a bare `@mention`
  • − Changesets
  • − Changesets for the Rust products
  • − Code Style (TypeScript only)
  • − Conventions
  • − Common Gotchas
  • − Error Type Checking in Jest (TypeScript only)
  • − Working with GitHub PRs, Issues, and Comments
  • − Resolving Conflicts in GitHub PRs
  • − Key Configuration Files
  • + AGENTS.md (pacquet)
  • + What this project is
  • + The cardinal rule
  • + Modeling branded string types
  • + Follow the project guides
  • + Repo layout (inside `pnpm/`)
  • + Commands
  • + Tests
  • + No "tolerant" tests for missing tools
  • + Running tests narrowly
  • + One crate
  • + One test by name substring
  • + One integration test file
  • + Style
  • + Preserve existing method chains
  • + Code reuse (pacquet specifics)
  • + Errors and diagnostics
  • + Commit and PR hygiene
  • + Things not to do
  •   Commit Messages
  •   Comments

Commands

1 shared · 26 only in A · 31 only in B
  • − pnpm install
  • − pnpm run compile
  • − pnpm --filter <package_name> run compile
  • − pnpm --filter pnpm run compile
  • − pnpm/test/
  • − pnpm --filter <package_name> test
  • − pnpm --filter <package_name> test <file_path>
  • − pnpm --filter <package_name> test <file_path> -t <test_name_pattern>
  • − pnpm run lint
  • − pnpm exec husky
  • − pnpm/AGENTS.md
  • − pnpm/dist/pnpm.mjs
  • − pnpm run bundle
  • − git config core.hooksPath
  • − pnpm/pnpm#NNN
  • − git commit --no-verify
  • − pnpm change
  • − pnpm change status
  • − pnpm version -r
  • − pnpm-workspace.yaml
  • − pnpm lane main --filter …
  • − pnpm add
  • − git log
  • − git blame
  • − gh pr create
  • − pnpm-lock.yaml
  • + cargo nextest run -p pacquet-lockfile
  • + cargo nextest run -p pacquet-lockfile <name_substring>
  • + cargo nextest run -p pacquet-lockfile --test <file_stem>
  • + pnpm:<channel>
  • + pnpm login
  • + pnpm publish
  • + cargo
  • + just
  • + just ready
  • + just test
  • + cargo nextest run
  • + just lint
  • + cargo clippy --locked --workspace --all-targets -- --deny warnings
  • + just check
  • + cargo check --locked --workspace --all-targets
  • + just fmt
  • + cargo fmt
  • + just cli -- <args>
  • + just registry-mock <args>
  • + just integrated-benchmark <args>
  • + cargo insta review
  • + cargo test
  • + just registry-mock launch
  • + git restore <file>
  • + git
  • + node
  • + npm
  • + pnpm/scripts/pre-push-rust.sh
  • + cargo clippy
  • + cargo-dylint
  • + git log --oneline
  •   pnpm

Section tags

6 shared · 6 only in A · 3 only in B
  • − setup
  • − build
  • − dependencies
  • − performance
  • − do-not
  • − agent-behaviour
  • + lint-format
  • + testing-strategy
  • + monorepo
  •   test
  •   code-style
  •   architecture
  •   types
  •   git-pr
  •   docs

Line diff

+361 added−260 removed74 unchanged17.0% identical
pnpm/pnpm · AGENTS.md
@@ −1 @@
1# Agent Guide to pnpm Repository
2 
3This document provides context and instructions for AI agents working on the pnpm codebase.
4 
5The repository contains three products:
6 
7- The **TypeScript pnpm CLI** — the main TypeScript workspaces outside `pnpm/` and `pnpr/`.
8- The **Rust pacquet port** — `pnpm/`. See [`pnpm/AGENTS.md`](./pnpm/AGENTS.md) for pacquet-specific rules; it adds to (and never contradicts) the conventions below.
9- The **Rust pnpr registry server** — `pnpr/`. See [`pnpr/AGENTS.md`](./pnpr/AGENTS.md) for pnpr-specific rules; it adds to (and never contradicts) the conventions below.
10 
11Sections 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.
 
 
 
 
 
 
12 
13## Keep pnpm and pacquet in sync
14 
15The two stacks are parallel implementations of the same CLI, kept behaviorally identical — the same flags, defaults, error codes, file formats, and lockfile shape. They are now at near-complete feature parity and are developed together, so **any user-visible change has to land in both at the same time.** Neither stack is downstream of the other: pacquet is a source of truth in its own right, not a port that trails the TypeScript CLI.
 
 
 
 
 
 
 
 
16 
17When you change one side, do the equivalent change on the other in the same PR if you can. If you can't (different expertise, scope too large, or pacquet hasn't ported the surrounding feature yet), open the PR with just your side — call out in the description what still needs porting, and someone else will push the matching commits to the same PR before it lands.
 
 
 
 
 
18 
19"User-visible" means anything that affects the CLI surface or the on-disk contract: command-line flags and defaults, environment-variable handling, lockfile/manifest/state-file format, error codes and messages, log emissions parsed by `@pnpm/cli.default-reporter`, store layout, hook semantics. Pure internal refactors, perf wins, and TS-only test cleanups don't need mirroring.
20 
21**Any user-visible change to either stack must be replicated in the other.**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22 
23The pacquet-side conventions for keeping the two stacks aligned are in [`pnpm/AGENTS.md`](./pnpm/AGENTS.md#the-cardinal-rule).
 
24 
25## Repository Structure
26 
27The pnpm codebase is a monorepo managed by pnpm itself. The root contains functional directories organized by domain:
 
 
 
 
 
 
 
 
28 
29### Core Directories
 
30 
31- `pnpm/`: The CLI entry point and main package.
32- `pkg-manager/`: Core package management logic (installation, linking, etc.).
33- `resolving/`: Dependency resolution logic (resolvers for npm, tarballs, git, etc.).
34- `fetching/`: Package fetching logic.
35- `store/`: Store management logic (content-addressable storage).
36- `lockfile/`: Lockfile handling, parsing, and utilities.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37 
38### CLI & Configuration
39 
40- `cli/`: CLI command implementations and infrastructure.
41- `config/`: Configuration management and parsing.
42- `hooks/`: pnpm hooks (readPackage, etc.).
43- `completion/`: Shell completion support.
44 
45### Other Functional Directories
46 
47- `network/`: Network-related utilities (proxy, fetch, auth).
48- `workspace/`: Workspace-related utilities.
49- `exec/`: Execution-related commands (run, exec, dlx).
50- `env/`: Node.js environment management.
51- `cache/`: Cache-related commands and utilities.
52- `patching/`: Package patching functionality.
53- `reviewing/`: License and dependency review tools.
54- `releasing/`: Release and publishing utilities.
 
 
 
 
 
55 
56### Shared Utilities
 
 
57 
58- `packages/`: Shared utility packages (constants, error handling, logger, types, etc.).
59- `fs/`: Filesystem utilities.
60- `crypto/`: Cryptographic utilities.
61- `text/`: Text processing utilities.
62 
63### Rust Projects
 
 
64 
65- `pnpm/`: The pnpm CLI ported to Rust. Self-contained sub-project with its own crates, tests, and tooling — see [`pnpm/AGENTS.md`](./pnpm/AGENTS.md).
66- `pnpr/`: The pnpm-compatible npm registry server. Self-contained sub-project with its own crates, tests, and tooling — see [`pnpr/AGENTS.md`](./pnpr/AGENTS.md).
 
 
 
 
 
 
 
 
67 
68## Setup & Build (TypeScript only)
 
69 
70To set up the environment and build the project:
71 
72```bash
73pnpm install
74pnpm run compile
75```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76 
77To compile a specific package:
78 
79```bash
80pnpm --filter <package_name> run compile
81```
82 
83**Important:** The pnpm CLI e2e tests (in `pnpm/test/`) use the **bundled** `pnpm/dist/pnpm.mjs`, not the individual package `lib/` outputs. After changing any package, you must rebuild the bundle before running e2e tests:
 
 
 
 
 
 
 
84 
85```bash
86pnpm --filter pnpm run compile
 
 
 
 
 
87```
88 
89This runs `tsgo --build`, linting, and `pnpm run bundle` (which bundles all packages into `pnpm/dist/pnpm.mjs`). Without this step, e2e tests will use a stale bundle and your changes won't be tested.
 
 
 
 
 
90 
91## Testing (TypeScript only)
 
 
 
92 
93Never run all tests in the repository as it takes a lot of time.
 
 
 
 
 
94 
95Run tests for a specific project instead:
96 
97```bash
98# From the project directory
99pnpm test
100 
101# From the root, filtering by package name
102pnpm --filter <package_name> test
103```
104 
105Or better yet, run tests for a specific file:
 
106 
107```bash
108pnpm --filter <package_name> test <file_path>
109```
110 
111Or a specific test case in a specific file:
112 
113```bash
114pnpm --filter <package_name> test <file_path> -t <test_name_pattern>
115```
116 
117## Linting (TypeScript only)
118 
119To run all linting checks:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120 
121```bash
122pnpm run lint
123```
124 
125## Never ignore test failures
126 
127Do 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.
128 
129## AI Review Guidance
 
 
130 
131The repository's review framework lives in **[REVIEW_GUIDE.md](./REVIEW_GUIDE.md)** — how changes are accepted or rejected, the security-first / performance-second priorities, the security checklist and advisory regression themes, and the test/changeset/parity 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.)
132 
133Security 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.
134 
135## Code Reuse and Avoiding Duplication
 
 
 
 
 
 
 
136 
137**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.
 
138 
139- **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 `packages/`, `fs/`, `crypto/`, `text/`, and other shared directories first.
140- **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.
141- **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.
142- **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.
143 
144## Commit Messages
145 
146Follow the [Conventional Commits](https://www.conventionalcommits.org/) specification.
147 
148- `feat`: a new feature
149- `fix`: a bug fix
150- `docs`: documentation only changes
151- `style`: formatting, missing semi-colons, etc.
152- `refactor`: code change that neither fixes a bug nor adds a feature
153- `perf`: a code change that improves performance
154- `test`: adding missing tests
155- `chore`: changes to build process or auxiliary tools
156 
157### Install the git hooks before committing
158 
159The 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:
160 
161```bash
162pnpm install # runs the "prepare": "husky" script as part of install
163# or, if dependencies are already installed, register the hooks on their own:
164pnpm exec husky
165```
166 
167You 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.
168 
169### Never use bare `#NNN` issue/PR references
170 
171**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.
172 
173GitHub 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 two forms:
174 
175- Using `#1`, `#2`, `#3`, … 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.
176- 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`.
177 
178For 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.
179 
180**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.
181 
182### Never use a bare `@mention`
183 
184**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.
185 
186GitHub turns any `@name` into a mention of that user/org/team, which is wrong either way it is meant:
187 
188- If it is code (a scoped package like `@pnpm/core`, a handle, a path), GitHub should not treat it as a mention.
189- If it really is a person, every push, force-push, and rebase that carries the commit re-notifies them — noise nobody asked for.
190 
191**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.
192 
193## Changesets
194 
195If 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.
196 
197**IMPORTANT: Always explicitly include `"pnpm"` in the changeset** with the appropriate version bump (patch, minor, or major). The pnpm CLI will only receive automatic patch bumps from its dependencies, so if your change warrants a minor or major version bump for the CLI, you must specify it explicitly. The changeset description will appear on the release notes page.
198 
199Example:
200 
201```
202---
203"@pnpm/installing.deps-installer": minor
204"pnpm": minor
205---
206 
207Added a new setting `blockExoticSubdeps` that prevents the resolution of exotic protocols in transitive dependencies [#10352](https://github.com/pnpm/pnpm/issues/10352).
208```
 
 
209 
210**Versioning Guidelines for pnpm CLI:**
211- **patch**: Bug fixes, internal refactors, and changes that don't require documentation updates
212- **minor**: New features, settings, or commands that should be documented (anything users should know about)
213- **major**: Breaking changes
214 
215### Changesets for the Rust products
 
 
 
216 
217The 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, targeting:
 
 
 
 
 
 
 
218 
219- `pacquet` — the Rust pnpm CLI (published to npm as `pnpm` and `@pnpm/exe` under its `next-<major>` dist-tag; 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.
220- `@pnpm/napi` — the Node.js addon bindings for the Rust engine.
221- `@pnpm/pnpr` — the pnpr registry server (published as `@pnpm/pnpr` and its platform packages, plus the `ghcr.io/pnpm/pnpr` Docker image).
222 
223The Rust products release on `alpha` lanes (`versioning.lanes` in `pnpm-workspace.yaml`): each run of `pnpm version -r` that consumes an intent for one of them cuts an `X.Y.Z-alpha.N` prerelease, while the TypeScript CLI keeps releasing stable versions on the main lane. `pnpm lane main --filter …` graduates a product to a stable version.
 
 
 
224 
225Do not add `"pnpm"` to a Rust-only changeset: in changesets, `pnpm` always means the TypeScript CLI package. A changeset whose implementation is Rust-only and targets `pacquet` must omit `"pnpm"`. A parity change that lands in both stacks carries one changeset naming both the affected TypeScript packages (plus `"pnpm"`) and the Rust wrapper(s).
226 
227Use `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.
 
 
 
 
 
 
 
 
 
 
 
228 
229## Comments
230 
231These 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.
 
 
 
 
232 
233Write 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.
234 
235Defaults:
236 
237- **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.
238- **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.
239- **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.
240- **Documentation comments are for the item's contract** — preconditions, postconditions, edge cases, why the item exists. Not for re-narrating the body.
241- **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.
242 
243Write a comment only when:
244 
245- 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).
246- The right name doesn't fit — e.g., a temporary technical constraint that's worth flagging but doesn't justify a new symbol.
247 
248Before 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."
249 
250## Code Style (TypeScript only)
251 
252This repository uses [Standard Style](https://github.com/standard/standard) with a few modifications:
253- **Trailing commas** are used.
254- **Functions are preferred** over classes.
255- **Functions are declared after they are used** (hoisting is relied upon).
256- **Functions should have no more than two or three arguments.** If a function needs more parameters, use a single options object instead.
257- **Import Order**:
258 1. Standard libraries (e.g., `fs`, `path`).
259 2. External dependencies (sorted alphabetically).
260 3. Relative imports.
261 
262To ensure your code adheres to the style guide, run:
263 
264```bash
265pnpm run lint
266```
267 
268### Conventions
269 
270Recurring engineering conventions in this codebase — the rules reviewers most often enforce:
271 
272- **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.
273- **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.
274- **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.
275- **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`).
276- **Dependency placement.** Shared infrastructure (the logger, etc.) is a peer dependency. (The narrowest-package rule is covered under "Code Reuse and Avoiding Duplication" above.)
277- **Config and layering.** Configurable values flow through `@pnpm/config` 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.
278- **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.
279 
280## Common Gotchas
281 
282### Error Type Checking in Jest (TypeScript only)
283 
284When checking if a caught error is an `Error` object, **do not use `instanceof Error`**. Jest runs tests in a VM context where `instanceof` checks can fail across realms.
285 
286Instead, use `util.types.isNativeError()`:
287 
288```typescript
289import util from 'util'
290 
291try {
292 // ... some operation
293} catch (err: unknown) {
294 // ❌ Wrong - may fail in Jest
295 if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
296 return null
297 }
298
299 // ✅ Correct - works across realms
300 if (util.types.isNativeError(err) && 'code' in err && err.code === 'ENOENT') {
301 return null
302 }
303 throw err
304}
305```
306 
307## Working with GitHub PRs, Issues, and Comments
308 
309- **Open every PR with the repository template.** `gh pr create` does not apply `.github/pull_request_template.md` automatically, so read that file and pass its filled-in contents as the PR body (`--body`/`--body-file`). Keep every section (Summary, Squash Commit Body, Checklist), fill them in for this change, mark the checklist items, and remove only the lines the template says are inapplicable.
310- **Keep PR titles and descriptions current.** When pushing new changes to a PR, review the title and description and update them if they no longer accurately reflect what the PR does.
311- **Reply to and resolve review conversations.** Once a review comment has been addressed, reply to the thread with a description of the resolution including the commit hash that fixed it, then mark the conversation as resolved.
312- **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:
313 
314 ```markdown
315 ---
316 Written by an agent (Claude Code, claude-opus-4-7).
317 ```
318 
319## Resolving Conflicts in GitHub PRs
320 
321Use `shell/resolve-pr-conflicts.sh` to resolve PR conflicts:
322 
323```bash
324./shell/resolve-pr-conflicts.sh <PR_NUMBER>
325```
326 
327The 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.
328 
329## Key Configuration Files
330 
331- `pnpm-workspace.yaml`: Defines the workspace structure.
332- `package.json` (root): Root scripts and devDependencies.
333- `CONTRIBUTING.md`: Detailed contribution guidelines.
334 
pnpm/pnpm · pnpm/AGENTS.md
@@ +1 @@
1# AGENTS.md (pacquet)
2 
3Guidance for AI coding agents working in `pnpm/`.
4 
5**Read [`../AGENTS.md`](../AGENTS.md) first.** It covers the conventions that apply across the whole monorepo — GitHub PR workflow, signing agent-authored content, conventional commit messages, code-reuse philosophy, "never ignore test failures," and the PR-conflict resolution script. This file specializes those rules for pacquet's Rust code and adds pacquet-only ones.
6 
7## What this project is
 
 
8 
9`pacquet` is the [pnpm](https://pnpm.io) CLI implemented in Rust. It is one of
10two parallel implementations of the same package manager — the other is the
11TypeScript pnpm CLI (the workspaces outside `pnpm/`). The two are kept
12behaviorally identical: the same commands, flags, defaults, error codes, file
13formats, lockfile shape, and directory layout. pacquet is not a downstream port
14that trails the TypeScript CLI; it is a source of truth in its own right, at
15near-complete feature parity, and the two stacks are developed together.
16 
17## The cardinal rule
18 
19**pacquet and the TypeScript pnpm CLI must stay behaviorally identical.**
20They are parallel implementations of one package manager, developed together at
21near-complete feature parity. Any user-visible change — a command, flag,
22default, error code or message, lockfile/manifest/state-file format, log
23emission parsed by `@pnpm/cli.default-reporter`, store layout, or hook
24semantic — must land in both stacks at the same time. The repo-wide statement
25of this obligation lives in
26[`../AGENTS.md`](../AGENTS.md#keep-pnpm-and-pacquet-in-sync); this section is the
27pacquet-side detail.
28 
29Neither stack is downstream of the other. You are not "porting from" the
30TypeScript code: when you implement or change behavior in pacquet, make the
31equivalent change in the TypeScript workspaces in the same PR, and vice versa.
32If you genuinely can't (different expertise, scope too large, or the other
33stack hasn't grown the surrounding feature yet), ship your side and say so in
34the PR description so the matching commits can follow before it lands.
35 
36Working rules:
37 
381. **Keep the two implementations in agreement.** When you touch behavior in
39 pacquet, find the counterpart in the TypeScript workspaces — they live at
40 the repo root (`pnpm/` for the CLI entry, `pkg-manager/`, `resolving/`,
41 `lockfile/`, `store/`, `fetching/`, `config/`, `hooks/`, and so on; see the
42 [repo-structure section](../AGENTS.md#repository-structure)) — and change it
43 there too. The two must agree on logic, edge cases, config resolution, error
44 messages, and file/lockfile formats.
452. **Match observable behavior, not structure.** Structural similarity (similar
46 function decomposition and names) is a convenience for cross-referencing, not
47 a requirement. What must match is what a user or a downstream tool can
48 observe.
493. **Don't diverge unilaterally.** Do not add a feature, flag, or quirk to one
50 stack without the other, and do not "fix" a behavior in only one. A genuine
51 bug present in both is fixed in both.
524. **Log emissions are part of behavioral identity.** A function that fires
53 `pnpm:<channel>` events through the reporter must use the same call site,
54 payload, and ordering in both stacks so `@pnpm/cli.default-reporter` parses
55 pacquet's NDJSON the same way it parses the TypeScript CLI's. See
56 [Reporter / log events](./CODE_STYLE_GUIDE.md#reporter--log-events)
57 in the style guide for the convention (channel mapping, threading
58 `R: Reporter`, emit-site placement, recording-fake tests).
595. **Prefer real fixtures; reach for the dependency-injection seam
60 only when they can't cover the branch.** Most happy paths and
61 error paths should be tested with a `tempfile::TempDir`, the
62 mocked registry, or an integration test that spawns the actual
63 binary. Use the DI seam — a capability trait on the `Host`
64 provider, threaded as `Sys: <Bounds>` — only for branches a real
65 fixture can't reach portably: filesystem error kinds
66 (`PermissionDenied`, `ENOSPC`, …), deterministic time, shared
67 process-global state a test would otherwise mutate
68 (`env::set_var`, `set_current_dir`, the umask, …), or the
69 external-service happy paths in features like `pnpm login` (2FA)
70 and `pnpm publish` (OIDC / provenance) when those land. See
71 [Dependency injection for tests](./CODE_STYLE_GUIDE.md#dependency-injection-for-tests)
72 in the style guide for the gating rule, the names (`Sys`, `Host`,
73 `Fs*`, `Clock`, `EnvVar`, …), the eight principles, and the
74 `modules-yaml` worked example.
75 
76If the intended behavior is unclear or looks wrong, stop and ask the user
77rather than guessing.
78 
79## Modeling branded string types
80 
81TypeScript pnpm leans on *branded* string types. A branded string is a
82plain string narrowed by a phantom property (for example,
83`type PkgName = string & { __brand: 'PkgName' }`), so the type system can
84track intent that the runtime cannot see. Some brands are stamped through
85a validating constructor. Others are minted with a bare `as` type assertion and
86have no runtime check at all. Both stacks must preserve that distinction,
87because it is part of the public contract pnpm exposes through manifest,
88lockfile, state, and config files. The TypeScript brand and the Rust newtype
89must agree on validation policy.
90 
91Rules for a Rust newtype standing in for a branded string type ("the
92TypeScript side" below is its TypeScript counterpart):
93 
941. **Declare a newtype wrapper.** Do not collapse the brand into a plain
95 `String` or `&str`. Give the type its own struct so misuse is a type
96 error in pacquet too.
972. **If upstream always validates before construction, validate too.**
98 When every brand site in pnpm runs through a checking factory, pacquet's
99 wrapper must construct only via `TryFrom<String>` and/or `FromStr`. Do
100 not provide an infallible public constructor that takes an arbitrary
101 string.
1023. **If upstream never validates, just brand for type-safety.** Some
103 upstream brands exist purely to keep the type system from confusing
104 one string slot with another. For example, a brand may exist to prevent
105 a `PkgId` from being passed where a `PkgName` is expected, even though
106 the value is never validated at runtime. In that case the Rust wrapper
107 should expose an infallible `From<String>` (and `From<&str>` when
108 convenient). The type-safety win is the whole point, and no validator
109 is needed.
1104. **If upstream occasionally constructs without validation, expose
111 `from_str_unchecked`.** When pnpm sometimes mints the brand via a bare
112 `as` assertion, skipping its validator, add a `from_str_unchecked` (or
113 similarly named) constructor on the Rust side so callers can opt into
114 the same unchecked path explicitly. Keep the validating constructor as
115 well. `from_str_unchecked` is the escape hatch, not the default.
1165. **Match upstream serde behavior.** If the branded type crosses a
117 JSON, YAML, or INI boundary (manifest files, lockfiles, state files,
118 config files, and similar), wire the wrapper into serde so the
119 validation policy survives serialization:
120 - `#[serde(try_from = "String")]` for deserialization, so
121 deserialized values go through the validator.
122 - `#[serde(into = "String")]` for serialization.
123 Use both when the type is round-tripped.
1246. **Derive simple conversions with `derive_more`.** When the conversion
125 impls implied by the rules above are mechanical (a one-liner that
126 wraps or unwraps the inner field), use `#[derive(derive_more::From)]`
127 and `#[derive(derive_more::Into)]` rather than handwriting an `impl`
128 block. Fall back to a manual `impl` only when the conversion needs
129 custom logic, such as validation or normalization. `derive_more` is
130 already a workspace dependency.
1317. **String-literal unions become `enum`s.** If upstream uses a string
132 literal type or a union of string literals (for example,
133 `'auto' | 'always' | 'never'`), model it as a Rust `enum`, not a
134 newtype wrapper. The set of valid values is closed, so encode that.
1358. **Template literal types are branded strings.** If upstream uses a
136 string template literal type (for example,
137 ``` `${string}@${string}` ```), treat it the same as a branded string
138 type. Use a newtype wrapper with the validation discipline from rules
139 2 through 5 above.
140 
141## Follow the project guides
142 
1431. Follow the contributing guide in [`CONTRIBUTING.md`](./CONTRIBUTING.md), and **ALWAYS** double-check before committing. It covers commit message format, writing style, setup, and the automated checks to run before committing.
1442. Follow the code style guide in [`CODE_STYLE_GUIDE.md`](./CODE_STYLE_GUIDE.md), and **ALWAYS** double-check before committing. It covers code-level conventions not enforced by tooling: imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, `pipe-trait`, error handling, test layout, logging during tests, and cloning of `Arc` and `Rc`.
 
 
145 
146## Repo layout (inside `pnpm/`)
147 
148- `crates/` — library and binary crates that make up pacquet.
149 - `cli`, `package-manager`, `package-manifest`, `lockfile`, `store-dir`,
150 `tarball`, `registry`, `network`, `npmrc`, `fs`, `executor`,
151 `diagnostics`, `testing-utils`.
152- `tasks/` — developer tooling: `integrated-benchmark`, `micro-benchmark`,
153 `registry-mock`.
154- `CONTRIBUTING.md` — commit-message format, writing style, setup, and the
155 automated checks to run before submitting. Read it before submitting code.
156- `CODE_STYLE_GUIDE.md` — manual code-style conventions beyond what `cargo
157 fmt`, `taplo`, and clippy enforce: imports, modules, naming, ownership
158 and borrowing, trait bounds, pattern matching, `pipe-trait`, error
159 handling, test layout, and `Arc`/`Rc` cloning. Read it before submitting
160 code.
161 
162The Rust workspace (`Cargo.toml`, `Cargo.lock`, `rust-toolchain.toml`,
163`justfile`, `.cargo/`, `.taplo.toml`, etc.) lives at the **repo root**, not
164inside `pnpm/`. Run `cargo` and `just` from the repo root.
165 
166## Commands
 
 
 
167 
168Prefer `just` recipes when one fits; drop down to `cargo` / `taplo` / etc.
169directly when you need flags the recipe doesn't expose (e.g. filtering tests
170by crate or name — see below).
171 
172- `just ready` — run the same checks CI runs (typos, fmt, check, test, lint).
173 Run this before declaring a task complete.
174- `just test` — `cargo nextest run`.
175- `just lint` — `cargo clippy --locked --workspace --all-targets -- --deny warnings`.
176- `just check` — `cargo check --locked --workspace --all-targets`.
177- `just fmt` — `cargo fmt` + `taplo format`.
178- `just cli -- <args>` — run the pacquet binary.
179- `just registry-mock <args>` — manage the mock registry used by tests.
180- `just integrated-benchmark <args>` — compare revisions or compare against
181 pnpm itself (see `CONTRIBUTING.md`).
182 
183Warnings are errors (`--deny warnings` in lint). Do not silence them with
184`#[allow(...)]` unless there is a specific, justified reason.
185 
186## Tests
187 
188- Tests live alongside the code they exercise (standard Cargo layout) plus
189 integration tests under each crate's `tests/`. Shared pacquet fixtures live
190 under `crates/testing-utils/src/fixtures/`; registry package fixtures live
191 under `../pnpr/.fixtures/packages/`.
192- Snapshot tests use `insta`. When an intentional change alters a snapshot,
193 review the diff carefully, then accept with `cargo insta review`. Never
194 accept snapshot changes blindly.
195- Tests that need the mocked registry start `pnpr` through
196 `pacquet-testing-utils`; `cargo test` / `cargo nextest run` should not
197 require a separate `just registry-mock launch` step.
198- When a behavior change spans both stacks, keep their tests in sync — give
199 pacquet a Rust test for the same scenario the TypeScript stack covers (and
200 vice versa) whenever it translates. Matching test coverage is the easiest
201 way to prove behavioral parity.
202- The active test-porting plan lives in
203 [`plans/TEST_PORTING.md`](./plans/TEST_PORTING.md). It enumerates the
204 upstream TypeScript tests scheduled to be ported (with file paths and line
205 numbers) and the conventions expected of the ports — `known_failures`
206 modules, `pacquet_testing_utils::allow_known_failure!` at the
207 not-yet-implemented boundary, and the practice of temporarily breaking the
208 subject under test to verify the ported test actually catches the
209 regression. Consult it before adding ported tests, and update its
210 checkboxes as items land.
211- When temporarily breaking an implementation (to prove a test catches the
212 regression, or for any other experiment), revert with `git restore <file>`,
213 never by moving a saved backup copy into place. Cargo's freshness check is
214 mtime-based, and an mtime-preserving restore leaves the binary compiled
215 from the broken source looking fresh — later test runs then fail in
216 impossible-looking, "flaky" ways with nothing pointing at the stale
217 artifact. Details in the "Test the tests" section of
218 [`plans/TEST_PORTING.md`](./plans/TEST_PORTING.md). If test outcomes ever
219 flip with no code change, `touch` the implementation file and rerun before
220 debugging anything else.
221 
222### No "tolerant" tests for missing tools
223 
224Tests must not be tolerant of a missing build / runtime environment by
225silently `return`-ing early when a tool isn't found. Patterns like:
 
226 
227```rust
228fn skip_if_no_git() -> bool {
229 if std::process::Command::new("git").arg("--version").output().is_err() {
230 eprintln!("skipping: `git` not on PATH");
231 return true;
232 }
233 false
234}
235 
236#[test]
237fn my_test() {
238 if skip_if_no_git() {
239 return;
240 }
241 // ...
242}
243```
244 
245are forbidden. If the test needs a tool, just call into it and let the
246existing `.unwrap()` / `.expect(...)` panic when the tool is absent — a
247failing test in an under-provisioned environment is the correct signal.
248Tolerance defeats the purpose of testing: if the environment really
249doesn't have the required tools, that's the *environment's* fault and it
250needs to be fixed.
251 
252This applies in particular to `git`, `node`, and `npm` — git is ubiquitous
253on developer machines, and Node.js is a documented prerequisite for
254building pnpm. There is no realistic environment in which pacquet's tests
255should run *and* these tools should be absent.
256 
257The only marginally acceptable exception is platform-locked tools — APIs
258or binaries that exist on one OS but not another. Even then, prefer
259`#[cfg_attr(target_os = "windows", ignore = "...")]` (or the matching
260`#[cfg(unix)]` gate already used in this crate for `/bin/sh` shims) over a
261runtime probe-and-skip helper. The gate is visible to `cargo test` and
262shows up in the test report; a silent `return` does not.
263 
264### Running tests narrowly
265 
266Running the full suite is slow. While iterating, target what you're working
267on:
 
268 
269```sh
270# One crate
271cargo nextest run -p pacquet-lockfile
272 
273# One test by name substring
274cargo nextest run -p pacquet-lockfile <name_substring>
275 
276# One integration test file
277cargo nextest run -p pacquet-lockfile --test <file_stem>
278```
279 
280Run `just ready` (full suite) before handing the PR off.
281 
282## Style
 
 
283 
284`CODE_STYLE_GUIDE.md` is the source of truth. Highlights:
285 
286- Choose owned vs. borrowed parameters to minimize copies; widen to the most
287 encompassing type (`&Path` over `&PathBuf`, `&str` over `&String`) when it
288 doesn't force extra copies.
289- Prefer `Arc::clone(&x)` / `Rc::clone(&x)` over `x.clone()` for reference-
290 counted types, so the cost is visible at the call site.
291- Follow the test-logging guidance in the style guide — log before non-
292 `assert_eq!` assertions, `dbg!` complex structures, skip logging for simple
293 scalar `assert_eq!`.
294- Follow [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/naming.html)
295 for naming.
296- **No star imports inside module bodies.** Write `use super::{Foo, bar}`
297 instead of `use super::*;`, and the same for any other glob whose
298 target is a module you control. Two forms stay allowed: external-crate
299 preludes such as `use rayon::prelude::*;` and root-of-module
300 re-exports such as `pub use submodule::*;` in a `lib.rs`. See the
301 "No star imports" section in `CODE_STYLE_GUIDE.md`.
302 
303### Comments
 
 
304 
305Same baseline as [`../AGENTS.md`](../AGENTS.md#comments): write code that explains itself; comments are for the non-obvious *why*, not a translation of the *what*.
306 
307Rust-specific additions:
308 
309- **Doc comments (`///`, `//!`) are rustdoc-visible API documentation.** Use them for item contracts. Put implementation-only rationale in regular `//` comments.
310- **Tests are documentation. Do not duplicate them in prose.** If a behavioral scenario, edge case, failure mode, or worked example is already captured by a test (its name, its setup, its assertions), do not also narrate it in the doc comment on the implementation. The doc comment should state the contract once; the test demonstrates the behavior. The same applies in reverse: a test's own doc comment should not re-explain what the asserts already say, only the *why* if it is not obvious.
311- **`// SAFETY:`, `// TODO:`, and similar prefixes are the exception.** They signal hidden invariants or known follow-ups that a reader cannot recover from the code alone.
312 
313Prefer renaming, restructuring, or extracting a helper over leaving a comment. Reach for prose only when the right names and types genuinely cannot carry the information.
314 
315### Preserve existing method chains
316 
317When editing existing code, do not break a method chain (including `pipe-trait`
318`.pipe(...)` chains) into intermediate `let` bindings unless you can justify
319the rewrite. Valid justifications include a chain that fails to compile after
320your edit, a borrow checker rejection, a meaningful performance win from
321splitting it up, or any other concrete reason the chain cannot stay as it is.
322Refactoring for style alone is not a justification when the task is something
323else. Keep the surrounding code shape intact and confine your edits to what
324the task asks for.
325 
326When the change you need can fit inside the existing chain, keep it there.
327For example, swapping a `PathBuf::from` allocation for a `Path::new` borrow:
328 
329```diff
330 output
331 .stdout
332 .pipe(String::from_utf8)
333 .expect("convert stdout to UTF-8")
334 .trim_end()
335- .pipe(PathBuf::from)
336+ .pipe(Path::new)
337 .parent()
338 .expect("parent of root manifest")
339 .to_path_buf()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340```
341 
342Do not flatten the chain just because you happen to be editing nearby:
343 
344```diff
345-output
346- .stdout
347- .pipe(String::from_utf8)
348- .expect("convert stdout to UTF-8")
349- .trim_end()
350- .pipe(PathBuf::from)
351- .parent()
352- .expect("parent of root manifest")
353- .to_path_buf()
354+let stdout = String::from_utf8(output.stdout).expect("convert stdout to UTF-8");
355+Path::new(stdout.trim_end()).parent().expect("parent of root manifest").to_path_buf()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356```
 
 
 
 
357 
358If you do need to break a chain, state the justification in your reply, the
359commit message, or the PR description so a reviewer can confirm the rewrite
360was warranted. If the rewrite is purely stylistic, raise it with the user as
361its own change rather than including it in an unrelated edit.
362 
363## Code reuse (pacquet specifics)
 
 
 
364 
365The general "search before you write / extract shared code / prefer mature
366crates / keep deps at the right level" rules from
367[`../AGENTS.md`](../AGENTS.md#code-reuse-and-avoiding-duplication) apply.
368Pacquet-specific notes:
369 
370- Shared helpers tend to live in `crates/fs`, `crates/testing-utils`, and
371 `crates/diagnostics` — check there first.
372- Check whether the workspace already depends on something suitable (see
373 `[workspace.dependencies]` in the root `Cargo.toml`) before adding a new
374 dependency.
375- **Keep dependencies at the right level.** Add a new dependency to the
376 specific crate that needs it, not to the workspace root or to a shared
377 crate unless multiple crates actually depend on it.
378 
379## Errors and diagnostics
 
 
380 
381User-facing errors go through `miette` via the `pacquet-diagnostics` crate.
382Match pnpm's error codes and messages where pnpm defines them — error codes
383are part of the public contract, not implementation detail. See
384<https://pnpm.io/errors> for the canonical list.
385 
386## Commit and PR hygiene
387 
388- Keep commits focused. A bug fix commit should not also refactor or
389 reformat unrelated code.
390- When a change has a counterpart in the TypeScript pnpm CLI, land both
391 together; if they must be split, cross-reference the matching PR so a
392 reviewer can confirm the two stacks stay in sync.
393- Run `just ready` before pushing.
394- The repo-wide husky `pre-push` hook runs `pnpm/scripts/pre-push-rust.sh`,
395 which checks `rustfmt`, `taplo`, `cargo clippy` (with `--all-targets -D
396 warnings`), `cargo doc` (with `RUSTDOCFLAGS=-D warnings`), and `cargo
397 dylint`. Make sure your environment
398 can run cargo (the hook needs it) before pushing; `cargo-dylint` is
399 detected at runtime and skipped with a warning if not installed.
400 
401### Commit messages
402 
403Conventional Commits applies (see
404[`../AGENTS.md`](../AGENTS.md#commit-messages) for the full type list). Use
405a scope that names the crate or area being touched, matching the existing
406history (`git log --oneline` for examples). Pacquet adds one type beyond the
407standard list:
408 
409- `bench`: benchmark-only changes.
410 
411Examples (from this repo's history):
412 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413```
414fix(network): set explicit timeouts on default reqwest client
415feat(lockfile): support npm-alias dependencies in snapshots
416perf(store-dir): share one read-only StoreIndex across cache lookups
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417```
418 
419## Things not to do
420 
421- Do not add a feature, flag, or behavior to one stack without making the
422 same change to the other. The two move together.
423- Do not change lockfile format, store layout, `.npmrc` semantics, or CLI
424 surface in only one stack — those are the shared contract and must change
425 in both at once.
426- A dependency that is already declared in `[workspace.dependencies]` in the
427 root `Cargo.toml` may be added to any crate that needs it.
428- Do not add a dependency that is not already declared in the workspace
429 without an explicit human request. If there is a clear benefit and
430 justification for pulling in a new third-party crate, ask the human to
431 approve it and to add it to `[workspace.dependencies]` rather than adding
432 it yourself. Consult `deny.toml` when evaluating candidates.
433- Do not introduce `unsafe` without a clear justification and review.
434- Do not disable lints, tests, or CI checks to make a PR green.
 
 
 
 
 
 
 
 
 
 
 
435 
@@ −1 +1 @@
1−# Agent Guide to pnpm Repository
1+# AGENTS.md (pacquet)
22  
3−This document provides context and instructions for AI agents working on the pnpm codebase.
3+Guidance for AI coding agents working in `pnpm/`.
44  
5−The repository contains three products:
5+**Read [`../AGENTS.md`](../AGENTS.md) first.** It covers the conventions that apply across the whole monorepo — GitHub PR workflow, signing agent-authored content, conventional commit messages, code-reuse philosophy, "never ignore test failures," and the PR-conflict resolution script. This file specializes those rules for pacquet's Rust code and adds pacquet-only ones.
66  
7−- The **TypeScript pnpm CLI** — the main TypeScript workspaces outside `pnpm/` and `pnpr/`.
8−- The **Rust pacquet port** — `pnpm/`. See [`pnpm/AGENTS.md`](./pnpm/AGENTS.md) for pacquet-specific rules; it adds to (and never contradicts) the conventions below.
9−- The **Rust pnpr registry server** — `pnpr/`. See [`pnpr/AGENTS.md`](./pnpr/AGENTS.md) for pnpr-specific rules; it adds to (and never contradicts) the conventions below.
7+## What this project is
108  
11−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.
9+`pacquet` is the [pnpm](https://pnpm.io) CLI implemented in Rust. It is one of
10+two parallel implementations of the same package manager — the other is the
11+TypeScript pnpm CLI (the workspaces outside `pnpm/`). The two are kept
12+behaviorally identical: the same commands, flags, defaults, error codes, file
13+formats, lockfile shape, and directory layout. pacquet is not a downstream port
14+that trails the TypeScript CLI; it is a source of truth in its own right, at
15+near-complete feature parity, and the two stacks are developed together.
1216  
13−## Keep pnpm and pacquet in sync
17+## The cardinal rule
1418  
15−The two stacks are parallel implementations of the same CLI, kept behaviorally identical — the same flags, defaults, error codes, file formats, and lockfile shape. They are now at near-complete feature parity and are developed together, so **any user-visible change has to land in both at the same time.** Neither stack is downstream of the other: pacquet is a source of truth in its own right, not a port that trails the TypeScript CLI.
19+**pacquet and the TypeScript pnpm CLI must stay behaviorally identical.**
20+They are parallel implementations of one package manager, developed together at
21+near-complete feature parity. Any user-visible change — a command, flag,
22+default, error code or message, lockfile/manifest/state-file format, log
23+emission parsed by `@pnpm/cli.default-reporter`, store layout, or hook
24+semantic — must land in both stacks at the same time. The repo-wide statement
25+of this obligation lives in
26+[`../AGENTS.md`](../AGENTS.md#keep-pnpm-and-pacquet-in-sync); this section is the
27+pacquet-side detail.
1628  
17−When you change one side, do the equivalent change on the other in the same PR if you can. If you can't (different expertise, scope too large, or pacquet hasn't ported the surrounding feature yet), open the PR with just your side — call out in the description what still needs porting, and someone else will push the matching commits to the same PR before it lands.
29+Neither stack is downstream of the other. You are not "porting from" the
30+TypeScript code: when you implement or change behavior in pacquet, make the
31+equivalent change in the TypeScript workspaces in the same PR, and vice versa.
32+If you genuinely can't (different expertise, scope too large, or the other
33+stack hasn't grown the surrounding feature yet), ship your side and say so in
34+the PR description so the matching commits can follow before it lands.
1835  
19−"User-visible" means anything that affects the CLI surface or the on-disk contract: command-line flags and defaults, environment-variable handling, lockfile/manifest/state-file format, error codes and messages, log emissions parsed by `@pnpm/cli.default-reporter`, store layout, hook semantics. Pure internal refactors, perf wins, and TS-only test cleanups don't need mirroring.
36+Working rules:
2037  
21−**Any user-visible change to either stack must be replicated in the other.**
38+1. **Keep the two implementations in agreement.** When you touch behavior in
39+ pacquet, find the counterpart in the TypeScript workspaces — they live at
40+ the repo root (`pnpm/` for the CLI entry, `pkg-manager/`, `resolving/`,
41+ `lockfile/`, `store/`, `fetching/`, `config/`, `hooks/`, and so on; see the
42+ [repo-structure section](../AGENTS.md#repository-structure)) — and change it
43+ there too. The two must agree on logic, edge cases, config resolution, error
44+ messages, and file/lockfile formats.
45+2. **Match observable behavior, not structure.** Structural similarity (similar
46+ function decomposition and names) is a convenience for cross-referencing, not
47+ a requirement. What must match is what a user or a downstream tool can
48+ observe.
49+3. **Don't diverge unilaterally.** Do not add a feature, flag, or quirk to one
50+ stack without the other, and do not "fix" a behavior in only one. A genuine
51+ bug present in both is fixed in both.
52+4. **Log emissions are part of behavioral identity.** A function that fires
53+ `pnpm:<channel>` events through the reporter must use the same call site,
54+ payload, and ordering in both stacks so `@pnpm/cli.default-reporter` parses
55+ pacquet's NDJSON the same way it parses the TypeScript CLI's. See
56+ [Reporter / log events](./CODE_STYLE_GUIDE.md#reporter--log-events)
57+ in the style guide for the convention (channel mapping, threading
58+ `R: Reporter`, emit-site placement, recording-fake tests).
59+5. **Prefer real fixtures; reach for the dependency-injection seam
60+ only when they can't cover the branch.** Most happy paths and
61+ error paths should be tested with a `tempfile::TempDir`, the
62+ mocked registry, or an integration test that spawns the actual
63+ binary. Use the DI seam — a capability trait on the `Host`
64+ provider, threaded as `Sys: <Bounds>` — only for branches a real
65+ fixture can't reach portably: filesystem error kinds
66+ (`PermissionDenied`, `ENOSPC`, …), deterministic time, shared
67+ process-global state a test would otherwise mutate
68+ (`env::set_var`, `set_current_dir`, the umask, …), or the
69+ external-service happy paths in features like `pnpm login` (2FA)
70+ and `pnpm publish` (OIDC / provenance) when those land. See
71+ [Dependency injection for tests](./CODE_STYLE_GUIDE.md#dependency-injection-for-tests)
72+ in the style guide for the gating rule, the names (`Sys`, `Host`,
73+ `Fs*`, `Clock`, `EnvVar`, …), the eight principles, and the
74+ `modules-yaml` worked example.
2275  
23−The pacquet-side conventions for keeping the two stacks aligned are in [`pnpm/AGENTS.md`](./pnpm/AGENTS.md#the-cardinal-rule).
76+If the intended behavior is unclear or looks wrong, stop and ask the user
77+rather than guessing.
2478  
25−## Repository Structure
79+## Modeling branded string types
2680  
27−The pnpm codebase is a monorepo managed by pnpm itself. The root contains functional directories organized by domain:
81+TypeScript pnpm leans on *branded* string types. A branded string is a
82+plain string narrowed by a phantom property (for example,
83+`type PkgName = string & { __brand: 'PkgName' }`), so the type system can
84+track intent that the runtime cannot see. Some brands are stamped through
85+a validating constructor. Others are minted with a bare `as` type assertion and
86+have no runtime check at all. Both stacks must preserve that distinction,
87+because it is part of the public contract pnpm exposes through manifest,
88+lockfile, state, and config files. The TypeScript brand and the Rust newtype
89+must agree on validation policy.
2890  
29−### Core Directories
91+Rules for a Rust newtype standing in for a branded string type ("the
92+TypeScript side" below is its TypeScript counterpart):
3093  
31−- `pnpm/`: The CLI entry point and main package.
32−- `pkg-manager/`: Core package management logic (installation, linking, etc.).
33−- `resolving/`: Dependency resolution logic (resolvers for npm, tarballs, git, etc.).
34−- `fetching/`: Package fetching logic.
35−- `store/`: Store management logic (content-addressable storage).
36−- `lockfile/`: Lockfile handling, parsing, and utilities.
94+1. **Declare a newtype wrapper.** Do not collapse the brand into a plain
95+ `String` or `&str`. Give the type its own struct so misuse is a type
96+ error in pacquet too.
97+2. **If upstream always validates before construction, validate too.**
98+ When every brand site in pnpm runs through a checking factory, pacquet's
99+ wrapper must construct only via `TryFrom<String>` and/or `FromStr`. Do
100+ not provide an infallible public constructor that takes an arbitrary
101+ string.
102+3. **If upstream never validates, just brand for type-safety.** Some
103+ upstream brands exist purely to keep the type system from confusing
104+ one string slot with another. For example, a brand may exist to prevent
105+ a `PkgId` from being passed where a `PkgName` is expected, even though
106+ the value is never validated at runtime. In that case the Rust wrapper
107+ should expose an infallible `From<String>` (and `From<&str>` when
108+ convenient). The type-safety win is the whole point, and no validator
109+ is needed.
110+4. **If upstream occasionally constructs without validation, expose
111+ `from_str_unchecked`.** When pnpm sometimes mints the brand via a bare
112+ `as` assertion, skipping its validator, add a `from_str_unchecked` (or
113+ similarly named) constructor on the Rust side so callers can opt into
114+ the same unchecked path explicitly. Keep the validating constructor as
115+ well. `from_str_unchecked` is the escape hatch, not the default.
116+5. **Match upstream serde behavior.** If the branded type crosses a
117+ JSON, YAML, or INI boundary (manifest files, lockfiles, state files,
118+ config files, and similar), wire the wrapper into serde so the
119+ validation policy survives serialization:
120+ - `#[serde(try_from = "String")]` for deserialization, so
121+ deserialized values go through the validator.
122+ - `#[serde(into = "String")]` for serialization.
123+ Use both when the type is round-tripped.
124+6. **Derive simple conversions with `derive_more`.** When the conversion
125+ impls implied by the rules above are mechanical (a one-liner that
126+ wraps or unwraps the inner field), use `#[derive(derive_more::From)]`
127+ and `#[derive(derive_more::Into)]` rather than handwriting an `impl`
128+ block. Fall back to a manual `impl` only when the conversion needs
129+ custom logic, such as validation or normalization. `derive_more` is
130+ already a workspace dependency.
131+7. **String-literal unions become `enum`s.** If upstream uses a string
132+ literal type or a union of string literals (for example,
133+ `'auto' | 'always' | 'never'`), model it as a Rust `enum`, not a
134+ newtype wrapper. The set of valid values is closed, so encode that.
135+8. **Template literal types are branded strings.** If upstream uses a
136+ string template literal type (for example,
137+ ``` `${string}@${string}` ```), treat it the same as a branded string
138+ type. Use a newtype wrapper with the validation discipline from rules
139+ 2 through 5 above.
37140  
38−### CLI & Configuration
141+## Follow the project guides
39142  
40−- `cli/`: CLI command implementations and infrastructure.
41−- `config/`: Configuration management and parsing.
42−- `hooks/`: pnpm hooks (readPackage, etc.).
43−- `completion/`: Shell completion support.
143+1. Follow the contributing guide in [`CONTRIBUTING.md`](./CONTRIBUTING.md), and **ALWAYS** double-check before committing. It covers commit message format, writing style, setup, and the automated checks to run before committing.
144+2. Follow the code style guide in [`CODE_STYLE_GUIDE.md`](./CODE_STYLE_GUIDE.md), and **ALWAYS** double-check before committing. It covers code-level conventions not enforced by tooling: imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, `pipe-trait`, error handling, test layout, logging during tests, and cloning of `Arc` and `Rc`.
44145  
45−### Other Functional Directories
146+## Repo layout (inside `pnpm/`)
46147  
47−- `network/`: Network-related utilities (proxy, fetch, auth).
48−- `workspace/`: Workspace-related utilities.
49−- `exec/`: Execution-related commands (run, exec, dlx).
50−- `env/`: Node.js environment management.
51−- `cache/`: Cache-related commands and utilities.
52−- `patching/`: Package patching functionality.
53−- `reviewing/`: License and dependency review tools.
54−- `releasing/`: Release and publishing utilities.
148+- `crates/` — library and binary crates that make up pacquet.
149+ - `cli`, `package-manager`, `package-manifest`, `lockfile`, `store-dir`,
150+ `tarball`, `registry`, `network`, `npmrc`, `fs`, `executor`,
151+ `diagnostics`, `testing-utils`.
152+- `tasks/` — developer tooling: `integrated-benchmark`, `micro-benchmark`,
153+ `registry-mock`.
154+- `CONTRIBUTING.md` — commit-message format, writing style, setup, and the
155+ automated checks to run before submitting. Read it before submitting code.
156+- `CODE_STYLE_GUIDE.md` — manual code-style conventions beyond what `cargo
157+ fmt`, `taplo`, and clippy enforce: imports, modules, naming, ownership
158+ and borrowing, trait bounds, pattern matching, `pipe-trait`, error
159+ handling, test layout, and `Arc`/`Rc` cloning. Read it before submitting
160+ code.
55161  
56−### Shared Utilities
162+The Rust workspace (`Cargo.toml`, `Cargo.lock`, `rust-toolchain.toml`,
163+`justfile`, `.cargo/`, `.taplo.toml`, etc.) lives at the **repo root**, not
164+inside `pnpm/`. Run `cargo` and `just` from the repo root.
57165  
58−- `packages/`: Shared utility packages (constants, error handling, logger, types, etc.).
59−- `fs/`: Filesystem utilities.
60−- `crypto/`: Cryptographic utilities.
61−- `text/`: Text processing utilities.
166+## Commands
62167  
63−### Rust Projects
168+Prefer `just` recipes when one fits; drop down to `cargo` / `taplo` / etc.
169+directly when you need flags the recipe doesn't expose (e.g. filtering tests
170+by crate or name — see below).
64171  
65−- `pnpm/`: The pnpm CLI ported to Rust. Self-contained sub-project with its own crates, tests, and tooling — see [`pnpm/AGENTS.md`](./pnpm/AGENTS.md).
66−- `pnpr/`: The pnpm-compatible npm registry server. Self-contained sub-project with its own crates, tests, and tooling — see [`pnpr/AGENTS.md`](./pnpr/AGENTS.md).
172+- `just ready` — run the same checks CI runs (typos, fmt, check, test, lint).
173+ Run this before declaring a task complete.
174+- `just test` — `cargo nextest run`.
175+- `just lint` — `cargo clippy --locked --workspace --all-targets -- --deny warnings`.
176+- `just check` — `cargo check --locked --workspace --all-targets`.
177+- `just fmt` — `cargo fmt` + `taplo format`.
178+- `just cli -- <args>` — run the pacquet binary.
179+- `just registry-mock <args>` — manage the mock registry used by tests.
180+- `just integrated-benchmark <args>` — compare revisions or compare against
181+ pnpm itself (see `CONTRIBUTING.md`).
67182  
68−## Setup & Build (TypeScript only)
183+Warnings are errors (`--deny warnings` in lint). Do not silence them with
184+`#[allow(...)]` unless there is a specific, justified reason.
69185  
70−To set up the environment and build the project:
186+## Tests
71187  
72−```bash
73−pnpm install
74−pnpm run compile
75−```
188+- Tests live alongside the code they exercise (standard Cargo layout) plus
189+ integration tests under each crate's `tests/`. Shared pacquet fixtures live
190+ under `crates/testing-utils/src/fixtures/`; registry package fixtures live
191+ under `../pnpr/.fixtures/packages/`.
192+- Snapshot tests use `insta`. When an intentional change alters a snapshot,
193+ review the diff carefully, then accept with `cargo insta review`. Never
194+ accept snapshot changes blindly.
195+- Tests that need the mocked registry start `pnpr` through
196+ `pacquet-testing-utils`; `cargo test` / `cargo nextest run` should not
197+ require a separate `just registry-mock launch` step.
198+- When a behavior change spans both stacks, keep their tests in sync — give
199+ pacquet a Rust test for the same scenario the TypeScript stack covers (and
200+ vice versa) whenever it translates. Matching test coverage is the easiest
201+ way to prove behavioral parity.
202+- The active test-porting plan lives in
203+ [`plans/TEST_PORTING.md`](./plans/TEST_PORTING.md). It enumerates the
204+ upstream TypeScript tests scheduled to be ported (with file paths and line
205+ numbers) and the conventions expected of the ports — `known_failures`
206+ modules, `pacquet_testing_utils::allow_known_failure!` at the
207+ not-yet-implemented boundary, and the practice of temporarily breaking the
208+ subject under test to verify the ported test actually catches the
209+ regression. Consult it before adding ported tests, and update its
210+ checkboxes as items land.
211+- When temporarily breaking an implementation (to prove a test catches the
212+ regression, or for any other experiment), revert with `git restore <file>`,
213+ never by moving a saved backup copy into place. Cargo's freshness check is
214+ mtime-based, and an mtime-preserving restore leaves the binary compiled
215+ from the broken source looking fresh — later test runs then fail in
216+ impossible-looking, "flaky" ways with nothing pointing at the stale
217+ artifact. Details in the "Test the tests" section of
218+ [`plans/TEST_PORTING.md`](./plans/TEST_PORTING.md). If test outcomes ever
219+ flip with no code change, `touch` the implementation file and rerun before
220+ debugging anything else.
76221  
77−To compile a specific package:
222+### No "tolerant" tests for missing tools
78223  
79−```bash
80−pnpm --filter <package_name> run compile
81−```
224+Tests must not be tolerant of a missing build / runtime environment by
225+silently `return`-ing early when a tool isn't found. Patterns like:
82226  
83−**Important:** The pnpm CLI e2e tests (in `pnpm/test/`) use the **bundled** `pnpm/dist/pnpm.mjs`, not the individual package `lib/` outputs. After changing any package, you must rebuild the bundle before running e2e tests:
227+```rust
228+fn skip_if_no_git() -> bool {
229+ if std::process::Command::new("git").arg("--version").output().is_err() {
230+ eprintln!("skipping: `git` not on PATH");
231+ return true;
232+ }
233+ false
234+}
84235  
85−```bash
86−pnpm --filter pnpm run compile
236+#[test]
237+fn my_test() {
238+ if skip_if_no_git() {
239+ return;
240+ }
241+ // ...
242+}
87243 ```
88244  
89−This runs `tsgo --build`, linting, and `pnpm run bundle` (which bundles all packages into `pnpm/dist/pnpm.mjs`). Without this step, e2e tests will use a stale bundle and your changes won't be tested.
245+are forbidden. If the test needs a tool, just call into it and let the
246+existing `.unwrap()` / `.expect(...)` panic when the tool is absent — a
247+failing test in an under-provisioned environment is the correct signal.
248+Tolerance defeats the purpose of testing: if the environment really
249+doesn't have the required tools, that's the *environment's* fault and it
250+needs to be fixed.
90251  
91−## Testing (TypeScript only)
252+This applies in particular to `git`, `node`, and `npm` — git is ubiquitous
253+on developer machines, and Node.js is a documented prerequisite for
254+building pnpm. There is no realistic environment in which pacquet's tests
255+should run *and* these tools should be absent.
92256  
93−Never run all tests in the repository as it takes a lot of time.
257+The only marginally acceptable exception is platform-locked tools — APIs
258+or binaries that exist on one OS but not another. Even then, prefer
259+`#[cfg_attr(target_os = "windows", ignore = "...")]` (or the matching
260+`#[cfg(unix)]` gate already used in this crate for `/bin/sh` shims) over a
261+runtime probe-and-skip helper. The gate is visible to `cargo test` and
262+shows up in the test report; a silent `return` does not.
94263  
95−Run tests for a specific project instead:
264+### Running tests narrowly
96265  
97−```bash
98−# From the project directory
99−pnpm test
266+Running the full suite is slow. While iterating, target what you're working
267+on:
100268  
101−# From the root, filtering by package name
102−pnpm --filter <package_name> test
103−```
269+```sh
270+# One crate
271+cargo nextest run -p pacquet-lockfile
104272  
105−Or better yet, run tests for a specific file:
273+# One test by name substring
274+cargo nextest run -p pacquet-lockfile <name_substring>
106275  
107−```bash
108−pnpm --filter <package_name> test <file_path>
276+# One integration test file
277+cargo nextest run -p pacquet-lockfile --test <file_stem>
109278 ```
110279  
111−Or a specific test case in a specific file:
280+Run `just ready` (full suite) before handing the PR off.
112281  
113−```bash
114−pnpm --filter <package_name> test <file_path> -t <test_name_pattern>
115−```
282+## Style
116283  
117−## Linting (TypeScript only)
284+`CODE_STYLE_GUIDE.md` is the source of truth. Highlights:
118285  
119−To run all linting checks:
286+- Choose owned vs. borrowed parameters to minimize copies; widen to the most
287+ encompassing type (`&Path` over `&PathBuf`, `&str` over `&String`) when it
288+ doesn't force extra copies.
289+- Prefer `Arc::clone(&x)` / `Rc::clone(&x)` over `x.clone()` for reference-
290+ counted types, so the cost is visible at the call site.
291+- Follow the test-logging guidance in the style guide — log before non-
292+ `assert_eq!` assertions, `dbg!` complex structures, skip logging for simple
293+ scalar `assert_eq!`.
294+- Follow [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/naming.html)
295+ for naming.
296+- **No star imports inside module bodies.** Write `use super::{Foo, bar}`
297+ instead of `use super::*;`, and the same for any other glob whose
298+ target is a module you control. Two forms stay allowed: external-crate
299+ preludes such as `use rayon::prelude::*;` and root-of-module
300+ re-exports such as `pub use submodule::*;` in a `lib.rs`. See the
301+ "No star imports" section in `CODE_STYLE_GUIDE.md`.
120302  
121−```bash
122−pnpm run lint
123−```
303+### Comments
124304  
125−## Never ignore test failures
305+Same baseline as [`../AGENTS.md`](../AGENTS.md#comments): write code that explains itself; comments are for the non-obvious *why*, not a translation of the *what*.
126306  
127−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.
307+Rust-specific additions:
128308  
129−## AI Review Guidance
309+- **Doc comments (`///`, `//!`) are rustdoc-visible API documentation.** Use them for item contracts. Put implementation-only rationale in regular `//` comments.
310+- **Tests are documentation. Do not duplicate them in prose.** If a behavioral scenario, edge case, failure mode, or worked example is already captured by a test (its name, its setup, its assertions), do not also narrate it in the doc comment on the implementation. The doc comment should state the contract once; the test demonstrates the behavior. The same applies in reverse: a test's own doc comment should not re-explain what the asserts already say, only the *why* if it is not obvious.
311+- **`// SAFETY:`, `// TODO:`, and similar prefixes are the exception.** They signal hidden invariants or known follow-ups that a reader cannot recover from the code alone.
130312  
131−The repository's review framework lives in **[REVIEW_GUIDE.md](./REVIEW_GUIDE.md)** — how changes are accepted or rejected, the security-first / performance-second priorities, the security checklist and advisory regression themes, and the test/changeset/parity 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.)
313+Prefer renaming, restructuring, or extracting a helper over leaving a comment. Reach for prose only when the right names and types genuinely cannot carry the information.
132314  
133−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.
315+### Preserve existing method chains
134316  
135−## Code Reuse and Avoiding Duplication
317+When editing existing code, do not break a method chain (including `pipe-trait`
318+`.pipe(...)` chains) into intermediate `let` bindings unless you can justify
319+the rewrite. Valid justifications include a chain that fails to compile after
320+your edit, a borrow checker rejection, a meaningful performance win from
321+splitting it up, or any other concrete reason the chain cannot stay as it is.
322+Refactoring for style alone is not a justification when the task is something
323+else. Keep the surrounding code shape intact and confine your edits to what
324+the task asks for.
136325  
137−**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.
326+When the change you need can fit inside the existing chain, keep it there.
327+For example, swapping a `PathBuf::from` allocation for a `Path::new` borrow:
138328  
139−- **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 `packages/`, `fs/`, `crypto/`, `text/`, and other shared directories first.
140−- **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.
141−- **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.
142−- **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.
143− 
144−## Commit Messages
145− 
146−Follow the [Conventional Commits](https://www.conventionalcommits.org/) specification.
147− 
148−- `feat`: a new feature
149−- `fix`: a bug fix
150−- `docs`: documentation only changes
151−- `style`: formatting, missing semi-colons, etc.
152−- `refactor`: code change that neither fixes a bug nor adds a feature
153−- `perf`: a code change that improves performance
154−- `test`: adding missing tests
155−- `chore`: changes to build process or auxiliary tools
156− 
157−### Install the git hooks before committing
158− 
159−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:
160− 
161−```bash
162−pnpm install # runs the "prepare": "husky" script as part of install
163−# or, if dependencies are already installed, register the hooks on their own:
164−pnpm exec husky
329+```diff
330+ output
331+ .stdout
332+ .pipe(String::from_utf8)
333+ .expect("convert stdout to UTF-8")
334+ .trim_end()
335+- .pipe(PathBuf::from)
336++ .pipe(Path::new)
337+ .parent()
338+ .expect("parent of root manifest")
339+ .to_path_buf()
165340 ```
166341  
167−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.
342+Do not flatten the chain just because you happen to be editing nearby:
168343  
169−### Never use bare `#NNN` issue/PR references
170− 
171−**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.
172− 
173−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 two forms:
174− 
175−- Using `#1`, `#2`, `#3`, … 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.
176−- 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`.
177− 
178−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.
179− 
180−**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.
181− 
182−### Never use a bare `@mention`
183− 
184−**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.
185− 
186−GitHub turns any `@name` into a mention of that user/org/team, which is wrong either way it is meant:
187− 
188−- If it is code (a scoped package like `@pnpm/core`, a handle, a path), GitHub should not treat it as a mention.
189−- If it really is a person, every push, force-push, and rebase that carries the commit re-notifies them — noise nobody asked for.
190− 
191−**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.
192− 
193−## Changesets
194− 
195−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.
196− 
197−**IMPORTANT: Always explicitly include `"pnpm"` in the changeset** with the appropriate version bump (patch, minor, or major). The pnpm CLI will only receive automatic patch bumps from its dependencies, so if your change warrants a minor or major version bump for the CLI, you must specify it explicitly. The changeset description will appear on the release notes page.
198− 
199−Example:
200− 
344+```diff
345+-output
346+- .stdout
347+- .pipe(String::from_utf8)
348+- .expect("convert stdout to UTF-8")
349+- .trim_end()
350+- .pipe(PathBuf::from)
351+- .parent()
352+- .expect("parent of root manifest")
353+- .to_path_buf()
354++let stdout = String::from_utf8(output.stdout).expect("convert stdout to UTF-8");
355++Path::new(stdout.trim_end()).parent().expect("parent of root manifest").to_path_buf()
201356 ```
202−---
203−"@pnpm/installing.deps-installer": minor
204−"pnpm": minor
205−---
206357  
207−Added a new setting `blockExoticSubdeps` that prevents the resolution of exotic protocols in transitive dependencies [#10352](https://github.com/pnpm/pnpm/issues/10352).
208−```
358+If you do need to break a chain, state the justification in your reply, the
359+commit message, or the PR description so a reviewer can confirm the rewrite
360+was warranted. If the rewrite is purely stylistic, raise it with the user as
361+its own change rather than including it in an unrelated edit.
209362  
210−**Versioning Guidelines for pnpm CLI:**
211−- **patch**: Bug fixes, internal refactors, and changes that don't require documentation updates
212−- **minor**: New features, settings, or commands that should be documented (anything users should know about)
213−- **major**: Breaking changes
363+## Code reuse (pacquet specifics)
214364  
215−### Changesets for the Rust products
365+The general "search before you write / extract shared code / prefer mature
366+crates / keep deps at the right level" rules from
367+[`../AGENTS.md`](../AGENTS.md#code-reuse-and-avoiding-duplication) apply.
368+Pacquet-specific notes:
216369  
217−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, targeting:
370+- Shared helpers tend to live in `crates/fs`, `crates/testing-utils`, and
371+ `crates/diagnostics` — check there first.
372+- Check whether the workspace already depends on something suitable (see
373+ `[workspace.dependencies]` in the root `Cargo.toml`) before adding a new
374+ dependency.
375+- **Keep dependencies at the right level.** Add a new dependency to the
376+ specific crate that needs it, not to the workspace root or to a shared
377+ crate unless multiple crates actually depend on it.
218378  
219−- `pacquet` — the Rust pnpm CLI (published to npm as `pnpm` and `@pnpm/exe` under its `next-<major>` dist-tag; 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.
220−- `@pnpm/napi` — the Node.js addon bindings for the Rust engine.
221−- `@pnpm/pnpr` — the pnpr registry server (published as `@pnpm/pnpr` and its platform packages, plus the `ghcr.io/pnpm/pnpr` Docker image).
379+## Errors and diagnostics
222380  
223−The Rust products release on `alpha` lanes (`versioning.lanes` in `pnpm-workspace.yaml`): each run of `pnpm version -r` that consumes an intent for one of them cuts an `X.Y.Z-alpha.N` prerelease, while the TypeScript CLI keeps releasing stable versions on the main lane. `pnpm lane main --filter …` graduates a product to a stable version.
381+User-facing errors go through `miette` via the `pacquet-diagnostics` crate.
382+Match pnpm's error codes and messages where pnpm defines them — error codes
383+are part of the public contract, not implementation detail. See
384+<https://pnpm.io/errors> for the canonical list.
224385  
225−Do not add `"pnpm"` to a Rust-only changeset: in changesets, `pnpm` always means the TypeScript CLI package. A changeset whose implementation is Rust-only and targets `pacquet` must omit `"pnpm"`. A parity change that lands in both stacks carries one changeset naming both the affected TypeScript packages (plus `"pnpm"`) and the Rust wrapper(s).
386+## Commit and PR hygiene
226387  
227−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.
388+- Keep commits focused. A bug fix commit should not also refactor or
389+ reformat unrelated code.
390+- When a change has a counterpart in the TypeScript pnpm CLI, land both
391+ together; if they must be split, cross-reference the matching PR so a
392+ reviewer can confirm the two stacks stay in sync.
393+- Run `just ready` before pushing.
394+- The repo-wide husky `pre-push` hook runs `pnpm/scripts/pre-push-rust.sh`,
395+ which checks `rustfmt`, `taplo`, `cargo clippy` (with `--all-targets -D
396+ warnings`), `cargo doc` (with `RUSTDOCFLAGS=-D warnings`), and `cargo
397+ dylint`. Make sure your environment
398+ can run cargo (the hook needs it) before pushing; `cargo-dylint` is
399+ detected at runtime and skipped with a warning if not installed.
228400  
229−## Comments
401+### Commit messages
230402  
231−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.
403+Conventional Commits applies (see
404+[`../AGENTS.md`](../AGENTS.md#commit-messages) for the full type list). Use
405+a scope that names the crate or area being touched, matching the existing
406+history (`git log --oneline` for examples). Pacquet adds one type beyond the
407+standard list:
232408  
233−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.
409+- `bench`: benchmark-only changes.
234410  
235−Defaults:
411+Examples (from this repo's history):
236412  
237−- **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.
238−- **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.
239−- **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.
240−- **Documentation comments are for the item's contract** — preconditions, postconditions, edge cases, why the item exists. Not for re-narrating the body.
241−- **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.
242− 
243−Write a comment only when:
244− 
245−- 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).
246−- The right name doesn't fit — e.g., a temporary technical constraint that's worth flagging but doesn't justify a new symbol.
247− 
248−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."
249− 
250−## Code Style (TypeScript only)
251− 
252−This repository uses [Standard Style](https://github.com/standard/standard) with a few modifications:
253−- **Trailing commas** are used.
254−- **Functions are preferred** over classes.
255−- **Functions are declared after they are used** (hoisting is relied upon).
256−- **Functions should have no more than two or three arguments.** If a function needs more parameters, use a single options object instead.
257−- **Import Order**:
258− 1. Standard libraries (e.g., `fs`, `path`).
259− 2. External dependencies (sorted alphabetically).
260− 3. Relative imports.
261− 
262−To ensure your code adheres to the style guide, run:
263− 
264−```bash
265−pnpm run lint
266413 ```
267− 
268−### Conventions
269− 
270−Recurring engineering conventions in this codebase — the rules reviewers most often enforce:
271− 
272−- **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.
273−- **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.
274−- **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.
275−- **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`).
276−- **Dependency placement.** Shared infrastructure (the logger, etc.) is a peer dependency. (The narrowest-package rule is covered under "Code Reuse and Avoiding Duplication" above.)
277−- **Config and layering.** Configurable values flow through `@pnpm/config` 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.
278−- **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.
279− 
280−## Common Gotchas
281− 
282−### Error Type Checking in Jest (TypeScript only)
283− 
284−When checking if a caught error is an `Error` object, **do not use `instanceof Error`**. Jest runs tests in a VM context where `instanceof` checks can fail across realms.
285− 
286−Instead, use `util.types.isNativeError()`:
287− 
288−```typescript
289−import util from 'util'
290− 
291−try {
292− // ... some operation
293−} catch (err: unknown) {
294− // ❌ Wrong - may fail in Jest
295− if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
296− return null
297− }
298−
299− // ✅ Correct - works across realms
300− if (util.types.isNativeError(err) && 'code' in err && err.code === 'ENOENT') {
301− return null
302− }
303− throw err
304−}
414+fix(network): set explicit timeouts on default reqwest client
415+feat(lockfile): support npm-alias dependencies in snapshots
416+perf(store-dir): share one read-only StoreIndex across cache lookups
305417 ```
306418  
307−## Working with GitHub PRs, Issues, and Comments
419+## Things not to do
308420  
309−- **Open every PR with the repository template.** `gh pr create` does not apply `.github/pull_request_template.md` automatically, so read that file and pass its filled-in contents as the PR body (`--body`/`--body-file`). Keep every section (Summary, Squash Commit Body, Checklist), fill them in for this change, mark the checklist items, and remove only the lines the template says are inapplicable.
310−- **Keep PR titles and descriptions current.** When pushing new changes to a PR, review the title and description and update them if they no longer accurately reflect what the PR does.
311−- **Reply to and resolve review conversations.** Once a review comment has been addressed, reply to the thread with a description of the resolution including the commit hash that fixed it, then mark the conversation as resolved.
312−- **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:
313− 
314− ```markdown
315− ---
316− Written by an agent (Claude Code, claude-opus-4-7).
317− ```
318− 
319−## Resolving Conflicts in GitHub PRs
320− 
321−Use `shell/resolve-pr-conflicts.sh` to resolve PR conflicts:
322− 
323−```bash
324−./shell/resolve-pr-conflicts.sh <PR_NUMBER>
325−```
326− 
327−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.
328− 
329−## Key Configuration Files
330− 
331−- `pnpm-workspace.yaml`: Defines the workspace structure.
332−- `package.json` (root): Root scripts and devDependencies.
333−- `CONTRIBUTING.md`: Detailed contribution guidelines.
421+- Do not add a feature, flag, or behavior to one stack without making the
422+ same change to the other. The two move together.
423+- Do not change lockfile format, store layout, `.npmrc` semantics, or CLI
424+ surface in only one stack — those are the shared contract and must change
425+ in both at once.
426+- A dependency that is already declared in `[workspace.dependencies]` in the
427+ root `Cargo.toml` may be added to any crate that needs it.
428+- Do not add a dependency that is not already declared in the workspace
429+ without an explicit human request. If there is a clear benefit and
430+ justification for pulling in a new third-party crate, ask the human to
431+ approve it and to add it to `[workspace.dependencies]` rather than adding
432+ it yourself. Consult `deny.toml` when evaluating candidates.
433+- Do not introduce `unsafe` without a clear justification and review.
434+- Do not disable lints, tests, or CI checks to make a PR green.
334435  
RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack

RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack

RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack