Skip to content

feat(installing): delegate resolution to pacquet >= 0.11.7 when configured#12210

Merged
zkochan merged 9 commits into
mainfrom
pq-run-for-all
Jun 14, 2026
Merged

feat(installing): delegate resolution to pacquet >= 0.11.7 when configured#12210
zkochan merged 9 commits into
mainfrom
pq-run-for-all

Conversation

@zkochan

@zkochan zkochan commented Jun 5, 2026

Copy link
Copy Markdown
Member

What

When pacquet is declared in configDependencies, pnpm previously always ran it with --frozen-lockfile: pnpm resolved the dependency graph (in JS, writing pnpm-lock.yaml) and pacquet only fetched / imported / linked. This PR lets pacquet do the resolution too — when the installed pacquet is new enough to support full resolving installs (>= 0.11.7).

With a resolution-capable pacquet, a non-frozen plain pnpm install on the default isolated nodeLinker is delegated end-to-end in a single non-frozen pass: pacquet resolves the manifests, writes pnpm-lock.yaml, and materializes node_modules. Older pacquet keeps the existing resolve-then-materialize split, and add / update / remove still resolve in pnpm (it must rewrite the manifests first).

How

  • installing/commands/src/runPacquet.tsmakeRunPacquet now returns a PacquetEngine { supportsResolution, run }. supportsResolution is detected from the pacquet version in node_modules/.pnpm-config/<name>/package.json (gated >= 0.11.7; prereleases like 0.11.7-rc.1 count; unreadable version → false, degrading to the safe materialize-only path). run gained a resolve option that omits the injected --frozen-lockfile / --ignore-manifest-check.
  • installing/deps-installer/src/install/index.ts — the isolated-linker non-frozen path now delegates the whole install via run({ resolve: true }) for plain installs when pacquet supports resolution, returning a minimal InstallFunctionResult (mirroring the frozen delegation). The redundant verifyLockfileResolutions pass is skipped when pacquet will re-resolve (it applies the resolver policy during fresh resolution itself).
  • configDependencies env document preservationconfigDependencies are recorded in a YAML document prepended to pnpm-lock.yaml, a pnpm-only concept pacquet doesn't model. Since pacquet rewrites pnpm-lock.yaml on the resolve path, pnpm now captures that env document before delegating and restores it afterwards; otherwise it was dropped and the next --frozen-lockfile install failed its config-deps freshness gate.
  • Call sites updated from opts.runPacquet(...) to opts.runPacquet.run(...); engine type threaded through extendInstallOptions.ts and recursive.ts.

Tests

  • installing/commands/test/runPacquet.ts — hermetic unit tests for the version gate.
  • pnpm/test/install/pacquet.ts — e2e pin bumped to the released 0.11.7 (published under both pacquet and @pnpm/pacquet). Added coverage for the resolve path, the materialize-only fallback (pinned to 0.11.6), and the scoped alias; un-skipped the add / update tests (pacquet 0.11.7 now writes a compatible .modules.yaml) and fixed the update test (is-positive has no v4 — it updates from 1.0.0). All 7 pass against the live registry.

Scope

Resolution delegation is currently scoped to the default isolated linker + plain install; hoisted-linker and add/update/remove continue to resolve in pnpm. This can be widened later.


Written by an agent (Claude Code, claude-opus-4-8).
Updated by an agent (Codex, GPT-5).

Summary by CodeRabbit

  • New Features
    • Opt-in preview: when pacquet is configured and its version is ≥ 0.11.7, non-frozen installs can delegate dependency resolution and materialization to pacquet in a single pass.
    • Backward compatible: pacquet < 0.11.7 keeps the prior resolve-then-materialize behavior.
  • Refactor
    • Install flow now switches based on pacquet resolution capability and preserves pnpm’s env lockfile during delegation.
  • Bug Fixes
    • Ensures lockfile check mode is not delegated to pacquet when it shouldn’t be.
  • Tests
    • Expanded pacquet install/update coverage with version-specific assertions.
  • Chores
    • Bumped relevant package versions via a changeset.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR extends pnpm's pacquet integration to delegate dependency resolution to pacquet >= 0.11.7 during non-frozen installs. It introduces a PacquetEngine with version-based capability detection, a resolve-mode invocation that omits lockfile-enforcing flags, single-pass delegation with env-lockfile capture/restore, compatibility fallbacks to the legacy two-phase flow, unit and integration tests, and a changeset note.

Changes

Pacquet delegation with resolution capability

Layer / File(s) Summary
Engine contract propagation
installing/commands/src/recursive.ts, installing/deps-installer/src/install/extendInstallOptions.ts
Change runPacquet from a callable to an engine object { supportsResolution, run(opts) } and update related option types and parameter typings.
Engine implementation and CLI arg modes
installing/commands/src/runPacquet.ts
Refactor makeRunPacquet to return PacquetEngine; add RunPacquetCallOpts.resolve; conditionally omit --frozen-lockfile/--ignore-manifest-check when resolve:true; add helpers to read installed pacquet package.json and determine supportsResolution from the version.
Installer delegation gating and execution paths
installing/deps-installer/src/install/index.ts
Expand delegation predicate to consider supportsResolution and nodeLinker; switch call sites to opts.runPacquet.run(...); implement single-pass run({ resolve: true }) for non-frozen install-only flows with readEnvLockfile/writeEnvLockfile around pacquet rewrites; add pacquetResolveResult; preserve legacy resolve-then-materialize fallback.
Unit tests for capability detection
installing/commands/test/runPacquet.ts
Add Jest tests that create pacquet config fixtures and assert supportsResolution across supported/unsupported versions, missing/invalid metadata, absent config dir, and scoped alias.
Integration tests and release notes
pnpm/test/install/pacquet.ts, .changeset/pacquet-resolving-install-delegation.md
Bump test pacquet version constants to 0.11.7, parameterize prepareWithPacquet to accept versions, add tests for resolve-vs-materialize behavior split by pacquet version, unskip pnpm update test, and add a changeset documenting delegation behavior for pacquet >= 0.11.7.
Lockfile check gating test
installing/deps-installer/test/install/dedupe.ts
Add tests asserting that lockfileCheck mode and dedupe do not delegate to pacquet; verify that pacquet's resolved lockfile is used for post-install checks when run({ resolve: true }) is invoked.

Sequence Diagram

sequenceDiagram
  participant install as pnpm install
  participant willDelegate as willDelegateToPacquet
  participant pacquetEngine as PacquetEngine
  participant lockfile as Lockfile FS
  
  install->>willDelegate: Check if delegation enabled
  willDelegate->>willDelegate: Check supportsResolution, nodeLinker, lockfileCheck
  
  alt Single-pass resolve + materialize (pacquet >= 0.11.7)
    install->>lockfile: readEnvLockfile()
    install->>pacquetEngine: run({ resolve: true })
    pacquetEngine->>lockfile: Write resolved pnpm-lock.yaml
    install->>lockfile: writeEnvLockfile(saved config)
  else Resolve in pnpm, then delegate materialize
    install->>install: _installInContext(lockfileOnly: true)
    install->>pacquetEngine: run({ filterResolvedProgress: true })
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • pnpm/pnpm#11811: Related changes around injecting --ignore-manifest-check and pacquet manifest freshness handling.
  • pnpm/pnpm#11781: Prior adjustments to pacquet argv forwarding and invocation behavior at the same code surface.
  • pnpm/pnpm#11734: Baseline frozen-install pacquet delegation that this PR extends to resolution delegation.

