Stage 0
Stage 1 — Headless installer
Make pacquet install --frozen-lockfile feature-complete with pnpm install --frozen-lockfile.
Pacquet does not resolve dependencies in this stage. The user runs pnpm install (or pnpm install --lockfile-only) to produce pnpm-lock.yaml, then pacquet install --frozen-lockfile materializes node_modules from it. The lockfile is the contract between the two tools.
This mirrors pnpm's internal @pnpm/headless boundary, so the seam is natural.
Completed
TODO
Ordered by implementation dependency — items lower in the list generally build on items above.
Tier 1 — Foundations. Correct snapshot filtering and lockfile loading; nothing downstream is sound until these are in place.
Tier 2 — Real-world install enablers. Each unlocks a large slice of real pnpm repos and is largely independent of the others.
Tier 3 — New resolution types and layout modes. Each is a self-contained extension to the install pipeline; the ordering inside this tier is mostly arbitrary.
Tier 4 — Consistency, polish, auxiliary. Quality-of-life fixes that close the parity gap once the structural work is in place.
Integration milestone (between Stage 1 and Stage 2)
Once the headless installer is solid, integrate pacquet as an opt-in install backend for pnpm itself — for example via an install-backend=pacquet setting, or a Node N-API addon hooked at the @pnpm/headless seam. This ships Stage 1 value to every pnpm user, not just those who install pacquet directly, and provides a feedback firehose for parity validation before Stage 2 is built.
Stage 2 — Implementing resolution
Make pacquet install feature-complete with pnpm install (no --frozen-lockfile). Pacquet reads package.json + pnpm-workspace.yaml, resolves the dependency graph, writes a pnpm-lock.yaml that round-trips byte-identically with pnpm's, and materializes node_modules. This unlocks the install subcommands that today only work via pnpm: install (default), install --lockfile-only, add, update, remove, outdated, why.
Pacquet already has scaffolding from Stage 1: crates/registry, crates/resolving-npm-resolver (metadata fetch + cache, named registries, mirroring, trust checks), crates/resolving-resolver-base, and a full lockfile writer (crates/lockfile/save_lockfile). Stage 2 fills in the parts that turn a manifest into a resolved graph.
TODO
Ordered by implementation dependency — items lower in the list generally build on items above.
Tier 1 — Foundations. Nothing downstream is sound until these land.
Tier 2 — Per-protocol resolvers. Each unlocks a class of spec. Largely independent.
Tier 3 — Resolution policies & constraints. Behaviors layered on top of the recursion that change which version wins or whether the install proceeds.
Tier 4 — Hooks & custom resolution. Both require running user JavaScript inside pacquet — design call needed.
Tier 5 — Commands unlocked by resolution. Stage 1 only delivers pacquet install --frozen-lockfile. Once resolution lands, each of these is a CLI surface port that follows the underlying capability.
Tier 6 — Consistency, polish, auxiliary. Close the parity gap once the structural work is in place.
Integration milestone (between Stage 2 and Stage 3)
Once resolution is solid, extend the Stage-1 → Stage-2 opt-in seam (install-backend=pacquet / N-API) to route everyday pnpm install calls through pacquet end-to-end — not just --frozen-lockfile. Stage 3 then targets the non-install command surface (publish, audit, dlx, exec, run, store).
Stage 3 — TBD
Stage 2 plan drafted by an agent (Claude Code, claude-opus-4-7).
Stage 0
Stage 1 — Headless installer
Make
pacquet install --frozen-lockfilefeature-complete withpnpm install --frozen-lockfile.Pacquet does not resolve dependencies in this stage. The user runs
pnpm install(orpnpm install --lockfile-only) to producepnpm-lock.yaml, thenpacquet install --frozen-lockfilematerializesnode_modulesfrom it. The lockfile is the contract between the two tools.This mirrors pnpm's internal
@pnpm/headlessboundary, so the seam is natural.Completed
.modules.yamlwrite and verify pacquet#331 (.modules.yamlwrite and verify)@pnpm/core-loggersschema (Run post install scripts after all dependencies were installed #345). Visual parity via@pnpm/cli.default-reporteris tracked under Tier 4 below (EACCESS error on runas #344).TODO
Ordered by implementation dependency — items lower in the list generally build on items above.
Tier 1 — Foundations. Correct snapshot filtering and lockfile loading; nothing downstream is sound until these are in place.
optionalDependenciessupport (umbrella). Without correctos/cpu/libcfiltering, every install over-fetches and may break on platforms other than the build host. Subsumes Install respects every snapshot — no os/cpu/libc filter for optional deps pacquet#266.crates/lockfile/src/resolution.rsuses#[serde(deny_unknown_fields)]and is missingTarballResolution.gitHosted,BinaryResolution, andVariationsResolution. A real v11pnpm-lock.yamlcontaining a github-tarball, a git-hosted package, or a runtime entry fails serde before the install dispatcher runs. Types covered piecemeal in Install git-hosted packages frompnpm-lock.yaml(frozen-lockfile) pacquet#436 and Install runtime dependencies (node@runtime:,deno@runtime:,bun@runtime:) frompnpm-lock.yamlpacquet#437; land them as a single unblocker so loading is correct before the install logic for each shape ships.package.jsondependencies don't match the lockfile importer entries, mirroring upstream'sallProjectsAreUpToDate. Today pacquet installs regardless of staleness, which silently masks lockfile/manifest drift in CI.Tier 2 — Real-world install enablers. Each unlocks a large slice of real pnpm repos and is largely independent of the others.
pacquet install --frozen-lockfilepacquet#431 — Support workspaces. Most real pnpm repos are workspaces; until this lands, pacquet can't install them at all.--frozen-lockfile: read+writenode_modules/.pnpm/lock.yamland skip unchanged snapshots pacquet#433 — Partial install with--frozen-lockfile: read+writenode_modules/.pnpm/lock.yamland skip unchanged snapshots. Closes the warm-install perf gap.hoistPatternandpublicHoistPatternpacquet#435 — Hoisting (hoistPattern,publicHoistPattern). Needed for ecosystem packages that rely on phantom dependencies and for editor tooling (eslint/prettier).Tier 3 — New resolution types and layout modes. Each is a self-contained extension to the install pipeline; the ordering inside this tier is mostly arbitrary.
pacquet install --frozen-lockfilepacquet#432 — Support the global virtual store dir (Stage 1: frozen-lockfile install + per-importer registry). Follow-ups: Implementpacquet store prunefor the global virtual store (#432 follow-up) pacquet#458 (prune sweep), Engine-agnostic GVS hash gating viaallowBuilds-drivenbuilt_dep_paths(#432 follow-up) pacquet#459 (engine-agnostic hash gating); CI auto-detect / CONFIG_CONFLICT validation / e2eglobalVirtualStore.tsport remain as bullets on Add global virtual store support topacquet install --frozen-lockfilepacquet#432.pnpm-lock.yaml(frozen-lockfile) pacquet#436 — Install git-hosted packages frompnpm-lock.yaml(frozen-lockfile).node@runtime:,deno@runtime:,bun@runtime:) frompnpm-lock.yamlpacquet#437 — Install runtime dependencies (node@runtime:,deno@runtime:,bun@runtime:).nodeLinker: 'hoisted'(umbrella). Alternative installer; orthogonal to Add hoisting support:hoistPatternandpublicHoistPatternpacquet#435.Tier 4 — Consistency, polish, auxiliary. Quality-of-life fixes that close the parity gap once the structural work is in place.
.pnpm/that no longer appear in the lockfile (upstream'spruneVirtualStore). Sister to Partial install with--frozen-lockfile: read+writenode_modules/.pnpm/lock.yamland skip unchanged snapshots pacquet#433 — partial install decides what to add; this decides what to remove..modules.yamlvalidation triggering re-install on layout change. Upstream'svalidateModulesforces a full re-import whennodeLinker/hoistPattern/publicHoistPattern/ etc. differ between runs. Pacquet writes.modules.yaml(feat(install): install from local packages #331) but does not compare on the read side, so layout-affecting setting changes are silently ignored.@pnpm/cli.default-reporterfor terminal output (via NDJSON). Brings visual parity withpnpm installon top of the reporting engine landed in feat(reporter): implement the reporting engine (NDJSON + Reporter trait) pacquet#345.Integration milestone (between Stage 1 and Stage 2)
Once the headless installer is solid, integrate pacquet as an opt-in install backend for pnpm itself — for example via an
install-backend=pacquetsetting, or a Node N-API addon hooked at the@pnpm/headlessseam. This ships Stage 1 value to every pnpm user, not just those who install pacquet directly, and provides a feedback firehose for parity validation before Stage 2 is built.Stage 2 — Implementing resolution
Make
pacquet installfeature-complete withpnpm install(no--frozen-lockfile). Pacquet readspackage.json+pnpm-workspace.yaml, resolves the dependency graph, writes apnpm-lock.yamlthat round-trips byte-identically with pnpm's, and materializesnode_modules. This unlocks the install subcommands that today only work via pnpm:install(default),install --lockfile-only,add,update,remove,outdated,why.Pacquet already has scaffolding from Stage 1:
crates/registry,crates/resolving-npm-resolver(metadata fetch + cache, named registries, mirroring, trust checks),crates/resolving-resolver-base, and a full lockfile writer (crates/lockfile/save_lockfile). Stage 2 fills in the parts that turn a manifest into a resolved graph.TODO
Ordered by implementation dependency — items lower in the list generally build on items above.
Tier 1 — Foundations. Nothing downstream is sound until these land.
@pnpm/resolving.parse-wanted-dependency(npm aliaspnpm:foo@npm:bar, scoped names, tag vs. semver) and the protocol-routing dispatcher at the top ofresolving/default-resolver/src/index.ts. Slots into the existingresolving-resolver-basecrate.installing/deps-resolver/src/resolveDependencyTree.tsandresolveDependencies.ts(~2.3k LoC combined). Owns dedupe by name (pkgIdsByName),preferredVersions, direct-vs-transitive context,resolutionsByDepPathcache,pendingNodes, linked-workspace tracking. Heart of this stage.save_lockfileround-trips Stage 1 inputs; confirm it serializes newpackages/snapshots/importers/catalogs/overrides/patchedDependenciesbyte-identically with pnpm sopnpm installafterpacquet installproduces no diff.Tier 2 — Per-protocol resolvers. Each unlocks a class of spec. Largely independent.
pickPackage.ts/pickPackageFromMeta.ts: semver matching, tag handling,allowedDeprecatedVersions,minimumReleaseAge. Metadata fetch already exists inresolving-npm-resolver.resolving/git-resolver:git+ssh,github:owner/repo#ref,#semver:^1selectors, GitHub-tarball preference. Pairs with the Stage-1 git-fetcher crate.resolving/tarball-resolver: directhttps://…/foo.tgzresolution + integrity verify.resolving/local-resolver:file:../foo,link:../foo, injected deps. The Stage-1directory-fetchercrate handles the fetch side.resolving/jsr-specifier-parser; wires into the JSR path already drafted inresolving-npm-resolver.node@runtime:/deno@runtime:/bun@runtime:. Stage 1 already loads them from the lockfile; resolution still has to choose a version and emit the entry.catalog:default/catalog:react18throughpnpm-workspace.yaml'scatalog(s):, then dispatch to npm. Snapshot the selections into the lockfile'scatalogs:block.workspace:*/workspace:^/workspace:~/workspace:1.2.3→link:lockfile entry + publish-time spec rewrite. Pacquet already loads workspaces (Add workspace support topacquet install --frozen-lockfilepacquet#431).Tier 3 — Resolution policies & constraints. Behaviors layered on top of the recursion that change which version wins or whether the install proceeds.
resolvePeers.ts(~1k LoC). Produces dep paths with peer hashes (foo@1.0.0(react@18.0.0)). Heaviest single port in this stage.pnpm.overridesfrompnpm-workspace.yaml. Apply atparseWantedDependencytime; record in the lockfile'soverrides:block.allowedDeprecatedVersions+ deprecation warning emission. Wire to the same NDJSONdeprecationchannel pnpm uses so@pnpm/cli.default-reporterformats them identically.blockExoticSubdeps. Reject git/tarball/file specs in transitive deps.patchedDependenciesresolution-side. Hash the patch and attachpkgIdWithPatchHashto dep paths so the installer picks the patched store entry.crates/lockfile/pkg_id_with_patch_hash.rsalready exists.allowNonAppliedPatches+verifyPatches.minimumReleaseAgeat resolve time. Today it gates global installs only; extend it so new transitive versions are also rejected, mirroring upstream's resolve-path enforcement.Tier 4 — Hooks & custom resolution. Both require running user JavaScript inside pacquet — design call needed.
.pnpmfile.cjs/pnpmfile.cjsexecution. Port thereadPackage,afterAllResolved,preResolution,filterLoghook surface fromhooks/pnpmfile. Implementation choice — embed a JS runtime (rquickjs / deno_core / boa) or shell out tonode— has its own parity, perf, and dependency-footprint trade-offs and is worth its own design issue before code lands.hooks.types.CustomResolver). Same JS-execution concern as pnpmfile.Tier 5 — Commands unlocked by resolution. Stage 1 only delivers
pacquet install --frozen-lockfile. Once resolution lands, each of these is a CLI surface port that follows the underlying capability.pacquet install(default, non-frozen).pacquet install --lockfile-only.pacquet add(incl.-D,--save-peer,--save-exact,--save-prefix).pacquet update(incl.--latest,updateMatching,-i).pacquet remove.pacquet outdated.pacquet why.Tier 6 — Consistency, polish, auxiliary. Close the parity gap once the structural work is in place.
pnpm-lock.yaml(key ordering, anchor reuse, no spurious churn).add/remove/update. PortupdateProjectManifest.ts: respectsave-prefix,save-exact,save-workspace-protocol.afterAllResolved/preResolutionlog-event parity so@pnpm/cli.default-reporterformats them identically.pnpm/test/install.ts,add.ts,update.ts,remove.ts,outdated.tsinto pacquet's test layout as each capability lands.Integration milestone (between Stage 2 and Stage 3)
Once resolution is solid, extend the Stage-1 → Stage-2 opt-in seam (
install-backend=pacquet/ N-API) to route everydaypnpm installcalls through pacquet end-to-end — not just--frozen-lockfile. Stage 3 then targets the non-install command surface (publish,audit,dlx,exec,run,store).Stage 3 — TBD
Stage 2 plan drafted by an agent (Claude Code, claude-opus-4-7).