Skip to content

feat(workspace): manage pnpm workspaces with Bit, on the workspace-root component - #10729

Open
davidfirst wants to merge 60 commits into
masterfrom
pnpm-vcs-port
Open

davidfirst wants to merge 60 commits into
masterfrom
pnpm-vcs-port

Conversation

@davidfirst

@davidfirst davidfirst commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Builds on #10675, ported onto the workspace-root component model (#10698). What changed from the original, and why:

Original Now
bit init --standalone --external-package-manager --default-scope <scope>, then bit pnpm sync bit pnpm init [--default-scope <scope>]
bit import --pnpm-vcs-root and bit switch --pnpm-vcs-bootstrap bit clone <root> [--lane]
Explicit file lists (useExplicitFiles) for the root and projects Root with rootDir: "." plus trackAllFiles
Topology stored in the tracker config (pnpmVcs.workspace) The root's versioned .bitmap, read through WorkspaceRootMain.listMembers()
Sync and the scripts env live in the tracker aspect A core teambit.workspace/pnpm-workspace aspect: it owns bit pnpm sync and the import handling (through an importer slot), and is itself the scripts env, so the env ships with bit and needs no install

Adopting a workspace: bit pnpm init. It creates a standalone workspace and runs the first sync, so no flags are needed. It does not change package.json, and it writes no AI agent or MCP files, because every file at the root goes into the root component. Re-run bit pnpm sync when projects are added, moved or deleted.

pnpm owns installation (pnpm/rfcs#33). bit pnpm init and bit pnpm sync add the "teambit.workspace/pnpm-workspace": {} entry to workspace.jsonc. Workspace.isPnpmWorkspace() reads only this entry. A pnpm-workspace.yaml file alone does not change how bit installs. workspace.jsonc is a file of the root component, so a clone or an import of the root brings the entry along. In such a workspace:

  • bit install stops and gives the pnpm command: pnpm install, or pnpm add <pkg> --filter <project>.
  • Import, merge, lane switch and other commands that install as one of their steps skip that step, and print run "pnpm install".
  • Bit never writes the dependencies into the root package.json.

bit status shows what a sync would change. It lists untracked projects, projects that left, moved projects (with the bit move command to run), and workspace: references to move to the catalog. These show under "workspace issues". They do not block a snap. They come from a new status slot, registerWorkspaceIssues.

Dropped on purpose

  • Toolchain profiles (engines.node / pnpm.vcs.profile checks on import): the current revision of RFC: Bit version control for pnpm workspaces pnpm/rfcs#33 removed them. The RFC keeps engines as package metadata for the tools that read it.
  • A project nested inside another project: the sync refuses it, because a component can't contain another one. A package.json at the root and in the project directories is supported.
  • The vcs: block in pnpm-workspace.yaml: the root component's .bitmap now holds that identity.

Kept: workspace: to catalog: at sync. The sync changes a workspace: reference between projects to catalog:, and writes the specifier to the default catalog (comments of pnpm-workspace.yaml stay). It refuses two different specifiers for one package, and a catalog entry that binds it otherwise. The snapped package.json then says catalog:, so an import into another workspace changes only that workspace's catalog, and the component is not modified. The import-side rewrite stays for components snapped before this change.

A workspace: catalog value needs pnpm 11.28.0 / 12.7.0 for the install and the pnpm -r order (pnpm/pnpm#15591), so the CircleCI e2e and bundle jobs install 12.7.0. The pnpm tasks run the pnpm the user's shell finds: bit's installs put the directory of its own node first on PATH, and a pnpm there hid the user's.

Sync and import keep pnpm-workspace.yaml consistent:

  • Once a project leaves the workspace, for example after a remove or a lane switch, a re-run of the sync binds its catalog entry to the exact version its dependents were snapped with. If there is no single version, the sync reports the entry.
  • Projects with the same package name get their directory as a suffix.
  • The import edits the yaml as a document, so the user's comments stay.
  • A snapped project whose directory is gone is marked removed. The sync reads the project's marker from the snapped version, because the snap takes it out of .bitmap. If the same package is now in an untracked directory, the sync stops and asks for bit move <old-dir> <new-dir>, so the component keeps its id and history.

A component that carries its own package.json (every synced project, where it is the main file):

  • In a workspace without trackAllFiles it keeps the file as source, so it imports into a regular bit workspace too.
  • Its capsule and published package.json take main, exports, bin, scripts and the rest from it. Bit still writes the name, the version, componentId and the dependencies, at their exact versions.

zkochan and others added 15 commits August 29, 2026 17:04
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… component

"bit pnpm sync" now tracks each pnpm project as a component and the root with rootDir ".", under
trackAllFiles, instead of explicit file lists and a topology in tracker config. Drops the
--pnpm-vcs-root/--pnpm-vcs-bootstrap flags, toolchain profiles, the vcs block in
pnpm-workspace.yaml, the manifest migration and the package-scripts env (CI runs the scripts
after "bit clone"). The import plan no longer rewrites package manifests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ionless packages

componentIdToPackageName now honors the dependency-resolver packageName config, so the linker and
the root-components linking agree on the name. A found package with no version (a private pnpm
package) is recorded as "*" instead of failing the dependency detection.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ce refs on import

Adds teambit.envs/pnpm-workspace-env, a plain-JS env on the empty env that compiles and builds
through the packages' own scripts, in a pnpm workspace rebuilt from the workspace-root component.
"bit pnpm sync" assigns it to projects with scripts (--env to override). On import, a "workspace:"
reference to a sibling the target workspace lacks becomes "catalog:", bound to its exact version.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Manage pnpm workspaces through workspace-root components

✨ Enhancement 🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Adds idempotent bit pnpm sync adoption through workspace-root components and versioned
 inventories.
• Preserves pnpm package identities, workspace links, catalog bindings, and snapped dependency
 metadata.
• Builds pnpm projects through native workspace scripts locally and in isolated environments.
Diagram

graph TD
  Sync["pnpm sync"] --> Inventory["Component inventory"] --> Graph["Dependency graph"] --> Snap["Snap metadata"]
  Import["Bit import"] --> Catalog["Catalog plan"] --> Inventory
  Inventory --> Env["Workspace env"] --> Pnpm["pnpm scripts"]
Loading
High-Level Assessment

The workspace-root model is the strongest approach because it makes the existing versioned .bitmap the authoritative inventory and avoids duplicating topology in tracker configuration. Explicit file lists, custom VCS manifest blocks, and adoption-time manifest migration were considered but would create parallel state or unexpectedly rewrite user-owned files; delegating script ordering to pnpm also preserves native workspace semantics.

Files changed (36) +2470 / -48

Enhancement (18) +1480 / -24
snap-cmd.tsAdd versioned JSON output for snap +73/-13

Add versioned JSON output for snap

• Separates snap execution from rendering and adds a schema-versioned JSON result containing batch, lane, component, removal, warning, and count data. Empty snap operations now return a stable JSON shape.

scopes/component/snapping/snap-cmd.ts

snapping.main.runtime.tsExpose snap batch identifiers in results +3/-1

Expose snap batch identifiers in results

• Carries the batch identifier returned by version creation into 'SnapResults' for reporting and automation.

scopes/component/snapping/snapping.main.runtime.ts

status-cmd.tsVersion status JSON response schemas +4/-0

Version status JSON response schemas

• Adds 'schemaVersion: 1' to both quick and full status JSON outputs for stable machine consumption.

scopes/component/status/status-cmd.ts

index.tsExpose pnpm synchronization and import APIs +2/-0

Expose pnpm synchronization and import APIs

• Exports pnpm workspace detection, import-plan creation and application, and their public result types from the tracker package.

scopes/component/tracker/index.ts

pnpm-vcs-sync.cmd.tsImplement pnpm workspace adoption and reconciliation +762/-0

Implement pnpm workspace adoption and reconciliation

• Adds 'bit pnpm sync' discovery, idempotent project and workspace-root tracking, environment assignment, stale-project removal, and schema-versioned output. It also tracks referenced catalog bindings and reconciles selective imports without broadly rewriting user manifests.

scopes/component/tracker/pnpm-vcs-sync.cmd.ts

tracker.main.runtime.tsRegister pnpm commands and catalog tracking +6/-1

Register pnpm commands and catalog tracking

• Registers 'bit pnpm' and its sync subcommand, then attaches an on-load hook that records catalog entries used by each component.

scopes/component/tracker/tracker.main.runtime.ts

dependency-resolver.main.runtime.tsSupport declared package names in graph calculation +4/-2

Support declared package names in graph calculation

• Extends variant and graph inputs with an optional package name and uses it instead of Bit's generated package identity when supplied.

scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts

index.jsExport the pnpm workspace environment +4/-0

Export the pnpm workspace environment

• Provides named and default CommonJS exports for the standalone pnpm workspace environment.

scopes/envs/pnpm-workspace-env/index.js

pnpm-script.task.jsRun package scripts across reconstructed pnpm workspaces +165/-0

Run package scripts across reconstructed pnpm workspaces

• Adds build, test, and lint tasks that reconstruct and install the complete versioned workspace before invoking pnpm recursively. Script outputs are copied back into component capsules as artifacts.

scopes/envs/pnpm-workspace-env/pnpm-script.task.js

pnpm-workspace-tree.jsReconstruct versioned pnpm workspace trees +202/-0

Reconstruct versioned pnpm workspace trees

• Loads workspace-root membership from local workspaces or scopes, validates member paths, writes complete trees, and signatures their contents for reuse.

scopes/envs/pnpm-workspace-env/pnpm-workspace-tree.js

pnpm-workspace.bit-env.jsDefine the package-script pnpm environment +31/-0

Define the package-script pnpm environment

• Introduces a dependency-free JavaScript environment whose compiler and build pipeline delegate build, test, and lint execution to pnpm workspace scripts.

scopes/envs/pnpm-workspace-env/pnpm-workspace.bit-env.js

pnpm-workspace.compiler.jsCompile components through ordered pnpm workspace builds +133/-0

Compile components through ordered pnpm workspace builds

• Runs the workspace build script once per source state, serializes concurrent compile requests, and copies each component's distribution output to its requested destination.

scopes/envs/pnpm-workspace-env/pnpm-workspace.compiler.js

utils.jsAdd pnpm process and filesystem helpers +34/-0

Add pnpm process and filesystem helpers

• Provides pnpm subprocess execution with captured output and a reusable asynchronous path-existence check.

scopes/envs/pnpm-workspace-env/utils.js

init-cmd.tsAdd explicit agent and MCP initialization opt-outs +5/-1

Add explicit agent and MCP initialization opt-outs

• Adds '--no-agent' and '--no-mcp' flags and forwards those choices to workspace initialization.

scopes/harmony/host-initializer/init-cmd.ts

import.cmd.tsExpose pnpm reconciliation plans in import JSON +6/-2

Expose pnpm reconciliation plans in import JSON

• Includes the computed pnpm import plan in JSON output when components were written into the workspace.

scopes/scope/importer/import.cmd.ts

importer.main.runtime.tsReconcile imports through pnpm workspace manifests +14/-2

Reconcile imports through pnpm workspace manifests

• Detects adopted pnpm workspaces and applies package and catalog reconciliation after imports. Non-pnpm workspaces retain the existing workspace configuration cleanup path.

scopes/scope/importer/importer.main.runtime.ts

workspace-root-data.tsCentralize versioned root bitmap parsing +15/-1

Centralize versioned root bitmap parsing

• Adds a reusable helper that locates and parses the '.bitmap' stored among workspace-root component files.

scopes/workspace/workspace-root/workspace-root-data.ts

workspace-root.main.runtime.tsExpose workspace-root component membership +17/-1

Expose workspace-root component membership

• Adds 'listMembers()' to return versioned bitmap entries while excluding the root component itself.

scopes/workspace/workspace-root/workspace-root.main.runtime.ts

Bug fix (6) +217 / -14
component-id-to-package-name.tsPreserve package names declared by pnpm projects +3/-0

Preserve package names declared by pnpm projects

• Uses the dependency-resolver 'packageName' configuration before generated package metadata, preserving package.json identities for adopted projects.

components/modules/component-package-name/component-id-to-package-name.ts

version-maker.tsPersist pnpm workspace links as component dependencies +128/-5

Persist pnpm workspace links as component dependencies

• Maps direct package-manager graph edges to exact Bit component dependencies using declared package names. Promoted dependencies update legacy lists and dependency-resolver data before flattened dependencies are calculated.

scopes/component/snapping/version-maker.ts

auto-detect-deps.tsResolve workspace packages through configured package names +42/-0

Resolve workspace packages through configured package names

• Builds a package-name-to-component map from bitmap configuration or tracked package.json files. Detected package imports are promoted to component dependencies while excluding the workspace root and self-dependencies.

scopes/dependencies/dependencies/dependencies-loader/auto-detect-deps.ts

missing-handler.tsAccept packages without declared versions +2/-2

Accept packages without declared versions

• Falls back to '*' instead of throwing when a resolved private pnpm workspace package has no version.

scopes/dependencies/dependencies/files-dependency-builder/missing-handler.ts

lockfile-deps-graph-converter.tsConvert pnpm workspace links into component graph nodes +33/-6

Convert pnpm workspace links into component graph nodes

• Preserves 'link:' and 'workspace:' references when their package names map to Bit components. It adds corresponding package attributes and graph nodes for later dependency promotion.

scopes/dependencies/pnpm/lockfile-deps-graph-converter.ts

pnpm.package-manager.tsRetain mapped pnpm workspace references +9/-1

Retain mapped pnpm workspace references

• Prevents cleanup from deleting 'link:' or 'workspace:' importer entries when they correspond to known Bit components.

scopes/dependencies/pnpm/pnpm.package-manager.ts

Refactor (2) +4 / -6
add-components.tsExport the workspace-root environment identifier +1/-1

Export the workspace-root environment identifier

• Exports the shared empty-environment constant so pnpm synchronization can configure projects without duplicating the identifier.

scopes/component/tracker/add-components.ts

clone.tsRead clone inventories through workspace-root data utilities +3/-5

Read clone inventories through workspace-root data utilities

• Reuses the centralized workspace-root bitmap reader when loading the component inventory needed for cloning.

scopes/workspace/workspace-root/clone.ts

Tests (8) +751 / -0
component-id-to-package-name.spec.tsTest dependency-resolver package-name precedence +17/-0

Test dependency-resolver package-name precedence

• Verifies default package-name derivation and confirms that a configured dependency-resolver package name takes precedence.

components/modules/component-package-name/component-id-to-package-name.spec.ts

add-harmony.e2e.tsExercise pnpm workspace compilation and builds end to end +58/-0

Exercise pnpm workspace compilation and builds end to end

• Creates a linked two-package pnpm workspace, adopts it with 'bit pnpm sync', and verifies compile, build, and test scripts run in workspace order.

e2e/harmony/add-harmony.e2e.ts

snapping.spec.tsTest snap batch and JSON result contracts +25/-0

Test snap batch and JSON result contracts

• Verifies batch identifiers are returned and validates populated and empty schema-versioned snap JSON payloads.

scopes/component/snapping/snapping.spec.ts

version-maker.spec.tsTest promotion of pnpm graph dependencies +113/-0

Test promotion of pnpm graph dependencies

• Covers direct workspace dependency promotion, lifecycle and optional metadata, snap-version conversion, and prerelease handling.

scopes/component/snapping/version-maker.spec.ts

pnpm-vcs-sync.spec.tsCover pnpm synchronization, imports, and catalogs +457/-0

Cover pnpm synchronization, imports, and catalogs

• Tests project discovery, stable identities, environment selection, removals, nested-project validation, selective import reconciliation, and component-specific catalog tracking.

scopes/component/tracker/pnpm-vcs-sync.spec.ts

missing-handler.spec.tsTest versionless private workspace package detection +36/-0

Test versionless private workspace package detection

• Verifies resolved packages use their manifest version when present and '*' when private workspace packages omit one.

scopes/dependencies/dependencies/files-dependency-builder/missing-handler.spec.ts

lockfile-deps-graph-converter.spec.tsTest workspace link preservation in pnpm graphs +25/-0

Test workspace link preservation in pnpm graphs

• Verifies a 'link:' importer dependency mapped to a workspace component remains a graph edge with component identity metadata.

scopes/dependencies/pnpm/lockfile-deps-graph-converter.spec.ts

workspace-root-data.spec.tsTest workspace-root bitmap inventory parsing +20/-0

Test workspace-root bitmap inventory parsing

• Verifies versioned bitmap entries are read from root component files and that missing bitmap files yield an empty inventory.

scopes/workspace/workspace-root/workspace-root-data.spec.ts

Documentation (1) +4 / -4
SKILL.mdNormalize pnpm engine documentation formatting +4/-4

Normalize pnpm engine documentation formatting

• Fixes indentation and JavaScript punctuation in the pnpm engine skill examples without changing their guidance.

.claude/skills/pnpm-engine/SKILL.md

Other (1) +14 / -0
.bitmapRegister the pnpm workspace environment component +14/-0

Register the pnpm workspace environment component

• Adds 'teambit.envs/pnpm-workspace-env' as a JavaScript component running on the empty environment without a compilation bootstrap.

.bitmap

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

qodo-free-for-open-source-projects Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (10) 📘 Rule violations (2) 📜 Skill insights (0)

⚠️ 5 lower-priority findings omitted to fit the comment size limit; re-run the review or view the findings in the Qodo portal.

Grey Divider


Action required

1. New workspace aspect omits typed config 📘 Rule violation ⚙ Maintainability
Description
PnpmWorkspaceMain.provider accepts only dependencies, and the main class has no typed
configuration constructor argument. When bit pnpm init or bit pnpm sync writes the aspect entry
to workspace.jsonc, future configuration fields have no established path into the runtime.
Code

scopes/workspace/pnpm-workspace/pnpm-workspace.main.runtime.ts[R176-178]

+  ]) {
+    const logger = loggerMain.createLogger(PnpmWorkspaceAspect.id);
+    const pnpmWorkspace = new PnpmWorkspaceMain(workspace, dependencyResolver, workspaceRoot, logger);
Evidence
Rule 5 requires typed aspect configuration to pass through the provider into the main class. The
provider has no second configuration argument, while sync writes an entry under the aspect ID.

CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider: CLAUDE.md: Pass Typed Aspect Configuration Through the Provider
scopes/workspace/pnpm-workspace/pnpm-workspace.main.runtime.ts[58-64]
scopes/workspace/pnpm-workspace/pnpm-workspace.main.runtime.ts[154-178]
scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[654-661]

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 new pnpm workspace aspect writes a workspace configuration entry but does not accept typed aspect configuration through its provider or inject it into the main class.
## Fix Focus Areas
- scopes/workspace/pnpm-workspace/pnpm-workspace.main.runtime.ts[58-64]
- scopes/workspace/pnpm-workspace/pnpm-workspace.main.runtime.ts[154-178]
## Recommended Fix
Define a TypeScript interface for the aspect configuration, accept it as the second provider argument, and pass it to the main class constructor.

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


2. Imported projects fail to build 🐞 Bug ≡ Correctness
Description
PnpmWorkspaceCompiler.transpileComponent() silently skips compilation without an ancestor
pnpm-workspace.yaml, while the project's build task requires a current workspace-root component.
An imported pnpm project retains this core environment in an ordinary Bit workspace, so compilation
produces no output and its build fails with no workspace root was found.
Code

scopes/workspace/pnpm-workspace/pnpm-workspace.compiler.ts[R65-66]

+    const workspaceDir = await findPnpmWorkspaceDir(componentDir);
+    if (!workspaceDir) return;
Evidence
Sync assigns the core environment to projects with scripts; the import callback does not replace it
in an ordinary workspace. The compiler then returns without building, and the registered task's
workspace tree source throws when no local root exists.

scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[676-711]
scopes/workspace/pnpm-workspace/pnpm-workspace.main.runtime.ts[119-126]
scopes/workspace/pnpm-workspace/pnpm-workspace.main.runtime.ts[174-183]
scopes/workspace/pnpm-workspace/pnpm-workspace.compiler.ts[59-67]
scopes/workspace/pnpm-workspace/pnpm-script.task.ts[50-67]
scopes/workspace/pnpm-workspace/pnpm-workspace-tree.ts[116-124]

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

## Issue description
An imported project retains the pnpm workspace environment, but an ordinary Bit workspace has neither the pnpm manifest required by compilation nor the workspace-root component required by the build task.
## Fix Focus Areas
- scopes/workspace/pnpm-workspace/pnpm-workspace.compiler.ts[59-67]
- scopes/workspace/pnpm-workspace/pnpm-workspace-tree.ts[116-124]
- scopes/workspace/pnpm-workspace/pnpm-workspace.main.runtime.ts[174-179]
## Recommended Fix
Provide a compilation and build path for imported pnpm projects when the current workspace is not their pnpm workspace. Reconstruct the versioned source workspace where needed rather than requiring a local pnpm manifest and root, and test an import into an ordinary Bit workspace.

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


3. Registry dependencies can change identity ✓ Resolved
Description
componentDependenciesFromGraph() trusts component metadata attached by package-name matching, and
_promoteGraphComponentDependencies() replaces the declared package dependency with that component
ID. When a registry package shares a workspace component’s name and its graph entry carries a
version, snapping records the workspace component instead of the resolved registry package.
Code

scopes/component/snapping/version-maker.ts[R406-408]

+      const graphDependencies = componentDependenciesFromGraph(graph);
+      for (const graphDependency of graphDependencies) {
+        if (component.id.isEqualWithoutVersion(graphDependency.id)) continue;
Evidence
The converter assigns component metadata by parsed package name without checking the package
resolution. The new promotion reads that metadata and deletes the original package dependency; the
registry-collision test demonstrates that a registry package can retain its own lockfile entry
despite sharing the workspace component’s name.

scopes/dependencies/pnpm/lockfile-deps-graph-converter.ts[300-309]
scopes/dependencies/pnpm/lockfile-deps-graph-converter.spec.ts[439-461]
scopes/component/snapping/version-maker.ts[109-125]
scopes/component/snapping/version-maker.ts[406-423]

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

## Issue description
A registry package can carry workspace-component metadata solely because its name matches a workspace component, then be promoted to that component during snapping.
## Fix Focus Areas
- scopes/dependencies/pnpm/lockfile-deps-graph-converter.ts[300-309]
- scopes/component/snapping/version-maker.ts[399-423]
## Recommended Fix
Preserve whether a direct dependency resolved through a workspace link and require that evidence before promoting it. Add a regression test with a same-named registry dependency whose graph package has a version.

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


View action required (2)
4. Existing projects lose package metadata ✓ Resolved
Description
trackPnpmProject() reuses an existing component ID without changing its main file to
package.json, although it sets that main file when tracking a new project. When bit pnpm sync
adopts a project already tracked with a different main file, the isolator skips its own manifest
fields, so capsule and published packages omit metadata such as exports, bin, and files.
Code

scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[R673-676]

+  const existingId = workspace.consumer.bitMap.getComponentIdByRootPath(project.rootDir);
+  const componentId =
+    existingId ??
+    (
Evidence
The existing-ID branch bypasses the tracker call that sets package.json as the main file. The
isolator reads and merges a component's own package metadata only when its main file is
package.json; the root sync path separately demonstrates how to update an existing bitmap main file.

scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[673-683]
scopes/component/isolator/isolator.main.runtime.ts[1546-1554]
scopes/component/isolator/isolator.main.runtime.ts[1854-1867]
scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[729-739]

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

## Issue description
Sync preserves an existing project's old main file, preventing its package.json metadata from reaching the isolated package.
## Fix Focus Areas
- scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[673-687]
- scopes/workspace/pnpm-workspace/pnpm-workspace-sync.spec.ts[115-130]
## Recommended Fix
Set an existing project's bitmap main file to package.json and mark the bitmap changed, as root tracking does when its main file changes. Add a test that begins with an already-tracked project whose main file differs.

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


5. Sync can remove another lane’s component ✓ Resolved
Description
findLeftProjects() scans every bitmap entry without checking whether it is available on the
current lane. When an off-lane project is absent from the current lane’s pnpm inventory,
removeLeftProjects() marks its shared bitmap entry removed.
Code

scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[R808-813]

+  const candidates = workspace.consumer.bitMap.components.filter(
+    (componentMap) =>
+      componentMap.rootDir !== WORKSPACE_ROOT_DIR &&
+      !projectRootDirs.has(componentMap.rootDir) &&
+      !componentMap.isRemoved()
+  );
Evidence
The bitmap retains entries across lanes and tracks their current-lane availability separately, but
the new filter does not use that flag before marking candidates removed.

components/legacy/bit-map/bit-map.ts[538-559]
components/legacy/bit-map/bit-map.ts[846-854]
scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[806-825]
scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[839-847]

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

## Issue description
Sync can mark a component belonging only to another lane as removed.
## Fix Focus Areas
- scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[806-825]
- scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[839-847]
## Recommended Fix
Filter left-project candidates by current-lane availability before determining which projects left; add a lane-switch regression test.

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



Remediation recommended

6. Imports change unrelated catalog entries 🐞 Bug ≡ Correctness ⭐ New
Description
applyPnpmImportPlan() changes every existing catalog entry whose package name matches an imported
component to workspace:*, without checking which catalog entries that component uses. Importing a
local package can therefore change a named catalog entry used by another project from its selected
version to the local workspace package.
Code

scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[R960-964]

+  Object.entries(catalogs).forEach(([catalogName, catalog]) => {
+    importedPackageNames.forEach((packageName) => {
+      if (!catalog[packageName]) return;
+      bind(catalogName, packageName, 'workspace:*');
+      workspaceBoundPackageNames.add(packageName);
Evidence
The import plan collects package names without catalog references, then visits every catalog and
rebinds any matching key. The catalog tests establish that default and named entries are distinct
references selected by manifests.

scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[945-970]
scopes/workspace/pnpm-workspace/pnpm-workspace-sync.spec.ts[859-875]

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

## Issue description
Import reconciliation changes existing catalog entries solely because their package name matches an imported component, including entries the component never references.
## Fix Focus Areas
- scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[945-970]
## Recommended Fix
Restrict rebinding to catalog entries used by imported manifests or otherwise explicitly identified for local rebinding. Preserve unrelated named-catalog entries, and add a test with the same package name in an unrelated catalog.

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


7. Former roots can retain their root marker 🐞 Bug ≡ Correctness ⭐ New
Description
markWorkspaceRoot() returns {} for a non-root component, but upsertExtensionData() merges that
empty object into any existing extension data instead of clearing isRoot. When a previously
snapped root is loaded as a member, code reading its consumer extensions can still classify it as a
workspace root.
Code

scopes/workspace/workspace-root/workspace-root.main.runtime.ts[136]

+  return consumerComponent.componentMap?.rootDir === WORKSPACE_ROOT_DIR ? { isRoot: true } : {};
Evidence
The loader takes the truthy {} through its upsert path, where Object.assign leaves the previous
isRoot value untouched. Root classification reads that value from the consumer extension.

scopes/workspace/workspace-root/workspace-root.main.runtime.ts[127-136]
scopes/workspace/workspace/workspace-component/workspace-component-loader.ts[1248-1260]
scopes/workspace/workspace/workspace-component/workspace-component-loader.ts[1269-1278]
scopes/workspace/workspace-root/workspace-root-data.ts[55-60]

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

## Issue description
Returning an empty object does not clear a previously stored workspace-root marker because the component loader merges extension data.
## Fix Focus Areas
- scopes/workspace/workspace-root/workspace-root.main.runtime.ts[134-136]
- scopes/workspace/workspace/workspace-component/workspace-component-loader.ts[1248-1260]
- scopes/workspace/workspace/workspace-component/workspace-component-loader.ts[1269-1278]
## Recommended Fix
Explicitly replace or clear the existing workspace-root extension data when the component is not the current root. Test loading a formerly snapped root after tracking it as a member.

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


8. Distinct workspace trees reuse stale builds 🐞 Bug ≡ Correctness ⭐ New
Description
treeSignature() hashes each path and its raw contents without encoding where one file ends and the
next begins. If one file's contents include the serialized path prefix of a following file, a
different tree can produce the same signature; prepareTree() then skips rewriting the tree and
reinstalling dependencies.
Code

scopes/workspace/pnpm-workspace/pnpm-workspace-tree.ts[235]

+      files.forEach((file) => hash.update(`${rootDir}/${file.relative}\0`).update(file.contents));
Evidence
For example, one tree with ./a containing x./b\0y hashes the same byte stream as a tree with
./a containing x and ./b containing y. An equal stored signature makes the task return
before writing or installing the second tree.

scopes/workspace/pnpm-workspace/pnpm-workspace-tree.ts[228-237]
scopes/workspace/pnpm-workspace/pnpm-script.task.ts[101-112]

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 tree signature's byte stream is ambiguous across file boundaries, allowing different sets of files to have the same cache key without a cryptographic collision.
## Fix Focus Areas
- scopes/workspace/pnpm-workspace/pnpm-workspace-tree.ts[228-237]
## Recommended Fix
Include an unambiguous content length or end-of-file framing for each path and contents pair before hashing. Add a regression test using one file whose contents contain another file's serialized path prefix.

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


View review recommended (8)
9. Switching back to a lane drops the workspace's own root 🐞 Bug ≡ Correctness ⭐ New
Description
filterForeignWorkspaceRoots decides whether a root is tracked using
consumer.bitmapIdsFromCurrentLaneIncludeRemoved, which only holds .bitmap entries available on
the lane you are switching from. If the workspace's own root component was first snapped on a lane,
it is lane-only and unavailable on main, so bit switch <lane> from main treats it as another
workspace's root, warns "use bit clone", and removes it from switchProps.ids/laneBitIds.
saveLanesData then calls bitMap.syncWithIds with that filtered list, so the root stays
unavailable and not marked onLanesOnly on its own lane.
Code

scopes/lanes/lanes/switch-lanes.ts[R176-178]

+  private async filterForeignWorkspaceRoots() {
+    const bitMapIds = this.consumer.bitmapIdsFromCurrentLaneIncludeRemoved;
+    const isTracked = (id: ComponentID) => Boolean(bitMapIds.searchWithoutVersion(id));
Evidence
getAllIdsAvailableOnLaneIncludeRemoved only includes entries where isAvailableOnCurrentLane is
true (bit-map.ts 538-561). As a result, a lane-only root entry that exists in .bitmap gets
isTracked === false. excludeForeignWorkspaceRoots then excludes any versioned id that is not
tracked and is a workspace root. After that, syncWithIds(switchProps.ids, laneBitIds) sets
isAvailableOnCurrentLane = ids.hasWithoutVersion(id) for every entry (bit-map.ts 848-855), which
leaves the excluded root unavailable after the switch.

components/legacy/bit-map/bit-map.ts[538-561]
components/legacy/bit-map/bit-map.ts[848-855]
scopes/lanes/lanes/switch-lanes.ts[62-72]
scopes/lanes/lanes/switch-lanes.ts[176-196]
scopes/lanes/lanes/switch-lanes.ts[270-280]

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

## Issue description
`filterForeignWorkspaceRoots` decides whether a root is tracked using `consumer.bitmapIdsFromCurrentLaneIncludeRemoved`. That list leaves out `.bitmap` entries that are not available on the current lane. So the workspace's own lane-only root gets treated as a foreign root and is excluded when switching to its lane.

## Fix Focus Areas
- scopes/lanes/lanes/switch-lanes.ts[176-178]

## Recommended Fix
Base `isTracked` on every `.bitmap` entry, whatever lane it belongs to. For example: `const isTracked = (id) => Boolean(this.consumer.bitMap.getBitmapEntryIfExist(id, { ignoreVersion: true }))`, or use `this.consumer.bitMap.getAllBitIdsFromAllLanes().searchWithoutVersion(id)`. This way any root this workspace already tracks is never filtered out. Add a unit or e2e case: snap the root on a lane, switch to main, then switch back, and check that the root is still available.

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


10. Catalog checks repeat unit coverage 📘 Rule violation ➹ Performance
Description
The new should refer to the sibling by "catalog:" end-to-end test repeats the dependency rewrite
and catalog-binding assertions already in pnpm-workspace-sync.spec.ts. It runs after the same
setup used by the build tests, adding a separate test case without exercising another command or
integration path.
Code

e2e/harmony/add-harmony.e2e.ts[R661-664]

+    it('should refer to the sibling by "catalog:", with the catalog binding it to the workspace', () => {
+      const appManifest = helper.fs.readJsonFile('packages/app/package.json');
+      expect(appManifest.dependencies).to.deep.equal({ '@acme/math': 'catalog:' });
+      expect(helper.fs.readFile('pnpm-workspace.yaml')).to.have.string('"@acme/math": workspace:*');
Evidence
The new end-to-end case checks that the app dependency becomes catalog: and the catalog binds it
to workspace:*. The focused sync spec already checks both results, while the end-to-end case adds
no command beyond the shared setup.

CLAUDE.md: Prefer Focused Unit Tests and Minimize E2E Test Cost: CLAUDE.md: Prefer Focused Unit Tests and Minimize E2E Test Cost: CLAUDE.md: Prefer Focused Unit Tests and Minimize E2E Test Cost: CLAUDE.md: Prefer Focused Unit Tests and Minimize E2E Test Cost
e2e/harmony/add-harmony.e2e.ts[650-665]
scopes/workspace/pnpm-workspace/pnpm-workspace-sync.spec.ts[365-375]

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 new end-to-end catalog assertions duplicate focused spec coverage and add a separate test case to the CI suite.
## Fix Focus Areas
- e2e/harmony/add-harmony.e2e.ts[661-665]
## Recommended Fix
Remove the separate catalog assertion test. Keep the focused spec's manifest checks and the end-to-end build tests that use the workspace.

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


11. Same-size edits can retain stale builds 🐞 Bug ≡ Correctness
Description
sourceSignature() identifies each source using only its path, byte size and filesystem
modification time, and buildIfSourcesChanged() skips the build when that signature matches. A
same-size edit made within one timestamp interval, or with its modification time preserved, leaves
the signature unchanged and reuses the previous build output.
Code

scopes/workspace/pnpm-workspace/pnpm-workspace.compiler.ts[R159-160]

+        const stat = await fs.stat(fullPath);
+        entries.push(`${path.relative(workspaceDir, fullPath)}:${stat.size}:${stat.mtimeMs}`);
Evidence
An equal signature immediately bypasses pnpm run build. The signature stores no content-derived
information, so distinct contents with equal size and modification time produce the same cache key.

scopes/workspace/pnpm-workspace/pnpm-workspace.compiler.ts[107-115]
scopes/workspace/pnpm-workspace/pnpm-workspace.compiler.ts[144-165]

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 build cache cannot distinguish source edits that retain both file size and modification time.
## Fix Focus Areas
- scopes/workspace/pnpm-workspace/pnpm-workspace.compiler.ts[107-110]
- scopes/workspace/pnpm-workspace/pnpm-workspace.compiler.ts[144-165]
## Recommended Fix
Include source contents in a collision-resistant signature, or use another cache key that reliably changes for every source edit. Test a same-size edit with preserved modification time.

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


12. A failed sync leaves projects partly tracked 🐞 Bug ☼ Reliability
Description
syncPnpmWorkspace() edits the catalog and workspace configuration and tracks projects before
trackPnpmWorkspaceRoot() reads the root package.json. On a first sync with a malformed root
manifest, that read throws after the earlier files and component entries have changed, leaving the
workspace only partly synchronized.
Code

scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[R277-281]

+  for (const project of projects) {
+    const componentId = await trackPnpmProject(workspace, tracker, project, envResolver);
+    components.push(syncedComponent(workspace, componentId, project.rootDir));
+  }
+  const rootId = await trackPnpmWorkspaceRoot(workspace, tracker);
Evidence
Catalog migration, configuration writes, and project tracking precede root tracking. For a
previously untracked root, root tracking then calls the manifest reader, which throws on invalid
JSON; the validation therefore occurs after the earlier changes.

scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[251-281]
scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[727-750]
scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[1041-1047]

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

## Issue description
A malformed root package.json is detected only after sync has changed the catalog, configuration, and tracked projects.
## Fix Focus Areas
- scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[251-281]
- scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[727-750]
## Recommended Fix
Read and validate the root manifest during sync preflight, before any catalog or configuration writes or component tracking. Pass the validated root information to root tracking and test that an invalid manifest leaves those files and entries unchanged.

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


13. Nested projects get a broken sync hint 🐞 Bug ≡ Correctness
Description
findPnpmWorkspaceDrift() discovers projects but does not run the nested-project validation used by
syncPnpmWorkspace(). When one pnpm project sits inside another, bit status tells the user to run
sync to track it, but sync rejects that layout before making changes.
Code

scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[606]

+    const projects = await discoverPnpmProjects(workspace.path, workspaceManifest.packages || []);
Evidence
Sync validates the discovered inventory and rejects nesting, whereas drift detection uses that
inventory without the validation and produces the generic untracked-project instruction.

scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[250-259]
scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[553-560]
scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[598-626]

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

## Issue description
Status recommends syncing nested pnpm projects even though sync rejects them.
## Fix Focus Areas
- scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[598-640]
## Recommended Fix
Run the same nested-project validation during drift detection and report its failure rather than presenting the nested project as one sync can add.

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


14. Status loads strip saved docs data 🐞 Bug ≡ Correctness
Description
executeLoadSlot() replaces an existing aspect entry's data with {} whenever its on-load hook
returns nothing, including when the hook was deliberately skipped. bit status disables docs and
compositions loading, so those hooks return undefined and the loaded components lose their
existing aspect data for the rest of that load.
Code

scopes/workspace/workspace/workspace-component/workspace-component-loader.ts[R1254-1255]

+        const existingEntry = component.state.aspects.find(aspectId);
+        if (existingEntry) existingEntry.data = {};
Evidence
The loader initially constructs the aspect list from component extensions, status requests both
hooks be skipped, and each hook returns undefined for that request; the new branch then overwrites
any existing entry's data.

scopes/workspace/workspace/workspace-component/workspace-component-loader.ts[1083-1099]
scopes/component/status/status.main.runtime.ts[99-109]
scopes/docs/docs/docs.main.runtime.ts[218-224]
scopes/compositions/compositions/compositions.main.runtime.ts[140-146]
scopes/workspace/workspace/workspace-component/workspace-component-loader.ts[1246-1256]

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

## Issue description
Skipping docs or compositions computation currently clears their existing data on the loaded component.
## Fix Focus Areas
- scopes/workspace/workspace/workspace-component/workspace-component-loader.ts[1246-1259]
- scopes/component/status/status.main.runtime.ts[103-109]
## Recommended Fix
Distinguish an on-load hook skipped by load options from a hook that computed no data. Leave the existing aspect data intact for skipped hooks while retaining the intended stale-data clearing behavior for computed absence.

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


15. Status hides unresolved catalog packages 🐞 Bug ≡ Correctness
Description
findPnpmWorkspaceDrift() checks projects that left and references awaiting migration, but does not
check catalog entries still bound to workspace: after sync. If a referenced project leaves before
its dependent has been snapped, sync cannot choose a version and leaves that binding unresolved;
once the departed component is marked removed, status reports no issue even though pnpm cannot
resolve the package.
Code

scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[R628-636]

+    try {
+      [...planCatalogMigration(projects, workspaceManifest).bindings.keys()].sort().forEach((packageName) => {
+        issues.push(`a project refers to ${packageName} by "workspace:", ${runSync} to refer to it by "catalog:"`);
+      });
+    } catch (err: any) {
+      // a conflict the sync stops at, the other issues are still worth listing
+      issues.push(`"bit pnpm sync" would fail: ${err.message}`);
+    }
+    return issues;
Evidence
Sync explicitly retains an unbound workspace:* catalog entry when no snapped dependency version
exists. The status checker does not inspect those bindings, and its departed-project check excludes
components already marked removed, so a later status has no remaining issue to report.

scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[329-375]
scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[598-641]
scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[806-825]
scopes/workspace/pnpm-workspace/pnpm-workspace-sync.spec.ts[425-435]

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

## Issue description
After sync leaves a catalog entry bound to a departed workspace package, status can report no issue even though the package remains unresolved.
## Fix Focus Areas
- scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[598-641]
- scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[329-375]
## Recommended Fix
Have drift detection inspect referenced catalog bindings for packages absent from the current project inventory. Report entries that still require a version, including after the departed component has been marked removed, and cover that post-sync state in a test.

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


16. Failed imports bypass workspace cleanup ✓ Resolved
Description
ImporterMain.import() awaits the new onComponentsWritten handlers before removing imported
packages from workspace configuration or calling consumer.onDestroy('import'). If a handler
rejects while reconciling the pnpm manifest, components have already been written but both cleanup
steps are skipped.
Code

scopes/scope/importer/importer.main.runtime.ts[R95-97]

+      const outcomes = await Promise.all(
+        this.onComponentsWrittenSlot.values().map((handler) => handler(writtenComponents))
+      );
Evidence
The component writer completes before the new handler invocation. The registered pnpm handler
performs manifest operations that can fail, while the two cleanup calls follow the unprotected
await.

scopes/scope/importer/import-components.ts[846-861]
scopes/scope/importer/importer.main.runtime.ts[90-105]
scopes/workspace/pnpm-workspace/pnpm-workspace.main.runtime.ts[120-127]

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

## Issue description
A post-write import handler can throw after components are written, preventing workspace-config cleanup and consumer destruction.
## Fix Focus Areas
- scopes/scope/importer/importer.main.runtime.ts[93-105]
## Recommended Fix
Protect the post-write phase so consumer destruction runs on handler failure, and ensure workspace-config cleanup is performed or the failure leaves a clearly recoverable state. Preserve the handler error for the caller.

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


Grey Divider

Context sources
Review mode: Auto: 🧠 Deep: Large cross-cutting feature spans many independent, defect-prone code paths.

Grey Divider

Tip of the day
💡 Did you know, you can route each severity your way: inline, summary, both, or drop

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

…nd clone

In a pnpm-adopted workspace, the component writer and bit clone no longer write dependencies into
the root package.json or run bit's install and compile, which would also run the packages' build
scripts. They hint to run "pnpm install" instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread .claude/skills/pnpm-engine/SKILL.md Resolved
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 920cefc

davidfirst and others added 3 commits September 23, 2026 17:15
- mark each synced project in its tracker config, so a later sync removes only its own components
  (nameless ones included) and can move one from the env it assigned to another --env
- drop a package name the package.json no longer gives
- read the default catalog by the same rule as import writes it
- clear a capsule's output dirs before copying a new build's output
- list only removals and a summary in the sync report, with the error symbol for removals
- skip schemaVersion in the e2e clean-status check

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

…s load as ESM

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

…napped with, one install hint on clone

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

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

Copy link
Copy Markdown

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

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

Copy link
Copy Markdown

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

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 04b07b3

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

Copy link
Copy Markdown

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

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 6373fcf

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

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

Sorry, something went wrong

We weren't able to complete the code review on our side. Please try again manually by commenting /agentic_review on this PR.

Grey Divider

Qodo Logo

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 24aaca1

Comment on lines +661 to +664
it('should refer to the sibling by "catalog:", with the catalog binding it to the workspace', () => {
const appManifest = helper.fs.readJsonFile('packages/app/package.json');
expect(appManifest.dependencies).to.deep.equal({ '@acme/math': 'catalog:' });
expect(helper.fs.readFile('pnpm-workspace.yaml')).to.have.string('"@acme/math": workspace:*');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

6. Catalog checks repeat unit coverage 📘 Rule violation ➹ Performance

The new should refer to the sibling by "catalog:" end-to-end test repeats the dependency rewrite
and catalog-binding assertions already in pnpm-workspace-sync.spec.ts. It runs after the same
setup used by the build tests, adding a separate test case without exercising another command or
integration path.
Agent Prompt
## Issue description
The new end-to-end catalog assertions duplicate focused spec coverage and add a separate test case to the CI suite.

## Fix Focus Areas
- e2e/harmony/add-harmony.e2e.ts[661-665]

## Recommended Fix
Remove the separate catalog assertion test. Keep the focused spec's manifest checks and the end-to-end build tests that use the workspace.

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

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 0c9793a

Comment on lines +960 to +964
Object.entries(catalogs).forEach(([catalogName, catalog]) => {
importedPackageNames.forEach((packageName) => {
if (!catalog[packageName]) return;
bind(catalogName, packageName, 'workspace:*');
workspaceBoundPackageNames.add(packageName);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

6. Imports change unrelated catalog entries 🐞 Bug ≡ Correctness

applyPnpmImportPlan() changes every existing catalog entry whose package name matches an imported
component to workspace:*, without checking which catalog entries that component uses. Importing a
local package can therefore change a named catalog entry used by another project from its selected
version to the local workspace package.
Agent Prompt
## Issue description
Import reconciliation changes existing catalog entries solely because their package name matches an imported component, including entries the component never references.
## Fix Focus Areas
- scopes/workspace/pnpm-workspace/pnpm-workspace-sync.ts[945-970]
## Recommended Fix
Restrict rebinding to catalog entries used by imported manifests or otherwise explicitly identified for local rebinding. Preserve unrelated named-catalog entries, and add a test with the same package name in an unrelated catalog.

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

async function markWorkspaceRoot(component: Component): Promise<WorkspaceRootData> {
const consumerComponent = component.state._consumer as ConsumerComponent;
return consumerComponent.componentMap?.rootDir === WORKSPACE_ROOT_DIR ? { isRoot: true } : undefined;
return consumerComponent.componentMap?.rootDir === WORKSPACE_ROOT_DIR ? { isRoot: true } : {};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

7. Former roots can retain their root marker 🐞 Bug ≡ Correctness

markWorkspaceRoot() returns {} for a non-root component, but upsertExtensionData() merges that
empty object into any existing extension data instead of clearing isRoot. When a previously
snapped root is loaded as a member, code reading its consumer extensions can still classify it as a
workspace root.
Agent Prompt
## Issue description
Returning an empty object does not clear a previously stored workspace-root marker because the component loader merges extension data.
## Fix Focus Areas
- scopes/workspace/workspace-root/workspace-root.main.runtime.ts[134-136]
- scopes/workspace/workspace/workspace-component/workspace-component-loader.ts[1248-1260]
- scopes/workspace/workspace/workspace-component/workspace-component-loader.ts[1269-1278]
## Recommended Fix
Explicitly replace or clear the existing workspace-root extension data when the component is not the current root. Test loading a formerly snapped root after tracking it as a member.

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

.sort(([dirA], [dirB]) => dirA.localeCompare(dirB))
.forEach(([rootDir, component]) => {
const files = [...component.filesystem.files].sort((a, b) => a.relative.localeCompare(b.relative));
files.forEach((file) => hash.update(`${rootDir}/${file.relative}\0`).update(file.contents));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

8. Distinct workspace trees reuse stale builds 🐞 Bug ≡ Correctness

treeSignature() hashes each path and its raw contents without encoding where one file ends and the
next begins. If one file's contents include the serialized path prefix of a following file, a
different tree can produce the same signature; prepareTree() then skips rewriting the tree and
reinstalling dependencies.
Agent Prompt
## Issue description
The tree signature's byte stream is ambiguous across file boundaries, allowing different sets of files to have the same cache key without a cryptographic collision.
## Fix Focus Areas
- scopes/workspace/pnpm-workspace/pnpm-workspace-tree.ts[228-237]
## Recommended Fix
Include an unambiguous content length or end-of-file framing for each path and contents pair before hashing. Add a regression test using one file whose contents contain another file's serialized path prefix.

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

Comment on lines +176 to +178
private async filterForeignWorkspaceRoots() {
const bitMapIds = this.consumer.bitmapIdsFromCurrentLaneIncludeRemoved;
const isTracked = (id: ComponentID) => Boolean(bitMapIds.searchWithoutVersion(id));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

9. Switching back to a lane drops the workspace's own root 🐞 Bug ≡ Correctness

filterForeignWorkspaceRoots decides whether a root is tracked using
consumer.bitmapIdsFromCurrentLaneIncludeRemoved, which only holds .bitmap entries available on
the lane you are switching from. If the workspace's own root component was first snapped on a lane,
it is lane-only and unavailable on main, so bit switch <lane> from main treats it as another
workspace's root, warns "use bit clone", and removes it from switchProps.ids/laneBitIds.
saveLanesData then calls bitMap.syncWithIds with that filtered list, so the root stays
unavailable and not marked onLanesOnly on its own lane.
Agent Prompt
## Issue description
`filterForeignWorkspaceRoots` decides whether a root is tracked using `consumer.bitmapIdsFromCurrentLaneIncludeRemoved`. That list leaves out `.bitmap` entries that are not available on the current lane. So the workspace's own lane-only root gets treated as a foreign root and is excluded when switching to its lane.

## Fix Focus Areas
- scopes/lanes/lanes/switch-lanes.ts[176-178]

## Recommended Fix
Base `isTracked` on every `.bitmap` entry, whatever lane it belongs to. For example: `const isTracked = (id) => Boolean(this.consumer.bitMap.getBitmapEntryIfExist(id, { ignoreVersion: true }))`, or use `this.consumer.bitMap.getAllBitIdsFromAllLanes().searchWithoutVersion(id)`. This way any root this workspace already tracks is never filtered out. Add a unit or e2e case: snap the root on a lane, switch to main, then switch back, and check that the root is still available.

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

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 8c6255f

This branch has not been deployed

No deployments
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