I'm a rabbit in a code-filled glen,
pacquet hops forward, resolving again.
Versions align, one pass takes flight,
older friends still rest in the night.
Hooray for tidy installs — nibble and delight! 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: delegating dependency resolution to pacquet when version >= 0.11.7 is configured, which aligns with the primary objective of the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pq-run-for-all

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@zkochan zkochan marked this pull request as ready for review June 5, 2026 08:54
@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

@gvago

gvago commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

/agentic_review

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 8, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (7) 📘 Rule violations (0)

Grey Divider


Action required

1. Branch lockfile reloaded 🐞 Bug ≡ Correctness
Description
After delegating a resolving install to pacquet, the code reloads the wanted lockfile via
readWantedLockfile() while still honoring useGitBranchLockfile/mergeGitBranchLockfiles. In
git-branch-lockfile mode this can reload an existing branch lockfile instead of the lockfile pacquet
just wrote, leaving ctx.wantedLockfile inconsistent with the materialized node_modules.
Code

installing/deps-installer/src/install/index.ts[R2120-2125]

+        const wantedLockfile = await readWantedLockfile(ctx.lockfileDir, {
+          ignoreIncompatible: opts.force || opts.ci === true,
+          mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
+          useGitBranchLockfile: opts.useGitBranchLockfile,
+          wantedVersions: [LOCKFILE_VERSION],
+        })
Evidence
The pacquet resolve-delegation path reloads the wanted lockfile with git-branch-lockfile options
enabled; but the lockfile reader prefers the branch lockfile name first when configured, so pnpm can
reload a different file than the one pacquet wrote, causing state mismatch.

installing/deps-installer/src/install/index.ts[2082-2131]
lockfile/fs/src/read.ts[212-246]
lockfile/fs/src/lockfileName.ts[9-17]
installing/commands/src/runPacquet.ts[142-164]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The pacquet resolving-install path (`run({ resolve: true })`) refreshes `ctx.wantedLockfile` by calling `readWantedLockfile()` with `useGitBranchLockfile`/`mergeGitBranchLockfiles` options. In git-branch-lockfile mode, `readWantedLockfile()` prefers an existing branch lockfile over `pnpm-lock.yaml`, so pnpm may re-read a different file than the one pacquet just produced, leaving `ctx.wantedLockfile` stale/out-of-sync.
### Issue Context
- `readWantedLockfile()` tries the git-branch lockfile name first when `useGitBranchLockfile` is enabled.
- pacquet is spawned with `install` and no lockfile-name override, so pnpm must ensure it reloads the same file pacquet wrote.
### Fix Focus Areas
- installing/deps-installer/src/install/index.ts[2082-2131]
- installing/deps-installer/src/install/index.ts[391-411]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. No-lockfile pacquet failure 🐞 Bug ≡ Correctness
Description
installInContext delegates a resolving install to pacquet and then unconditionally reads
pnpm-lock.yaml, throwing if it is missing, but this branch does not check opts.useLockfile
("--no-lockfile"). This can either violate the no-lockfile contract (by creating a lockfile) or fail
the install with PACQUET_LOCKFILE_READ_FAILED if pacquet honors --no-lockfile and does not write
one.
Code

installing/deps-installer/src/install/index.ts[R2081-2127]

+    if (opts.runPacquet != null && !opts.lockfileOnly && opts.lockfileCheck == null && opts.enableModulesDir) {
+      // pacquet >= 0.11.7 resolves itself: hand it the whole install
+      // (resolve + fetch + import + link + build, writing the lockfile)
+      // in a single non-frozen pass. Only for plain installs — `add` /
+      // `update` / `remove` need pnpm to mutate the manifests and
+      // resolve the new specs first (pacquet's `install` reads
+      // package.json from disk, which pnpm hasn't rewritten yet).
+      if (opts.runPacquet.supportsResolution && !opts.frozenLockfile && opts.handleResolutionPolicyViolations == null && allMutationsAreInstalls(projects)) {
+        // `configDependencies` are recorded in a YAML document prepended
+        // to `pnpm-lock.yaml` — purely a pnpm concept that pacquet doesn't
+        // model. Capture it before pacquet rewrites the lockfile and
+        // restore it afterwards (`writeEnvLockfile` re-reads pacquet's main
+        // document and re-prepends the env document), otherwise the next
+        // `--frozen-lockfile` install fails its config-deps freshness gate.
+        // The restore runs even if pacquet fails partway: a non-zero exit can
+        // still leave a rewritten lockfile behind, so the env document must be
+        // put back regardless.
+        const envLockfile = await readEnvLockfile(ctx.lockfileDir)
+        let pacquetError: unknown
+        try {
+          await opts.runPacquet.run({ resolve: true })
+        } catch (err: unknown) {
+          pacquetError = err
+          throw err
+        } finally {
+          if (envLockfile != null) {
+            await writeEnvLockfile(ctx.lockfileDir, envLockfile).catch((restoreErr: Error) => {
+              if (pacquetError == null) {
+                throw restoreErr
+              }
+              logger.warn({
+                error: restoreErr,
+                message: `Failed to restore the configDependencies document in pnpm-lock.yaml: ${restoreErr.message}`,
+                prefix: ctx.lockfileDir,
+              })
+            })
+          }
+        }
+        const wantedLockfile = await readWantedLockfile(ctx.lockfileDir, {
+          ignoreIncompatible: opts.force || opts.ci === true,
+          mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
+          useGitBranchLockfile: opts.useGitBranchLockfile,
+          wantedVersions: [LOCKFILE_VERSION],
+        })
+        if (wantedLockfile == null) {
+          throw new PnpmError('PACQUET_LOCKFILE_READ_FAILED', `pacquet did not write a readable ${WANTED_LOCKFILE}`)
+        }
Evidence
The pacquet resolving branch runs without any opts.useLockfile check and then requires a readable
wanted lockfile, while the installer options explicitly support disabling lockfile generation via
useLockfile (only lockfileOnly forbids it).

installing/deps-installer/src/install/index.ts[2078-2127]
installing/deps-installer/src/install/extendInstallOptions.ts[356-430]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The pacquet resolve-delegation path assumes `pnpm-lock.yaml` exists after `run({ resolve: true })` and throws if it doesn’t. This is incompatible with `useLockfile=false` / `--no-lockfile` installs.
### Issue Context
`extendInstallOptions` supports turning lockfiles off (`useLockfile=false`). In that mode, the installer should not require a wanted lockfile to exist, nor should it error because it’s missing.
### Fix Focus Areas
- installing/deps-installer/src/install/index.ts[2081-2127]
Suggested implementation direction:
- Add an explicit guard to the pacquet resolving-delegation branch so it only runs when lockfiles are enabled and expected to be written, e.g. require `opts.useLockfile && opts.saveLockfile` in the `supportsResolution` condition.
- Alternatively (if you want to support pacquet resolving installs without a lockfile), skip `readWantedLockfile(...)`/`PACQUET_LOCKFILE_READ_FAILED` and return an `InstallFunctionResult` that does not assume a lockfile exists; but this likely requires additional downstream adjustments, so the safe fix is to not delegate in no-lockfile mode.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Policy violations dropped 🐞 Bug ≡ Correctness
Description
In the pacquet resolving-install path, pnpm returns an InstallFunctionResult with
resolutionPolicyViolations hard-coded to [], so policy violations discovered during resolution are
not surfaced to the commands layer. This prevents policy UX/persistence like
minimumReleaseAgeExclude auto-collect (and any future policy-driven workspace-manifest updates),
risking later install failures when pnpm re-validates the lockfile outside the pacquet resolve path.
Code

installing/deps-installer/src/install/index.ts[R1948-1957]

+function pacquetResolveResult (projects: ImporterToUpdate[], ctx: PnpmContext): InstallFunctionResult {
+  return {
+    newLockfile: ctx.wantedLockfile,
+    projects: projects.map((project) => ({
+      manifest: project.originalManifest ?? project.manifest,
+      rootDir: project.rootDir,
+    })),
+    depsRequiringBuild: [],
+    resolutionPolicyViolations: [],
+  }
Evidence
The pacquet resolve path returns pacquetResolveResult(...), which hard-codes an empty
resolutionPolicyViolations array. The install command uses resolutionPolicyViolations to derive
and persist policy updates like minimumReleaseAgeExclude, so this new behavior suppresses those
updates and associated UX.

installing/deps-installer/src/install/index.ts[2069-2127]
installing/deps-installer/src/install/index.ts[1939-1958]
installing/deps-installer/src/install/index.ts[1519-1539]
installing/commands/src/installDeps.ts[407-465]
installing/commands/src/policyHandlers.ts[139-196]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When pacquet performs a resolving install (`runPacquet.run({ resolve: true })`), pnpm returns `pacquetResolveResult(...)`, which currently hard-codes `resolutionPolicyViolations: []`. The commands layer relies on `resolutionPolicyViolations` to drive policy UX and persistence (e.g. minimumReleaseAge strict prompting / auto-persisting `minimumReleaseAgeExclude`), so this path silently suppresses that behavior.
### Issue Context
- The resolving pacquet path returns early from `installInContext` and bypasses pnpm’s normal `resolveDependencies(...)` flow (where `handleResolutionPolicyViolations` is applied and violations are collected).
- Downstream code uses the returned `resolutionPolicyViolations` to update workspace policy state (e.g. write `minimumReleaseAgeExclude` entries).
### Fix Focus Areas
- installing/deps-installer/src/install/index.ts[1939-1957]
- installing/deps-installer/src/install/index.ts[2078-2127]
- installing/deps-installer/src/install/index.ts[1519-1539]
### Suggested fix approach
Pick one of these (A is simplest/most correct wrt strict-mode prompting):
**A) Don’t delegate resolve to pacquet when policy UX/persistence is active**
- In the `supportsResolution && !opts.frozenLockfile && allMutationsAreInstalls(...)` branch, add a guard that disables the `resolve:true` delegation when pnpm policy handling is active (e.g. `opts.handleResolutionPolicyViolations` is set and/or policy-related opts like `minimumReleaseAge` are present).
- Fall back to the existing pnpm resolve-then-materialize flow in that case so pnpm can collect violations and prompt/persist before side effects.
**B) Reconstruct and return violations after pacquet resolves**
- After reading the lockfile written by pacquet, compute `resolutionPolicyViolations` for the new lockfile (using the same verifier chain) and return them instead of `[]`.
- If `opts.handleResolutionPolicyViolations` exists, invoke it with the collected violations (note: strict-mode prompting after pacquet has already mutated state is likely unacceptable, so still consider gating strict mode back to A).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
4. Restore error swallowed 🐞 Bug ☼ Reliability
Description
After delegating a resolving install to pacquet, writeEnvLockfile() restore failures are caught and
only logged, so the command can succeed while leaving pnpm-lock.yaml without the
env/configDependencies document. This can later break --frozen-lockfile installs and also prevents
pacquet identity verification from locating the declared configDependency via readEnvLockfile().
Code

installing/deps-installer/src/install/index.ts[R2099-2106]

+          if (envLockfile != null) {
+            await writeEnvLockfile(ctx.lockfileDir, envLockfile).catch((restoreErr: Error) => {
+              logger.warn({
+                error: restoreErr,
+                message: `Failed to restore the configDependencies document in pnpm-lock.yaml: ${restoreErr.message}`,
+                prefix: ctx.lockfileDir,
+              })
+            })
Evidence
The pacquet resolve path catches and warns on writeEnvLockfile() failure, allowing a successful
install to leave the lockfile without the env document. The env document is required for
configDependencies-related frozen installs, and pacquet verification depends on reading
configDependencies from readEnvLockfile().

installing/deps-installer/src/install/index.ts[2089-2107]
lockfile/fs/src/envLockfile.ts[59-76]
pnpm/test/install/configDeps.ts[23-34]
installing/commands/src/verifyPacquetIdentity.ts[82-92]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`writeEnvLockfile()` restore errors are currently swallowed (warn-only) after a successful pacquet resolving install. This can leave `pnpm-lock.yaml` missing the prepended env/configDependencies document while the install still exits 0, causing subsequent `--frozen-lockfile` installs to fail and preventing pacquet identity verification (which reads configDependencies from the env doc) from working reliably.
### Issue Context
The restore is meant to preserve pnpm’s multi-document lockfile env header across pacquet rewrites. If restore fails, the correct behavior is to fail the command (at least when pacquet itself succeeded), rather than leaving a silently-corrupted lockfile.
### Fix Focus Areas
- installing/deps-installer/src/install/index.ts[2095-2107]
### Suggested fix
Restructure the `try/finally` into `try/catch/finally` so you can distinguish whether pacquet succeeded.
- If pacquet succeeded and `writeEnvLockfile()` fails, throw (or wrap) the restore error so the install fails fast.
- If pacquet failed, attempt restore; if restore also fails, log the restore failure but rethrow the original pacquet error (or consider an `AggregateError` so both are visible).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Pacquet bypasses check mode 🐞 Bug ≡ Correctness
Description
installInContext delegates to pacquet whenever runPacquet is set, without guarding
opts.lockfileCheck, so --check flows can end up doing a real install (and in resolve mode,
rewriting pnpm-lock.yaml). This violates the documented dry-run semantics used by commands like
pnpm dedupe --check.
Code

installing/deps-installer/src/install/index.ts[R2076-2108]

if (opts.runPacquet != null && !opts.lockfileOnly) {
+      // pacquet >= 0.11.7 resolves itself: hand it the whole install
+      // (resolve + fetch + import + link + build, writing the lockfile)
+      // in a single non-frozen pass. Only for plain installs — `add` /
+      // `update` / `remove` need pnpm to mutate the manifests and
+      // resolve the new specs first (pacquet's `install` reads
+      // package.json from disk, which pnpm hasn't rewritten yet).
+      if (opts.runPacquet.supportsResolution && !opts.frozenLockfile && allMutationsAreInstalls(projects)) {
+        // `configDependencies` are recorded in a YAML document prepended
+        // to `pnpm-lock.yaml` — purely a pnpm concept that pacquet doesn't
+        // model. Capture it before pacquet rewrites the lockfile and
+        // restore it afterwards (`writeEnvLockfile` re-reads pacquet's main
+        // document and re-prepends the env document), otherwise the next
+        // `--frozen-lockfile` install fails its config-deps freshness gate.
+        // The restore runs even if pacquet fails partway: a non-zero exit can
+        // still leave a rewritten lockfile behind, so the env document must be
+        // put back regardless.
+        const envLockfile = await readEnvLockfile(ctx.lockfileDir)
+        try {
+          await opts.runPacquet.run({ resolve: true })
+        } finally {
+          if (envLockfile != null) {
+            await writeEnvLockfile(ctx.lockfileDir, envLockfile).catch((restoreErr: Error) => {
+              logger.warn({
+                error: restoreErr,
+                message: `Failed to restore the configDependencies document in pnpm-lock.yaml: ${restoreErr.message}`,
+                prefix: ctx.lockfileDir,
+              })
+            })
+          }
+        }
+        return pacquetResolveResult(projects, ctx)
+      }
Evidence
dedupe --check routes through lockfileCheck specifically to avoid installing packages or editing
the lockfile. The core installer treats lockfileCheck as dry-run by skipping
linking/materialization and skipping lockfile writes, but the pacquet delegation branch does not
check lockfileCheck and can still run pacquet (including the new resolve-delegation path that
explicitly lets pacquet write pnpm-lock.yaml).

installing/commands/src/dedupe.ts[29-71]
installing/deps-installer/src/install/index.ts[1604-1622]
installing/deps-installer/src/install/index.ts[1852-1860]
installing/deps-installer/src/install/index.ts[2073-2108]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`lockfileCheck` is used as a non-mutating “check/dry-run” mode (no `node_modules` writes, no lockfile writes). The new pacquet delegation block in `installInContext` does not check `opts.lockfileCheck`, so pacquet can still be invoked and perform real install work.
## Issue Context
- `pnpm dedupe --check` explicitly sets `lockfileCheck` and documents that it should not install packages or edit the lockfile.
- The JS installer honors this by skipping linking/materialization and by skipping lockfile writes when `lockfileCheck` is present.
- The pacquet delegation block should follow the same rule and be disabled for `lockfileCheck` mode.
## Fix Focus Areas
- Guard pacquet delegation with `opts.lockfileCheck == null` (and likely also `opts.enableModulesDir`) so check-mode stays non-mutating.
- file/path references:
- installing/deps-installer/src/install/index.ts[2059-2119]
- installing/deps-installer/src/install/index.ts[1604-1605]
- installing/deps-installer/src/install/index.ts[1852-1853]
- installing/commands/src/dedupe.ts[29-71]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. Stale lockfile after delegation 🐞 Bug ≡ Correctness
Description
After delegating a resolving install to pacquet (resolve:true), pnpm never reloads the lockfile that
pacquet just rewrote, so ctx.wantedLockfile remains the pre-install snapshot. Post-install logic
that iterates ctx.wantedLockfile.packages (e.g. revoked build-approval detection) can therefore run
against the wrong package set and produce incorrect ignoredBuilds results/logs.
Code

installing/deps-installer/src/install/index.ts[R2093-2108]

+        const envLockfile = await readEnvLockfile(ctx.lockfileDir)
+        try {
+          await opts.runPacquet.run({ resolve: true })
+        } finally {
+          if (envLockfile != null) {
+            await writeEnvLockfile(ctx.lockfileDir, envLockfile).catch((restoreErr: Error) => {
+              logger.warn({
+                error: restoreErr,
+                message: `Failed to restore the configDependencies document in pnpm-lock.yaml: ${restoreErr.message}`,
+                prefix: ctx.lockfileDir,
+              })
+            })
+          }
+        }
+        return pacquetResolveResult(projects, ctx)
+      }
Evidence
The resolve-delegation branch runs pacquet with resolve:true and immediately returns a result that
uses ctx.wantedLockfile without re-reading the rewritten lockfile. The same function later
performs post-install checks that iterate ctx.wantedLockfile.packages, so those checks will
observe stale data after a pacquet-resolved install.

installing/deps-installer/src/install/index.ts[2073-2108]
installing/deps-installer/src/install/index.ts[1936-1956]
installing/deps-installer/src/install/index.ts[477-516]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In the pacquet resolve-delegation path (`await opts.runPacquet.run({ resolve: true })`), pacquet rewrites `pnpm-lock.yaml`, but pnpm does not refresh `ctx.wantedLockfile` afterward and returns `pacquetResolveResult(projects, ctx)`.
Later, `mutateModules()` runs post-install checks that iterate `ctx.wantedLockfile.packages`, which will still reflect the old lockfile contents, not the one pacquet just wrote.
## Issue Context
- The resolve-delegation path intentionally lets pacquet write `pnpm-lock.yaml`.
- pnpm keeps using the previously-loaded `ctx.wantedLockfile` object after delegation.
## Fix Focus Areas
- installing/deps-installer/src/install/index.ts[2073-2108]
- installing/deps-installer/src/install/index.ts[1936-1956]
- installing/deps-installer/src/install/index.ts[477-516]
## Suggested fix
1. After the pacquet run (and after restoring the env document), re-read the updated lockfile from disk and update `ctx.wantedLockfile` to the on-disk content (main document).
2. Return that freshly read lockfile in the `InstallFunctionResult` (either by passing it into `pacquetResolveResult` or by setting `newLockfile` to the re-read value), so any downstream logic uses the correct snapshot.
3. If re-reading fails (e.g. pacquet crashed before writing a valid main document), decide on a safe fallback: keep the old lockfile but skip post-install checks that require an up-to-date package list (or surface a warning).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Env doc lost on failure 🐞 Bug ☼ Reliability
Description
In the pacquet resolve-delegation path, the env/configDependencies YAML document is only restored
after a successful pacquet run; if pacquet exits non-zero after rewriting pnpm-lock.yaml, pnpm
throws before calling writeEnvLockfile(). This can leave pnpm-lock.yaml missing the env document and
break subsequent installs that rely on configDependencies freshness checks.
Code

installing/deps-installer/src/install/index.ts[R2039-2043]

+        const envLockfile = await readEnvLockfile(ctx.lockfileDir)
+        await opts.runPacquet.run({ resolve: true })
+        if (envLockfile != null) {
+          await writeEnvLockfile(ctx.lockfileDir, envLockfile)
+        }
Evidence
The delegation path captures the env doc and restores it only after pacquet completes; however, the
pacquet runner throws on non-zero exit, which skips the restore call, and writeEnvLockfile is the
mechanism that re-prepends the env document to the lockfile.

installing/deps-installer/src/install/index.ts[2033-2043]
installing/commands/src/runPacquet.ts[185-194]
lockfile/fs/src/envLockfile.ts[59-76]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When delegating a resolving install to pacquet, pnpm captures the env lockfile document before running pacquet and restores it afterward. If pacquet fails (throws), the restore step is skipped, potentially leaving `pnpm-lock.yaml` without the prepended env/configDependencies document.
## Issue Context
- `opts.runPacquet.run({ resolve: true })` throws on non-zero exit.
- The env doc restoration is currently only executed on the success path.
## Fix
Ensure the env lockfile document restoration is attempted in a `finally` block (or equivalent), without masking the original pacquet error:
- Capture `envLockfile` before invoking pacquet.
- `try { await runPacquet(...) } catch (e) { savedError = e } finally { if (envLockfile) { try { await writeEnvLockfile(...) } catch (restoreErr) { /* log warn; don't override savedError */ } } }`
- Re-throw the original pacquet error if present.
## Fix Focus Areas
- installing/deps-installer/src/install/index.ts[2033-2044]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Previous review results

Review updated until commit dde125d

Results up to commit ad49a92


🐞 Bugs (7) 📘 Rule violations (0)


Action required
1. Branch lockfile reloaded 🐞 Bug ≡ Correctness ⭐ New
Description
After delegating a resolving install to pacquet, the code reloads the wanted lockfile via
readWantedLockfile() while still honoring useGitBranchLockfile/mergeGitBranchLockfiles. In
git-branch-lockfile mode this can reload an existing branch lockfile instead of the lockfile pacquet
just wrote, leaving ctx.wantedLockfile inconsistent with the materialized node_modules.
Code

installing/deps-installer/src/install/index.ts[R2120-2125]

+        const wantedLockfile = await readWantedLockfile(ctx.lockfileDir, {
+          ignoreIncompatible: opts.force || opts.ci === true,
+          mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
+          useGitBranchLockfile: opts.useGitBranchLockfile,
+          wantedVersions: [LOCKFILE_VERSION],
+        })
Evidence
The pacquet resolve-delegation path reloads the wanted lockfile with git-branch-lockfile options
enabled; but the lockfile reader prefers the branch lockfile name first when configured, so pnpm can
reload a different file than the one pacquet wrote, causing state mismatch.

installing/deps-installer/src/install/index.ts[2082-2131]
lockfile/fs/src/read.ts[212-246]
lockfile/fs/src/lockfileName.ts[9-17]
installing/commands/src/runPacquet.ts[142-164]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The pacquet resolving-install path (`run({ resolve: true })`) refreshes `ctx.wantedLockfile` by calling `readWantedLockfile()` with `useGitBranchLockfile`/`mergeGitBranchLockfiles` options. In git-branch-lockfile mode, `readWantedLockfile()` prefers an existing branch lockfile over `pnpm-lock.yaml`, so pnpm may re-read a different file than the one pacquet just produced, leaving `ctx.wantedLockfile` stale/out-of-sync.

### Issue Context
- `readWantedLockfile()` tries the git-branch lockfile name first when `useGitBranchLockfile` is enabled.
- pacquet is spawned with `install` and no lockfile-name override, so pnpm must ensure it reloads the same file pacquet wrote.

### Fix Focus Areas
- installing/deps-installer/src/install/index.ts[2082-2131]
- installing/deps-installer/src/install/index.ts[391-411]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. No-lockfile pacquet failure 🐞 Bug ≡ Correctness
Description
installInContext delegates a resolving install to pacquet and then unconditionally reads
pnpm-lock.yaml, throwing if it is missing, but this branch does not check opts.useLockfile
("--no-lockfile"). This can either violate the no-lockfile contract (by creating a lockfile) or fail
the install with PACQUET_LOCKFILE_READ_FAILED if pacquet honors --no-lockfile and does not write
one.
Code

installing/deps-installer/src/install/index.ts[R2081-2127]

+    if (opts.runPacquet != null && !opts.lockfileOnly && opts.lockfileCheck == null && opts.enableModulesDir) {
+      // pacquet >= 0.11.7 resolves itself: hand it the whole install
+      // (resolve + fetch + import + link + build, writing the lockfile)
+      // in a single non-frozen pass. Only for plain installs — `add` /
+      // `update` / `remove` need pnpm to mutate the manifests and
+      // resolve the new specs first (pacquet's `install` reads
+      // package.json from disk, which pnpm hasn't rewritten yet).
+      if (opts.runPacquet.supportsResolution && !opts.frozenLockfile && opts.handleResolutionPolicyViolations == null && allMutationsAreInstalls(projects)) {
+        // `configDependencies` are recorded in a YAML document prepended
+        // to `pnpm-lock.yaml` — purely a pnpm concept that pacquet doesn't
+        // model. Capture it before pacquet rewrites the lockfile and
+        // restore it afterwards (`writeEnvLockfile` re-reads pacquet's main
+        // document and re-prepends the env document), otherwise the next
+        // `--frozen-lockfile` install fails its config-deps freshness gate.
+        // The restore runs even if pacquet fails partway: a non-zero exit can
+        // still leave a rewritten lockfile behind, so the env document must be
+        // put back regardless.
+        const envLockfile = await readEnvLockfile(ctx.lockfileDir)
+        let pacquetError: unknown
+        try {
+          await opts.runPacquet.run({ resolve: true })
+        } catch (err: unknown) {
+          pacquetError = err
+          throw err
+        } finally {
+          if (envLockfile != null) {
+            await writeEnvLockfile(ctx.lockfileDir, envLockfile).catch((restoreErr: Error) => {
+              if (pacquetError == null) {
+                throw restoreErr
+              }
+              logger.warn({
+                error: restoreErr,
+                message: `Failed to restore the configDependencies document in pnpm-lock.yaml: ${restoreErr.message}`,
+                prefix: ctx.lockfileDir,
+              })
+            })
+          }
+        }
+        const wantedLockfile = await readWantedLockfile(ctx.lockfileDir, {
+          ignoreIncompatible: opts.force || opts.ci === true,
+          mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
+          useGitBranchLockfile: opts.useGitBranchLockfile,
+          wantedVersions: [LOCKFILE_VERSION],
+        })
+        if (wantedLockfile == null) {
+          throw new PnpmError('PACQUET_LOCKFILE_READ_FAILED', `pacquet did not write a readable ${WANTED_LOCKFILE}`)
+        }
Evidence
The pacquet resolving branch runs without any opts.useLockfile check and then requires a readable
wanted lockfile, while the installer options explicitly support disabling lockfile generation via
useLockfile (only lockfileOnly forbids it).

installing/deps-installer/src/install/index.ts[2078-2127]
installing/deps-installer/src/install/extendInstallOptions.ts[356-430]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The pacquet resolve-delegation path assumes `pnpm-lock.yaml` exists after `run({ resolve: true })` and throws if it doesn’t. This is incompatible with `useLockfile=false` / `--no-lockfile` installs.
### Issue Context
`extendInstallOptions` supports turning lockfiles off (`useLockfile=false`). In that mode, the installer should not require a wanted lockfile to exist, nor should it error because it’s missing.
### Fix Focus Areas
- installing/deps-installer/src/install/index.ts[2081-2127]
Suggested implementation direction:
- Add an explicit guard to the pacquet resolving-delegation branch so it only runs when lockfiles are enabled and expected to be written, e.g. require `opts.useLockfile && opts.saveLockfile` in the `supportsResolution` condition.
- Alternatively (if you want to support pacquet resolving installs without a lockfile), skip `readWantedLockfile(...)`/`PACQUET_LOCKFILE_READ_FAILED` and return an `InstallFunctionResult` that does not assume a lockfile exists; but this likely requires additional downstream adjustments, so the safe fix is to not delegate in no-lockfile mode.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Policy violations dropped 🐞 Bug ≡ Correctness
Description
In the pacquet resolving-install path, pnpm returns an InstallFunctionResult with
resolutionPolicyViolations hard-coded to [], so policy violations discovered during resolution are
not surfaced to the commands layer. This prevents policy UX/persistence like
minimumReleaseAgeExclude auto-collect (and any future policy-driven workspace-manifest updates),
risking later install failures when pnpm re-validates the lockfile outside the pacquet resolve path.
Code

installing/deps-installer/src/install/index.ts[R1948-1957]

+function pacquetResolveResult (projects: ImporterToUpdate[], ctx: PnpmContext): InstallFunctionResult {
+  return {
+    newLockfile: ctx.wantedLockfile,
+    projects: projects.map((project) => ({
+      manifest: project.originalManifest ?? project.manifest,
+      rootDir: project.rootDir,
+    })),
+    depsRequiringBuild: [],
+    resolutionPolicyViolations: [],
+  }
Evidence
The pacquet resolve path returns pacquetResolveResult(...), which hard-codes an empty
resolutionPolicyViolations array. The install command uses resolutionPolicyViolations to derive
and persist policy updates like minimumReleaseAgeExclude, so this new behavior suppresses those
updates and associated UX.

installing/deps-installer/src/install/index.ts[2069-2127]
installing/deps-installer/src/install/index.ts[1939-1958]
installing/deps-installer/src/install/index.ts[1519-1539]
installing/commands/src/installDeps.ts[407-465]
installing/commands/src/policyHandlers.ts[139-196]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When pacquet performs a resolving install (`runPacquet.run({ resolve: true })`), pnpm returns `pacquetResolveResult(...)`, which currently hard-codes `resolutionPolicyViolations: []`. The commands layer relies on `resolutionPolicyViolations` to drive policy UX and persistence (e.g. minimumReleaseAge strict prompting / auto-persisting `minimumReleaseAgeExclude`), so this path silently suppresses that behavior.
### Issue Context
- The resolving pacquet path returns early from `installInContext` and bypasses pnpm’s normal `resolveDependencies(...)` flow (where `handleResolutionPolicyViolations` is applied and violations are collected).
- Downstream code uses the returned `resolutionPolicyViolations` to update workspace policy state (e.g. write `minimumReleaseAgeExclude` entries).
### Fix Focus Areas
- installing/deps-installer/src/install/index.ts[1939-1957]
- installing/deps-installer/src/install/index.ts[2078-2127]
- installing/deps-installer/src/install/index.ts[1519-1539]
### Suggested fix approach
Pick one of these (A is simplest/most correct wrt strict-mode prompting):
**A) Don’t delegate resolve to pacquet when policy UX/persistence is active**
- In the `supportsResolution && !opts.frozenLockfile && allMutationsAreInstalls(...)` branch, add a guard that disables the `resolve:true` delegation when pnpm policy handling is active (e.g. `opts.handleResolutionPolicyViolations` is set and/or policy-related opts like `minimumReleaseAge` are present).
- Fall back to the existing pnpm resolve-then-materialize flow in that case so pnpm can collect violations and prompt/persist before side effects.
**B) Reconstruct and return violations after pacquet resolves**
- After reading the lockfile written by pacquet, compute `resolutionPolicyViolations` for the new lockfile (using the same verifier chain) and return them instead of `[]`.
- If `opts.handleResolutionPolicyViolations` exists, invoke it with the collected violations (note: strict-mode prompting after pacquet has already mutated state is likely unacceptable, so still consider gating strict mode back to A).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
4. Restore error swallowed 🐞 Bug ☼ Reliability
Description
After delegating a resolving install to pacquet, writeEnvLockfile() restore failures are caught and
only logged, so the command can succeed while leaving pnpm-lock.yaml without the
env/configDependencies document. This can later break --frozen-lockfile installs and also prevents
pacquet identity verification from locating the declared configDependency via readEnvLockfile().
Code

installing/deps-installer/src/install/index.ts[R2099-2106]

+          if (envLockfile != null) {
+            await writeEnvLockfile(ctx.lockfileDir, envLockfile).catch((restoreErr: Error) => {
+              logger.warn({
+                error: restoreErr,
+                message: `Failed to restore the configDependencies document in pnpm-lock.yaml: ${restoreErr.message}`,
+                prefix: ctx.lockfileDir,
+              })
+            })
Evidence
The pacquet resolve path catches and warns on writeEnvLockfile() failure, allowing a successful
install to leave the lockfile without the env document. The env document is required for
configDependencies-related frozen installs, and pacquet verification depends on reading
configDependencies from readEnvLockfile().

installing/deps-installer/src/install/index.ts[2089-2107]
lockfile/fs/src/envLockfile.ts[59-76]
pnpm/test/install/configDeps.ts[23-34]
installing/commands/src/verifyPacquetIdentity.ts[82-92]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`writeEnvLockfile()` restore errors are currently swallowed (warn-only) after a successful pacquet resolving install. This can leave `pnpm-lock.yaml` missing the prepended env/configDependencies document while the install still exits 0, causing subsequent `--frozen-lockfile` installs to fail and preventing pacquet identity verification (which reads configDependencies from the env doc) from working reliably.
### Issue Context
The restore is meant to preserve pnpm’s multi-document lockfile env header across pacquet rewrites. If restore fails, the correct behavior is to fail the command (at least when pacquet itself succeeded), rather than leaving a silently-corrupted lockfile.
### Fix Focus Areas
- installing/deps-installer/src/install/index.ts[2095-2107]
### Suggested fix
Restructure the `try/finally` into `try/catch/finally` so you can distinguish whether pacquet succeeded.
- If pacquet succeeded and `writeEnvLockfile()` fails, throw (or wrap) the restore error so the install fails fast.
- If pacquet failed, attempt restore; if restore also fails, log the restore failure but rethrow the original pacquet error (or consider an `AggregateError` so both are visible).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Pacquet bypasses check mode 🐞 Bug ≡ Correctness
Description
installInContext delegates to pacquet whenever runPacquet is set, without guarding
opts.lockfileCheck, so --check flows can end up doing a real install (and in resolve mode,
rewriting pnpm-lock.yaml). This violates the documented dry-run semantics used by commands like
pnpm dedupe --check.
Code

installing/deps-installer/src/install/index.ts[R2076-2108]

if (opts.runPacquet != null && !opts.lockfileOnly) {
+      // pacquet >= 0.11.7 resolves itself: hand it the whole install
+      // (resolve + fetch + import + link + build, writing the lockfile)
+      // in a single non-frozen pass. Only for plain installs — `add` /
+      // `update` / `remove` need pnpm to mutate the manifests and
+      // resolve the new specs first (pacquet's `install` reads
+      // package.json from disk, which pnpm hasn't rewritten yet).
+      if (opts.runPacquet.supportsResolution && !opts.frozenLockfile && allMutationsAreInstalls(projects)) {
+        // `configDependencies` are recorded in a YAML document prepended
+        // to `pnpm-lock.yaml` — purely a pnpm concept that pacquet doesn't
+        // model. Capture it before pacquet rewrites the lockfile and
+        // restore it afterwards (`writeEnvLockfile` re-reads pacquet's main
+        // document and re-prepends the env document), otherwise the next
+        // `--frozen-lockfile` install fails its config-deps freshness gate.
+        // The restore runs even if pacquet fails partway: a non-zero exit can
+        // still leave a rewritten lockfile behind, so the env document must be
+        // put back regardless.
+        const envLockfile = await readEnvLockfile(ctx.lockfileDir)
+        try {
+          await opts.runPacquet.run({ resolve: true })
+        } finally {
+          if (envLockfile != null) {
+            await writeEnvLockfile(ctx.lockfileDir, envLockfile).catch((restoreErr: Error) => {
+              logger.warn({
+                error: restoreErr,
+                message: `Failed to restore the configDependencies document in pnpm-lock.yaml: ${restoreErr.message}`,
+                prefix: ctx.lockfileDir,
+              })
+            })
+          }
+        }
+        return pacquetResolveResult(projects, ctx)
+      }
Evidence
dedupe --check routes through lockfileCheck specifically to avoid installing packages or editing
the lockfile. The core installer treats lockfileCheck as dry-run by skipping
linking/materialization and skipping lockfile writes, but the pacquet delegation branch does not
check lockfileCheck and can still run pacquet (including the new resolve-delegation path that
explicitly lets pacquet write pnpm-lock.yaml).

installing/commands/src/dedupe.ts[29-71]
installing/deps-installer/src/install/index.ts[1604-1622]
installing/deps-installer/src/install/index.ts[1852-1860]
installing/deps-installer/src/install/index.ts[2073-2108]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`lockfileCheck` is used as a non-mutating “check/dry-run” mode (no `node_modules` writes, no lockfile writes). The new pacquet delegation block in `installInContext` does not check `opts.lockfileCheck`, so pacquet can still be invoked and perform real install work.
## Issue Context
- `pnpm dedupe --check` explicitly sets `lockfileCheck` and documents that it should not install packages or edit the lockfile.
- The JS installer honors this by skipping linking/materialization and by skipping lockfile writes when `lockfileCheck` is present.
- The pacquet delegation block should follow the same rule and be disabled for `lockfileCheck` mode.
## Fix Focus Areas
- Guard pacquet delegation with `opts.lockfileCheck == null` (and likely also `opts.enableModulesDir`) so check-mode stays non-mutating.
- file/path references:
- installing/deps-installer/src/install/index.ts[2059-2119]
- installing/deps-installer/src/install/index.ts[1604-1605]
- installing/deps-installer/src/install/index.ts[1852-1853]
- installing/commands/src/dedupe.ts[29-71]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
6. Stale lockfile after delegation 🐞 Bug ≡ Correctness
Description
After delegating a resolving install to pacquet (resolve:true), pnpm never reloads the lockfile that
pacquet just rewrote, so ctx.wantedLockfile remains the pre-install snapshot. Post-install logic
that iterates ctx.wantedLockfile.packages (e.g. revoked build-approval detection) can therefore run
against the wrong package set and produce incorrect ignoredBuilds results/logs.
Code

installing/deps-installer/src/install/index.ts[R2093-2108]

+        const envLockfile = await readEnvLockfile(ctx.lockfileDir)
+        try {
+          await opts.runPacquet.run({ resolve: true })
+        } finally {
+          if (envLockfile != null) {
+            await writeEnvLockfile(ctx.lockfileDir, envLockfile).catch((restoreErr: Error) => {
+              logger.warn({
+                error: restoreErr,
+                message: `Failed to restore the configDependencies document in pnpm-lock.yaml: ${restoreErr.message}`,
+                prefix: ctx.lockfileDir,
+              })
+            })
+          }
+        }
+        return pacquetResolveResult(projects, ctx)
+      }
Evidence
The resolve-delegation branch runs pacquet with resolve:true and immediately returns a result that
uses ctx.wantedLockfile without re-reading the rewritten lockfile. The same function later
performs post-install checks that iterate ctx.wantedLockfile.packages, so those checks will
observe stale data after a pacquet-resolved install.

installing/deps-installer/src/install/index.ts[2073-2108]
installing/deps-installer/src/install/index.ts[1936-1956]
installing/deps-installer/src/install/index.ts[477-516]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In the pacquet resolve-delegation path (`await opts.runPacquet.run({ resolve: true })`), pacquet rewrites `pnpm-lock.yaml`, but pnpm does not refresh `ctx.wantedLockfile` afterward and returns `pacquetResolveResult(projects, ctx)`.
Later, `mutateModules()` runs post-install checks that iterate `ctx.wantedLockfile.packages`, which will still reflect the old lockfile contents, not the one pacquet just wrote.
## Issue Context
- The resolve-delegation path intentionally lets pacquet write `pnpm-lock.yaml`.
- pnpm keeps using the previously-loaded `ctx.wantedLockfile` object after delegation.
## Fix Focus Areas
- installing/deps-installer/src/install/index.ts[2073-2108]
- installing/deps-installer/src/install/index.ts[1936-1956]
- installing/deps-installer/src/install/index.ts[477-516]
## Suggested fix
1. After the pacquet run (and after restoring the env document), re-read the updated lockfile from disk and update `ctx.wantedLockfile` to the on-disk content (main document).
2. Return that freshly read lockfile in the `InstallFunctionResult` (either by passing it into `pacquetResolveResult` or by setting `newLockfile` to the re-read value), so any downstream logic uses the correct snapshot.
3. If re-reading fails (e.g. pacquet crashed before writing a valid main document), decide on a safe fallback: keep the old lockfile but skip post-install checks that require an up-to-date package list (or surface a warning).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Env doc lost on failure 🐞 Bug ☼ Reliability
Description
In the pacquet resolve-delegation path, the env/configDependencies YAML document is only restored
after a successful pacquet run; if pacquet exits non-zero after rewriting pnpm-lock.yaml, pnpm
throws before calling writeEnvLockfile(). This can leave pnpm-lock.yaml missing the env document and
break subsequent installs that rely on configDependencies freshness checks.
Code

installing/deps-installer/src/install/index.ts[R2039-2043]

+        const envLockfile = await readEnvLockfile(ctx.lockfileDir)
+        await opts.runPacquet.run({ resolve: true })
+        if (envLockfile != null) {
+          await writeEnvLockfile(ctx.lockfileDir, envLockfile)
+        }
Evidence
The delegation path captures the env doc and restores it only after pacquet completes; however, the
pacquet runner throws on non-zero exit, which skips the restore call, and writeEnvLockfile is the
mechanism that re-prepends the env document to the lockfile.

installing/deps-installer/src/install/index.ts[2033-2043]
installing/commands/src/runPacquet.ts[185-194]
lockfile/fs/src/envLockfile.ts[59-76]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When delegating a resolving install to pacquet, pnpm captures the env lockfile document before running pacquet and restores it afterward. If pacquet fails (throws), the restore step is skipped, potentially leaving `pnpm-lock.yaml` without the prepended env/configDependencies document.
## Issue Context
- `opts.runPacquet.run({ resolve: true })` throws on non-zero exit.
- The env doc restoration is currently only executed on the success path.
## Fix
Ensure the env lockfile document restoration is attempted in a `finally` block (or equivalent), without masking the original pacquet error:
- Capture `envLockfile` before invoking pacquet.
- `try { await runPacquet(...) } catch (e) { savedError = e } finally { if (envLockfile) { try { await writeEnvLockfile(...) } catch (restoreErr) { /* log warn; don't override savedError */ } } }`
- Re-throw the original pacquet error if present.
## Fix Focus Areas
- installi...

Comment thread installing/deps-installer/src/install/index.ts
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 13, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit f637f25

zkochan added 4 commits June 14, 2026 14:36
When pacquet is declared in configDependencies, pnpm previously always
ran it with --frozen-lockfile (pnpm resolved, pacquet materialized). If
the installed pacquet is >= 0.11 it ships its own resolver, so a
non-frozen plain install on the default isolated linker is now delegated
end-to-end: pacquet resolves, writes pnpm-lock.yaml, and materializes in
a single pass. Older pacquet keeps the resolve-then-materialize split,
and add/update/remove still resolve in pnpm.
Bump the e2e pacquet pin to 0.11.0 (published under both pacquet and
@pnpm/pacquet) and add tests for the resolve path, the materialize-only
fallback (pinned to 0.2.14), and the scoped alias. Un-skip the add/update
tests now that pacquet 0.11 writes a compatible .modules.yaml, and fix
the update test (is-positive has no v4; update from 1.0.0 instead).

Also preserve the configDependencies env document when pacquet resolves:
pacquet rewrites pnpm-lock.yaml without the leading env YAML doc, which
dropped configDependencies and broke the next --frozen-lockfile install.
Capture it before delegating and restore it after.
…cquet fails

On the pacquet resolve-delegation path the configDependencies env document
was only restored after a successful pacquet run. A non-zero exit could
leave a rewritten pnpm-lock.yaml without it, breaking the next
--frozen-lockfile install's config-deps freshness gate. Restore it in a
finally block, swallowing (and warning on) any restore error so it cannot
mask the original pacquet failure.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 14, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 674f1ec

@zkochan zkochan changed the title feat(installing): delegate resolution to pacquet >= 0.11 when configured feat(installing): delegate resolution to pacquet >= 0.11.7 when configured Jun 14, 2026
Comment thread installing/deps-installer/src/install/index.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pnpm/test/install/pacquet.ts`:
- Around line 80-97: The two "newly-added dependency" tests (in
pnpm/test/install/pacquet.ts at lines 80-97 and 99-115) assert identical
outcomes despite describing different resolver ownership paths. Add a
differentiating assertion to each test that proves which resolver performed the
resolution, not just that pacquet materialized the result. The first test (lines
80-97) should validate the behavior specific to pacquet performing resolution
itself in a non-frozen pass, while the second test (lines 99-115) should
validate the behavior specific to pnpm performing the resolution. This prevents
a regression where both execution paths converge but still pass the same
assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a53f26f-c81d-4e1d-972f-d5ab9c915012

📥 Commits

Reviewing files that changed from the base of the PR and between f637f25 and 674f1ec.

📒 Files selected for processing (7)
  • .changeset/pacquet-resolving-install-delegation.md
  • installing/commands/src/recursive.ts
  • installing/commands/src/runPacquet.ts
  • installing/commands/test/runPacquet.ts
  • installing/deps-installer/src/install/extendInstallOptions.ts
  • installing/deps-installer/src/install/index.ts
  • pnpm/test/install/pacquet.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/pacquet-resolving-install-delegation.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • installing/commands/src/recursive.ts
  • installing/commands/test/runPacquet.ts
  • installing/deps-installer/src/install/extendInstallOptions.ts
  • installing/commands/src/runPacquet.ts
  • installing/deps-installer/src/install/index.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use Standard Style with trailing commas, prefer functions over classes, declare functions after they are used relying on hoisting, limit function arguments to two or three with options objects for additional parameters
Follow import order: standard libraries, external dependencies (sorted alphabetically), then relative imports
Use JSDoc for function contracts (preconditions, postconditions, edge cases, why it exists) not for re-narrating the body; do not record past implementation shape or refactor history in comments

Files:

  • pnpm/test/install/pacquet.ts
🧠 Learnings (4)
📚 Learning: 2026-05-14T09:04:00.133Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11622
File: resolving/npm-resolver/test/publishedBy.test.ts:350-354
Timestamp: 2026-05-14T09:04:00.133Z
Learning: In the pnpm/pnpm repository, ESLint is the authoritative style linter. Do not raise review findings for missing trailing commas in multiline function calls (e.g., `fs.writeFileSync(...)`) when this repo’s ESLint configuration does not report them and lint passes. Prefer deferring to the ESLint results for this specific trailing-comma rule rather than enforcing it manually in code review.

Applied to files:

  • pnpm/test/install/pacquet.ts
📚 Learning: 2026-05-24T08:18:00.622Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11895
File: pnpm/test/deploy.ts:91-95
Timestamp: 2026-05-24T08:18:00.622Z
Learning: In pnpm/pnpm integration tests that intentionally hit the real npm registry (registry.npmjs.org)—for example when the test passes `--config.registry=https://registry.npmjs.org/` to `execPnpm` and simply increases the timeout—do not request adding a runtime environment-variable gate (e.g., `PNPM_RUN_PUBLIC_REGISTRY_TESTS`). This is the established pattern for those tests (see files like `pnpm/test/install/pacquet.ts` and `pnpm/test/deploy.ts`).

Applied to files:

  • pnpm/test/install/pacquet.ts
📚 Learning: 2026-06-05T13:47:05.929Z
Learnt from: vsumner
Repo: pnpm/pnpm PR: 12190
File: installing/deps-installer/test/install/frozenStore.ts:2-17
Timestamp: 2026-06-05T13:47:05.929Z
Learning: In the pnpm/pnpm repository, the shared Jest preset keeps `injectGlobals` at its default (`true`), so `test` and `expect` are available as Jest globals. Therefore, reviewers should not flag (or treat as TypeScript/compilation errors) missing `import { test, expect } from 'jest/globals'` when a test file uses `test`/`expect` without importing them. Importing from `jest/globals` may still be used for consistency with sibling files, but it is not required for execution in this repo unless a Jest preset is explicitly configured with `injectGlobals: false`.

Applied to files:

  • pnpm/test/install/pacquet.ts
📚 Learning: 2026-06-05T13:47:26.046Z
Learnt from: vsumner
Repo: pnpm/pnpm PR: 12190
File: installing/deps-installer/src/install/index.ts:2337-2343
Timestamp: 2026-06-05T13:47:26.046Z
Learning: In the pnpm/pnpm codebase, `PnpmError` automatically prefixes `err.code` with `ERR_PNPM_` when you pass a code that does not already start with `ERR_PNPM_` (it normalizes `this.code` via `code.startsWith('ERR_PNPM_') ? code : `ERR_PNPM_${code}``). Therefore, during code review you should NOT flag `new PnpmError(...)` call sites for passing a bare error code (e.g., `new PnpmError('FROZEN_STORE_INCOMPATIBLE_WITH_PNPR', ...)`); the resulting `err.code` will still be `ERR_PNPM_FROZEN_STORE_INCOMPATIBLE_WITH_PNPR`.

Applied to files:

  • pnpm/test/install/pacquet.ts
🔇 Additional comments (1)
pnpm/test/install/pacquet.ts (1)

13-17: No boundary inconsistency found—implementation, tests, and release notes all consistently use >= 0.11.7.

All layers use the same version boundary: PACQUET_VERSION = '0.11.7' in tests, >= 0.11.7 in implementation comments, delegation gating, and release notes. The version check logic in pacquetSupportsResolution correctly handles pre-releases (e.g., 0.11.7-rc.1). No changes required.

			> Likely an incorrect or invalid review comment.

Comment thread pnpm/test/install/pacquet.ts
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 14, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 875ef2a

Comment thread installing/deps-installer/src/install/index.ts
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 14, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit ce8df5b

Comment thread installing/deps-installer/src/install/index.ts
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 14, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit cf7fa36

Comment thread installing/deps-installer/src/install/index.ts Outdated
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 14, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit ad49a92

Comment thread installing/deps-installer/src/install/index.ts
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 14, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit dde125d

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants