Skip to content

Fix npm store copies missed by agent apply and vex (#601, #603) - #605

Merged
Mikola Lysenko (mikolalysenko) merged 8 commits into
mainfrom
agent/fix-npm-store-copy-enumeration
Oct 5, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 8 commits into
mainfrom
agent/fix-npm-store-copy-enumeration

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Final-head CI is complete: 452 successful checks, 6 skipped; no failures or pending checks. Bugbot passed, there are no unresolved review threads, and the PR is mergeable.

Fixes #601 and #603. In pnpm, vlt, Bun and Deno layouts, agent apply, rollback and vex must discover every relevant physical copy. Previously, finding a normal installation could hide a copy bundled inside another store entry; agent VEX also omitted peer variants that apply already patched. VEX could therefore attest a package while a consumed copy remained unpatched.

The resolver now visits the owning package's bundled node_modules even when its store entry is skipped by the pending-name filter. Agent VEX uses the shared npm store-variant expansion so an unpatched bundled or peer copy prevents attestation. CLI_CONTRACT.md and CHANGELOG.md describe the every-copy behavior.

Review corrections preserve that coverage while bounding traversal and repeated work:

  • Each resolver pass tracks canonical node_modules directories separately for importer and store-entry mode. Cyclic bundled links terminate; root-first path choices, legitimate linked copies, and the two different link policies are retained.
  • Aggregate peer expansion enumerates each canonical store once per layout, package name and version. Every input still contributes its lexical, canonical and relocated candidate stores, so a later alias can reveal another store. The cache exists only for that aggregate call; standalone apply/rollback discovery gets fresh state.
  • Hosted VEX reuses the expanded installed set, expands new alias paths before canonical merging, and continues to expand identity-fallback paths when the installed entry is empty.

Validation on b92456b3:

  • 228 focused tests passed: 80 npm core unit tests, 85 crawler integration tests, 10 hosted-copy unit tests, 5 multicopy apply/rollback tests, 18 agent VEX tests, 28 hosted VEX tests, and 2 independent reviewer controls. These cover the original bundled/peer-copy regressions as well as alias, fallback, scope and linked-tree behavior.
  • Deterministic regressions fail before the correction and pass after it: an eight-copy transitive resolver result scans its store once, mixed identities and alias stores retain the complete copy set, and hosted verification does not expand already-expanded installed copies again.
  • The linked-directory reproducer exceeded a two-second process bound on the original PR head and now resolves in about 1.44 ms. A bounded 128-copy expanded-input helper probe dropped from about 6.24 s to 224 ms. These are local component measurements, not whole-command benchmarks or portable timing guarantees.
  • Targeted formatting and diff checks passed. Clippy passed for both libraries with unused_variables allowed for the existing macOS Python-crawler warning. The final committed files match the tested hashes; independent source/evidence review found no remaining issue, and the commit merges cleanly with main 045d7ec7.
  • Full CI, compatibility workflows, benchmarks, and Bugbot completed successfully on the corrected commit b92456b3. A Windows Bun 1.2.23 patch-detail API timeout was retried once at the failed-job level; all 49 cases passed on the retry, including the expected workspace refusal.

#599, concerning orphaned Bun store entries, remains a separate reachability change.


Note

Medium Risk
Changes npm install discovery and VEX copy sets for security-sensitive apply/verify paths; bounded traversal and deduplication reduce performance and infinite-loop risk but behavior shifts when multiple physical copies exist.

Overview
Fixes #601 and #603 so agent apply, rollback, and vex treat every physical npm copy that can actually run—not only the “normal” importer-linked install.

The npm resolver now walks bundled node_modules inside skipped pnpm/vlt/Bun/Deno store entries when the same name@version was already found elsewhere, with cycle-safe traversal so bundled trees that link back to ancestors do not hang or drop legitimate copies.

with_store_peer_variant_copies centralizes peer/index/alias store expansion (with one scan per store identity per batch). Manifest copy lookup applies it for npm; hosted vex reuses already-expanded installed paths and only expands new alias paths, avoiding redundant store rescans.

Docs (CHANGELOG, CLI_CONTRACT) state that agent verification must hash every crawler copy, including peer variants and bundled store copies. Regression tests cover apply/rollback on bundled layouts, agent VEX omission when any store copy is pristine, and hosted-merge behavior.

Reviewed by Cursor Bugbot for commit b92456b. Configure here.

Assisted-by: Claude Code:claude-opus-5-5
A copy of a package bundled inside another package's pnpm/vlt store
entry is left unpatched when the package is also installed normally
(#601), and agent-mode vex never hashes store peer-variant copies such
as a Deno _1 copy (#603). These tests fail on main.

Assisted-by: Claude Code:claude-opus-5-5
Agent-mode apply and rollback now reach a copy bundled inside another
package's pnpm, vlt, Bun or Deno store entry even when the same
name@version is installed normally: the resolver probes a skipped
store entry's bundled tree (one stat per entry). Agent-mode vex now
checks the store peer-variant copies apply patches, so one unpatched
copy omits the purl instead of producing a false not_affected.

Fixes #601, #603

Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 2, 2026 20:51
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor 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.

Stale Bugbot comment from a previous run.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 2, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[burn-down agent] Labeled Ready for review at a513449c43d3f82f1f7da8fa0a92e15a6dafe7f4.

  • CI: 452/458 check runs green on this head (6 skipped by path/matrix filters), 0 failing.
  • Bugbot: reviewed this exact head, no findings; no unresolved review threads.
  • Mergeable, up to date with main @ 045d7ec.
  • Reviewer focus: npm_crawler.rs per-purl copy enumeration now including store copies (bundled/peer variants), and the matching vex every-copy rule in vex_consumed.rs / CLI_CONTRACT.

Generated by Claude Code

@mikolalysenko

Mikola Lysenko (mikolalysenko) commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator Author

Review updated for b92456b37893a09fad16991058ba6aa0d5238bf5: Ready to merge as-is from this review. Final-head CI is complete: 452 successful checks, 6 skipped; no failures or pending checks. Bugbot passed, there are no unresolved review threads, and the PR is mergeable.

The correction bounds repeated npm store scans and bundled-directory cycles while preserving every-copy coverage:

  • Hosted VEX reuses the expanded installed set and expands only new aliases, with identity fallback still covered. The shared helper enumerates each store/layout/name/version once per aggregate call, including transitive-only resolver results; aliases can still expose different stores.
  • Resolver visits are deduplicated by canonical directory and importer/store mode per pass. Cycles terminate while legitimate linked copies and root-first paths remain discoverable.

All 228 focused tests passed, including multicopy apply/rollback, agent and hosted VEX, crawler oracles, and the new work-count/cycle regressions. The original cycle reproducer exceeded two seconds and now resolves in about 1.44 ms. The 128-copy expanded-input helper measurement improved from about 6.24 s to 224 ms; these are component timings, not whole-command benchmarks. Deterministic tests establish the store-scan bound.

The committed files match the tested source. Independent review found no remaining actionable issue; formatting, diff checks, and targeted clippy passed (with the existing macOS unused_variables allowance). The corrected commit merges cleanly with current main.

No remaining code finding from this review. The Ready label has been restored after all checks completed on the corrected commit.

CI note: the initial Windows Bun 1.2.23 job failed because its patch-detail API call timed out before the expected refusal. Artifact 11256446880, captures/1.2.23-workspace-vendored/cli-output.json, records all 1 patch-detail queries failed and operation timed out; all no-mutation checks passed. One targeted job retry passed all 49 cases with the same binary, the correct refusal, and unchanged project files.

@mikolalysenko Mikola Lysenko (mikolalysenko) removed the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 2, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Confirmed on a513449: find_manifest_package_copies_reusing already applies with_store_peer_variant_copies to every npm purl, and hosted_consumed_copies (vex_consumed.rs:112) then runs it again over that expanded list. The second pass is only needed for the paths the hosted step adds itself: npm_alias_copies_reusing and npm_identity_fallback_reusing.

You've said you're preparing the correction on this branch, so I'm not pushing a competing one. The fix I'd propose expands only those additions and leaves the installed paths as they are:

for purl in shared.values().flatten() {
    let installed_paths = installed.get(purl).cloned().unwrap_or_default();
    let mut paths = all.remove(purl).unwrap_or_default();
    paths.extend(aliases.remove(purl).unwrap_or_default());
    if npm.contains(&purl) {
        // `installed` is already store-variant expanded (#603); expand only
        // what the alias walk and identity fallback added here.
        let added: Vec<PathBuf> = paths
            .into_iter()
            .filter(|p| !installed_paths.contains(p))
            .collect();
        let mut merged = installed_paths;
        for p in with_store_peer_variant_copies(added).await {
            if !merged.contains(&p) {
                merged.push(p);
            }
        }
        paths = merged;
    }
    // ...
}

This keeps every-copy verification for agent records, and aliased or fallback copies still get their variants. For a purl with no alias or fallback copies, the store scan count goes back to one. The existing e2e_vex store-copy cases and the hosted alias and fallback tests in vex_consumed.rs should cover it.


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Cursor (@cursor) review

Both review findings are corrected in b92456b37893a09fad16991058ba6aa0d5238bf5: bounded bundled-directory traversal and store expansion reuse. Please review this updated head.

@cursor cursor 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit b92456b. Configure here.

@mikolalysenko

Mikola Lysenko (mikolalysenko) commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator Author

Update (23:20 UTC): the Bun patch compatibility workflow on b92456b passed on a re-run (attempt 2), including native (windows-latest, 1.2.23). Every workflow on this head is now green and Bugbot is clean. That takes back my "probably caused by this PR" below: the failure did not reproduce on the same commit.

It is still unexplained. If workspace vendored fails on Windows again, the refusal codes in that run's bun-results-windows-* artifact (captures/**/workspace*vendored*/cli-output.json) are what to look at first.

Original report

CI on b92456b: Bun patch compatibility / native (windows-latest, 1.2.23) failed on attempt 1 (job). One scenario out of 49 failed: 1.2.23 workspace vendored FAIL ['refusalCodesExact']. The same job passed 49/49 on a513449, and Ubuntu with Bun 1.2.23 passed on this head. I suspected the Windows canonical-path handling in the new visited set. I couldn't download the artifact from my sandbox to check the actual codes.


Generated by Claude Code

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 2, 2026
@mikolalysenko Mikola Lysenko (mikolalysenko) removed the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 5, 2026
Release notes are written when a release is cut, from the merged PR
log and the code, so PRs no longer edit CHANGELOG.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Resolves conflicts with main's npm alias (#738), first-party link
(#634) and pnpm store (#698) changes:

- CLI_CONTRACT.md: keep main's hosted row and this PR's agent row.
- vex_consumed.rs: drop aliases the installed lookup already found
  (main), then store-expand only the new ones (this PR), so already
  expanded copies are not scanned again.
- Tests: keep both sides' new multicopy and e2e_vex regressions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174mrknEY9ge42c94RNRRBx
@mikolalysenko
Mikola Lysenko (mikolalysenko) merged commit 4646693 into main Oct 5, 2026
60 of 64 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-npm-store-copy-enumeration branch October 5, 2026 11:56
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 5, 2026
Main is red since #605 (4646693): two commands::vex_consumed tests
assume the name-keyed resolver never returns npm-aliased copies, and
#605 taught it to find them. This fails socket-patch-cli --lib in
coverage and test on every PR. Port #851's test-only fix so this PR
can go green; it no-ops once #851 lands.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8YeCUhdg2tKdqyyY7z3sV
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 5, 2026
main has been red since #605 taught the name-keyed resolver to return
npm-aliased copies, which broke two vex_consumed tests added by #738.
Port #851's test update so this PR's CI goes green; it no-ops once
#851 lands on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uQyhodCtdJGrKaD7AAV1n
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 5, 2026
#605 taught the name-keyed npm resolver to probe bundled store
trees, so it now finds aliased copies (node_modules/lp) and a nested
host's store peers itself. Two vex_consumed tests from #738 assumed
that set never held aliases, so main's CI went red after both merged.

The tests now feed the alias-free set explicitly to keep covering
alias expansion, and also check the resolver's own set reaches the
same copies with no duplicates. No production code changes.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 40dac07)
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 5, 2026
main is red: since the store-copy change (#605) the npm resolver
already returns alias and nested-store copies, so two vex_consumed
tests that assumed an alias-free set fail on main and on this
branch. Same change as #851; it no-ops once main carries it.

Refs #335

Assisted-by: Claude Code:claude-opus-5-5
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 5, 2026
#605 taught the name-keyed npm resolver to probe bundled store
trees, so it now finds aliased copies (node_modules/lp) and a nested
host's store peers itself. Two vex_consumed tests from #738 assumed
that set never held aliases, so main's CI went red after both merged.

The tests now feed the alias-free set explicitly to keep covering
alias expansion, and also check the resolver's own set reaches the
same copies with no duplicates. No production code changes.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 40dac07)
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 5, 2026
* Start fix for #796

Assisted-by: Claude Code:claude-opus-5-5

* Find Bundler 4 standalone installs in ./bundle

`bundle install --standalone` puts gems in ./bundle and the app loads
them through bundle/bundler/setup.rb. Bundler 2 also recorded the path
in .bundle/config, but Bundler 4 writes no config at all, so the gem
crawler never looked in ./bundle. Agent apply then patched an ambient
copy of the same gem (or said it was not installed), VEX attested
not_affected while the app ran the unpatched copy, and the hosted
stale-install warning stayed silent.

Probe ./bundle as an install root when bundle/bundler/setup.rb is
present, in the slot Bundler 2's recorded path used to take. Fixes #796.

Assisted-by: Claude Code:claude-opus-5-5

* Document the standalone bundle install root

List the Bundler 4 standalone ./bundle tree in the CLI contract's gem
install-root order, so the documented roots match what the crawler
probes.

Assisted-by: Claude Code:claude-opus-5-5

* Port #851: fix vex alias tests after #605

main is red since #605: two commands::vex_consumed tests assumed the
name-keyed resolver never returns npm-aliased copies, but #605 taught it
to probe bundled store trees. This ports the tests-only fix from #851 so
this PR's CI goes green; it no-ops once main carries #851.

Assisted-by: Claude Code:claude-opus-5-5

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 5, 2026
The merge in 46c9d4b took this branch's stale vex_consumed tests over
feat/gradle-support's #605 updates: since #605 the name-keyed resolver
already returns npm aliases and nested-store peers, so the old
assertions that it returns none failed. Restore the gradle-side tests,
keeping this branch's sbt hosted repo lookup in maven_copies.

#664 (via main) refuses a linked `.socket/vendor` up front for every
ecosystem with `vendor_dir_symlink_unsupported`, before the sbt shape
check runs; expect that code for the linked vendor dir in the sbt
vendor e2e, as the Maven CLI test already does.

Co-Authored-By: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 5, 2026
…files (#590, #417) (#598)

* Start fix for #590, #417

Assisted-by: Claude Code:claude-opus-5-5

* Refuse hosted runs from a workspace member

Hosted scan and get read locks only in --cwd. Run from a pnpm
workspace member (or a project whose lockfile-dir puts pnpm-lock.yaml
elsewhere), they pinned nothing and still reported success, so pnpm
kept installing the unpatched package (#590). Run from a cargo
workspace member, they rewrote the member as a lockless project and
broke every build of the workspace (#417).

Both layouts are now refused before any takeover or write, exit 1,
naming the directory to run from: redirect_pnpm_lockfile_elsewhere
for pnpm, and the vendored cargo_manifest_not_workspace_root check,
now shared, for cargo.

Assisted-by: Claude Code:claude-opus-5-5

* Document the hosted workspace-member refusals

Assisted-by: Claude Code:claude-opus-5-5

* Honor lockfileDir set by the workspace root

A workspace root can move pnpm-lock.yaml with lockfileDir, and the
key may be written quoted in pnpm-workspace.yaml. Hosted runs from a
member of such a workspace, or of one with a quoted key, still
reported success while pinning nothing. Both are now refused like
any other member whose lock lives elsewhere.

Assisted-by: Claude Code:claude-opus-5-5

* Honor pnpm workspace lockfile configuration precedence

* Drop CHANGELOG entry from this PR

Release notes are written when a release is cut, from the merged PR
log and the code, so PRs no longer edit CHANGELOG.md.

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

* Fix vex alias tests broken by store-copy merge

#605 taught the name-keyed npm resolver to probe bundled store
trees, so it now finds aliased copies (node_modules/lp) and a nested
host's store peers itself. Two vex_consumed tests from #738 assumed
that set never held aliases, so main's CI went red after both merged.

The tests now feed the alias-free set explicitly to keep covering
alias expansion, and also check the resolver's own set reaches the
same copies with no duplicates. No production code changes.

Assisted-by: Claude Code:claude-opus-5-5

* Read lockfileDir past a BOM and take the last

A pnpm-workspace.yaml saved with a UTF-8 BOM, or one that sets
lockfileDir twice, could hide a relocated lock from the member check,
so a hosted run from a member still reported success while pinning
nothing. The reader now skips the BOM and uses the last assignment.

Assisted-by: Claude Code:claude-opus-5-5

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 5, 2026
* Start fix for #652

Assisted-by: Claude Code:claude-opus-5-5

* Refuse gem lines pulled from git sources

Hosted scans moved a gem declared with `gitlab:`, a custom
`git_source(:name)` key or a string-keyed `"git" =>` option into the
Socket source block. The option still overrode the block, so bundler
kept loading the unpatched git checkout while scan reported the gem
redirected and VEX attested it not_affected. String-keyed options
such as `"require" => false` were also silently dropped.

Both hosted and vendored modes now read gem options through one
shared reader that understands every key spelling and treats any key
outside bundler's non-source options as a source. Hosted mode also
refuses a gem the lock resolves from a GIT, PATH or plugin section.

Fixes #652

Assisted-by: Claude Code:claude-opus-5-5

* Add e2e and escape tests for gem git sources

Adds a real-bundler capstone where the gem comes from a custom
`git_source` key: the hosted scan must refuse it, write nothing and
attest nothing, and bundler must still install the project. The
option reader now also honors backslash escapes in single-quoted
strings, so a quote inside a value cannot hide a later git option.

Refs #652

Assisted-by: Claude Code:claude-opus-5-5

* Read the gem lock's git sections once per scan

The new git/path refusal re-parsed Gemfile.lock for every patched
gem, which made hosted bundler scans about 15% slower on the bench
fixture (800 gems, 20 patched). The lock is now parsed once per
rewrite; our own edits only touch GEM sections and CHECKSUMS, so
the GIT/PATH membership read up front stays accurate.

Refs #652

Assisted-by: Claude Code:claude-opus-5-5

* Fix vex alias tests broken by store-copy merge

#605 taught the name-keyed npm resolver to probe bundled store
trees, so it now finds aliased copies (node_modules/lp) and a nested
host's store peers itself. Two vex_consumed tests from #738 assumed
that set never held aliases, so main's CI went red after both merged.

The tests now feed the alias-free set explicitly to keep covering
alias expansion, and also check the resolver's own set reaches the
same copies with no duplicates. No production code changes.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 40dac07)

* Retry PDM backtest cases on transport errors

The PDM matrix runs against production PyPI and the public patch API.
Over the last 7 days 25 pdm-compatibility runs failed on one random
cell each, on unrelated PRs. The version, OS, shape, mode and check
differed every time (rescanIdempotent, appliedExactlyOne,
rescanAfterRelockApplies, ...). Each check judges a CLI scan, install
or rollback. `Run` retries a command once, and only on a non-zero
exit. The CLI usually reports an exhausted patch API fetch in its
JSON while exiting zero, so the cell just fails a later check.

Port backtest-poetry.py's case-level retry (#596). A case is re-run
from a fresh directory, at most three attempts, only when every
failed check recorded transport evidence from the operation it
judged. Evidence is a failed command's request error, PyPI give-up,
patch API 5xx or exhausted 429, or the same in the CLI's JSON error
records. Functional failures are never retried, even when a later
step raises a transport error. Failed attempts' logs go under
attempts/ and are uploaded. A failing case now prints its failed
checks' notes, since the job log alone never said why.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8YeCUhdg2tKdqyyY7z3sV
(cherry picked from commit 4329170)

* Judge PDM rollback and VEX checks by their run

Bugbot: the final hosted/vendored rollback checks, the unverifiable-
write rollback, the refused-lock VEX and the reverted-lock VEX runs
named no operation, so a transport failure there never made the case
retryable. installedBytesPatched fails together with a blipped pdm
sync and blocked the retry the same way.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8YeCUhdg2tKdqyyY7z3sV
(cherry picked from commit 6b0302a)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 5, 2026
* Start fix for #736

Assisted-by: Claude Code:claude-opus-5-5

* Read only the gem lock Bundler actually loads

A gems.rb project's gems.locked was invisible to the lock inventory,
ledger recovery read only Gemfile.lock, and VEX discovery read both
locks. A leftover redirected Gemfile.lock beside gems.rb + gems.locked
therefore made vex attest not_affected while bundle install installed
the unpatched gem from gems.locked.

Add one resolver for the lock Bundler loads (honouring BUNDLE_GEMFILE
and the app config) and route the inventory, gem_remotes, VEX
discovery and the hosted engine through it. VEX still reads the
ignored twin, but any Socket wiring there is diagnosed as
unattributable instead of attested.

Fixes #736

Assisted-by: Claude Code:claude-opus-5-5

* Test hosted engine on a gems.rb project

Assisted-by: Claude Code:claude-opus-5-5

* Avoid a single-element loop in the polyglot test

Assisted-by: Claude Code:claude-opus-5-5

* Note the gem lock reader fix in the changelog

Assisted-by: Claude Code:claude-opus-5-5

* Drop CHANGELOG entry from this PR

Release notes are written when a release is cut, from the merged PR
log and the code, so PRs no longer edit CHANGELOG.md.

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

* Port #851: fix vex alias tests broken by store-copy merge

main is red since 4646693 (#605): two commands::vex_consumed tests
assumed the name-keyed resolver never returns npm-aliased copies, which
#605 changed. Same tests-only change as #851; it no-ops once main
carries it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Ao6g9qAnawPfNxv11f3wM

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 5, 2026
* Start fix for #632

Assisted-by: Claude Code:claude-opus-5-5

* Pin yarn catalog deps in hosted mode

A dependency declared "catalog:" in package.json was never patched
by scan --mode hosted: the resolutions entry was keyed by the lock's
expanded npm: range, but yarn matches resolutions before it expands
the catalog. The scan reported success, then yarn install --immutable
failed (YN0028) and a plain yarn install kept the unpatched release.

Also route name@catalog: / name@catalog:<named> for every
.yarnrc.yml catalog that maps the package to a pinned range. A re-run
adds the selector to a pin written by an earlier release.

Fixes #632

Assisted-by: Claude Code:claude-opus-5-5

* Test yarn catalog pins end to end

Add a real-yarn check that a fresh checkout of a hosted-pinned
catalog dependency installs the patched bytes under --immutable,
an in-process scan + rollback round trip, and document catalog
pins in the yarn berry hosted notes.

Assisted-by: Claude Code:claude-opus-5-5

* Drop unrelated rustfmt churn

cargo fmt --all also reformatted 127 files this fix doesn't touch
(main isn't rustfmt-clean). Restore them and the untouched hunks of
the edited files to main, so the PR only carries the catalog fix,
its tests and the docs note.

Assisted-by: Claude Code:claude-opus-5-5

* Keep unquoted yarn catalog ranges as their source text

berry_catalog_selectors parsed .yarnrc.yml catalogs into serde_json
Values, so an unquoted range like `1.10` became the number 1.1 and
never matched the lock's `npm:1.10`: the `catalog:` selector was
dropped while the pin was still confirmed. Yarn reads .yarnrc.yml with
the failsafe schema, so deserialize the catalog tables as string
tables instead, which keeps each scalar's source text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPHxnE5P1rkfCpHFFCwzFR

* Port #851: fix vex alias tests broken by store-copy merge

main fails commands::vex_consumed::tests::hosted_expands_alias_only_copies
and hosted_reuses_expanded_npm_copies_and_merges_alias_variants since
#605 landed alongside #738; #851 fixes the tests. Carry the same change
so this PR's CI (coverage, test-release) is green; it no-ops once main
has it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPHxnE5P1rkfCpHFFCwzFR

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 5, 2026
* Start fix for #756, #772

Assisted-by: Claude Code:claude-opus-5-5

* Report writes to pnpm/vlt store twin copies

When a package has more than one pnpm or vlt peer-variant store copy,
apply and rollback already patch (or restore) every copy, but they only
reported what happened to the first one. A run that fixed only a twin
copy said "already patched" (applied: 0), and a rollback that restored
only a twin said "already original" (rolledBack: 0).

Apply and rollback now share one store-copy fan-out and one fold, which
merges each copy's per-file records into the result under the copy's
on-disk path. The two private folds, which had drifted on which
advisories they kept, are gone; both directions now carry only the
ownership advisory from a copy.

Fixes #756, #772.

Assisted-by: Claude Code:claude-opus-5-5

* Test CLI reporting of store twin writes

End-to-end regression for #756 through the real binary, on hand-built
pnpm and vlt store layouts: apply that patches only a twin copy reports
it as applied, and rollback that restores only a twin counts it as
rolled back. Documents the copy-qualified file paths in CLI_CONTRACT.

Assisted-by: Claude Code:claude-opus-5-5

* Keep --force skips in a twin copy local

Under --force, a store twin missing a patched file skips it and still
succeeds. The fold already dropped that copy's "all files skipped" note,
but it carried the skipped file's NotFound record, so a package whose
primary copy was already patched was reported as "applied" with no
files instead of "already patched". Those records now stay with the
copy, like its note.

Assisted-by: Claude Code:claude-opus-5-5

* Fix vex alias tests broken by store-copy merge

#605 taught the name-keyed npm resolver to probe bundled store
trees, so it now finds aliased copies (node_modules/lp) and a nested
host's store peers itself. Two vex_consumed tests from #738 assumed
that set never held aliases, so main's CI went red after both merged.

The tests now feed the alias-free set explicitly to keep covering
alias expansion, and also check the resolver's own set reaches the
same copies with no duplicates. No production code changes.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 40dac07)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 5, 2026
* Start fix for #798

Assisted-by: Claude Code:claude-opus-5-5

* Refuse npm VEX when the twin lock lacks the pkg

With both npm-shrinkwrap.json and package-lock.json committed,
lockfile-only `vex` attested a patch that only one lock wired when the
other lock had no entry for the package at all. npm 12 installs from
package-lock.json and re-resolves a missing entry from the registry,
so the checkout installed unpatched bytes while the VEX document said
`not_affected`.

A twin lock with no entry for the package now contests the wiring the
same way a registry entry does (`patched_ref_unattributable`), in both
directions and for hosted and vendored wiring. A twin that holds the
package only at another version still contests nothing: npm installs
that version, not unpatched bytes of the patched one.

Fixes #798

Assisted-by: Claude Code:claude-opus-5-5

* Make the dual-lock read test use agreeing twins

The test that proves both npm locks are read wired each package in only
one lock. After #798 such a pair is contested (npm re-resolves the
package missing from the other lock), so the fixture now has each lock
wire both packages. It still proves both locks are read (4 refs) and
that the v2 legacy mirror adds nothing.

Refs #798

Assisted-by: Claude Code:claude-opus-5-5

* Keep patch refs out of a vex test's messages

CodeQL flagged the new dual-lock test for printing the wired patch
reference (which holds the patch uuid) in an assertion message. The
message now names the wiring mode instead.

Refs #798

Assisted-by: Claude Code:claude-opus-5-5

* Fix vex alias tests broken by store-copy merge

#605 taught the name-keyed npm resolver to probe bundled store
trees, so it now finds aliased copies (node_modules/lp) and a nested
host's store peers itself. Two vex_consumed tests from #738 assumed
that set never held aliases, so main's CI went red after both merged.

The tests now feed the alias-free set explicitly to keep covering
alias expansion, and also check the resolver's own set reaches the
same copies with no duplicates. No production code changes.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 40dac07)

* Contest a twin npm lock that lacks the wired version

A twin lock that held the package only at another version still let the
wired ref through. npm keeps that entry only while it satisfies
package.json, and otherwise fetches the wired version unpatched from the
registry, so the lock alone can't vouch for it. The twin now contests
unless it has an entry for the same name@version at any path. Lock pairs
the rewriters keep in sync share that set, so they are unaffected.

Refs #798

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtZrsd52E6vxhvF9hpLVSw

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 5, 2026
* Start fix for #627

Assisted-by: Claude Code:claude-opus-5-5

* Refuse vendoring over a symlinked lockfile

Vendored mode renamed its rewritten lockfile (or package.json,
pnpm-workspace.yaml, nuget.config) over a symbolic link, turning a
shared lock into a detached copy: the link's target, the lock other
checkouts install from, stayed unpatched, and revert never restored
the link. Hosted mode already refused this.

The vendored group commit now refuses before writing anything when a
file it would change is a symlink or sits under a symlinked
directory. The run exits 1 with the same
redirect_symlinked_file_unsupported error hosted mode uses, and leaves
the link, its target and the vendor ledger untouched. A --dry-run
flags each symlinked wiring file with a
vendor_would_refuse_symlinked_file advisory.

Fixes #627

Assisted-by: Claude Code:claude-opus-5-5

* Gate only symlinked files, as hosted does

Writing into a symlinked directory goes through the link rather than
replacing it, so refusing it would newly break projects that link a
whole directory. Check the changed file itself, which matches the
hosted guard. Also add a real-yarn e2e for a symlinked yarn.lock.

Assisted-by: Claude Code:claude-opus-5-5

* Format the symlinked yarn.lock e2e

Assisted-by: Claude Code:claude-opus-5-5

* Warn about symlinked files in scan/get dry runs

scan and get --mode vendored --dry-run stop at the ledger preview and
never reach the vendor loop, so they gave no hint that the real run
would refuse a symlinked lockfile. The preview's would_vendor and
would_revendor rows now carry the same symlink warning that
vendor --dry-run emits, and human output prints it.

Assisted-by: Claude Code:claude-opus-5-5

* Narrow dry-run symlink warnings to real writes

The vendored dry run warned about symlinked files a vendored run only
reads (.yarnrc.yml, vlt.json, node_modules/.modules.yaml), which the
real run never writes and so never refuses. It also warned for
packages already in sync, whose re-run writes nothing. Both produced
false predictions of the symlink refusal.

The warning now covers only files a vendored run can rewrite, and
skips packages the dry run previews as already vendored.

Assisted-by: Claude Code:claude-opus-5-5

* Warn about symlinked pom.xml and hatch.toml too

The narrowed dry-run warning dropped files vendored Maven and Hatch
really rewrite: the root pom.xml, .mvn/maven.config and hatch.toml.
A symlinked root pom.xml was still refused by the real run with no
dry-run hint. Add them to the list of vendored write targets.

Assisted-by: Claude Code:claude-opus-5-5

* Port #851 fix for vex alias tests broken on main

main has been red since #605: two vex_consumed tests still assumed
the name-keyed resolver was alias-blind, so the CLI lib tests fail on
every branch built on main. This carries the same test-only change as
#851 and becomes a no-op once #851 lands.

Assisted-by: Claude Code:claude-opus-5-5

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 5, 2026
* Start fix for #804

Assisted-by: Claude Code:claude-opus-5-5

* Fix rollback of pip-written pylock.toml

`pip lock` writes PEP 751's array-of-tables spelling
(`[[packages.wheels]]` with a `[packages.wheels.hashes]` sub-table),
but the hosted upstream restore only read inline `wheels = [{ ... }]`
arrays. Every pip sibling looked artifact-free, so `rollback`, `remove`
and the hosted -> vendored takeover always refused a pip lock with "no
sibling registry package shows ...", leaving users with a hosted patch
they could not undo.

The restore now reads artifacts in either spelling, writes the entry
back in the siblings' spelling, and, since pip records only the one
artifact it selected, restores only the release's wheel (or its sdist
when it has no wheel), refusing a release with several wheels. The
refusal no longer blames "this uv release" for a pip-written lock.

Fixes #804

Assisted-by: Claude Code:claude-opus-5-5

* Port vex alias test fix from #851

main has been red since #605 taught the npm copy resolver to probe
bundled store trees: two vex_consumed alias tests (#738) still assumed
the resolver never returns npm-aliased copies, so the CLI lib tests
fail on every PR's merge ref. This ports #851's tests-only fix so the
PR's CI reflects its own change; it no-ops once #851 lands on main.

Assisted-by: Claude Code:claude-opus-5-5

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 5, 2026
* Start fix for #372

Assisted-by: Claude Code:claude-opus-5-5

* Accept vlt 1.3 brotli lock nodes

vlt 1.3 marks a lock node that fetches the registry's Brotli
(.tar.br) tarball with a new flag bit, 4, in slot [0]. socket-patch
only accepted flags 0-3, so hosted mode refused such a lock as "not
canonical" and exited 0 with nothing redirected, and vendored mode
failed with a misleading lockfile-version error.

Accept flags 0-7. When a pin or vendored wiring points a node at a
.tgz or local directory, clear the brotli bit as vlt would save it;
reverts put the recorded bit back with the original slots. The vlt
heal now reinstalls brotli prod and dev nodes like any other.

Fixes #372

Assisted-by: Claude Code:claude-opus-5-5

* Port #851: fix vex alias tests broken by store-copy merge

main is red since 4646693 (#605): two commands::vex_consumed tests
assumed the name-keyed resolver never returns npm-aliased copies.
Same test-only change as #851; it no-ops once main carries it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhRxWtzEYpLyrByBegiiRy

* Drop unrelated cargo fmt --all churn from the vlt brotli fix

The fix commit b3996a6 also reformatted 123 files it does not otherwise
touch (the output of cargo fmt --all on a tree main has not formatted).
Every one of those files is byte-identical to rustfmt run over main's
version, so this restores them to main. The PR now only touches the vlt
lock, redirect and heal code plus the ported #851 test fix, which keeps
the review small and stops the churn from conflicting with every other
open PR.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 5, 2026
* Start fix for #432

Assisted-by: Claude Code:claude-opus-5-5

* Pin npm aliases in the npm 6 lock mirror

A lockfileVersion 2 package-lock.json keeps a legacy `dependencies`
mirror for npm 6, which spells an alias install as
`"lp": {"version": "npm:left-pad@1.3.0"}`. Hosted scans matched mirror
nodes on a plain version only, so the alias node silently stayed on
the registry, and a lockfileVersion 1 alias lock pinned nothing at all
("no package-lock.json entry"). Rollback could not restore such a node
either.

Every reader of the legacy tree now decodes the alias through one
helper. Hosted scans rewire the alias node with the rest, and rollback
restores it. npm 6 fetches an aliased dependency from the registry
whatever `resolved` says, so under npm 6 the pinned lock fails closed
(EINTEGRITY) instead of installing unpatched bytes, and the run warns
`redirect_npm_legacy_alias_client`.

Refs #432

Assisted-by: Claude Code:claude-opus-5-5

* Vendor npm aliases in the npm 6 lock mirror

Vendoring skipped the v2 mirror node of an npm alias with
`vendor_legacy_alias_skipped`, so npm 6 installed the unpatched
registry tarball through it. npm 6 does install an alias node from a
`file:` resolved (checked against npm 6.14.18), so the node is now
rewired like every other mirror node and revert restores it.

Refs #432

Assisted-by: Claude Code:claude-opus-5-5

* Withhold VEX when the npm 6 mirror is unpatched

Lockfile-only VEX read a v2 lock's `packages` half only, so it attested
`not_affected` for a package whose legacy mirror (what npm 6 installs
from) still resolved to the registry, as locks written before this fix
do for npm aliases. Such a ref is now diagnosed as unattributable and
not attested. A lockfileVersion 1 alias node is read as an install of
its target package.

Fixes #432

Assisted-by: Claude Code:claude-opus-5-5

* Let a stale npm 6 mirror contest the sibling lock

When npm-shrinkwrap.json's legacy mirror still resolved a package from
the registry, VEX dropped the shrinkwrap's own ref but still attested
the same package from package-lock.json, although npm 6 installs from
the shrinkwrap. A mirror node off Socket now counts as resolving the
package elsewhere, so the sibling lock's ref is contested too.

Refs #432

Assisted-by: Claude Code:claude-opus-5-5

* Port vex alias test fix from #851

Main has been red since #605: two commands::vex_consumed tests assume
the name-keyed resolver never returns npm-aliased copies, but #605
taught it to probe bundled store trees. Port #851's test-only fix so
this PR's CI runs on a green base. It becomes a no-op once #851 lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EgBZwmqgXLZaRGyfDFoWwp

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 5, 2026
* Retry PDM backtest cases on transport errors

The PDM matrix runs against production PyPI and the public patch API.
Over the last 7 days 25 pdm-compatibility runs failed on one random
cell each, on unrelated PRs. The version, OS, shape, mode and check
differed every time (rescanIdempotent, appliedExactlyOne,
rescanAfterRelockApplies, ...). Each check judges a CLI scan, install
or rollback. `Run` retries a command once, and only on a non-zero
exit. The CLI usually reports an exhausted patch API fetch in its
JSON while exiting zero, so the cell just fails a later check.

Port backtest-poetry.py's case-level retry (#596). A case is re-run
from a fresh directory, at most three attempts, only when every
failed check recorded transport evidence from the operation it
judged. Evidence is a failed command's request error, PyPI give-up,
patch API 5xx or exhausted 429, or the same in the CLI's JSON error
records. Functional failures are never retried, even when a later
step raises a transport error. Failed attempts' logs go under
attempts/ and are uploaded. A failing case now prints its failed
checks' notes, since the job log alone never said why.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8YeCUhdg2tKdqyyY7z3sV

* Judge PDM rollback and VEX checks by their run

Bugbot: the final hosted/vendored rollback checks, the unverifiable-
write rollback, the refused-lock VEX and the reverted-lock VEX runs
named no operation, so a transport failure there never made the case
retryable. installedBytesPatched fails together with a blipped pdm
sync and blocked the retry the same way.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8YeCUhdg2tKdqyyY7z3sV

* Port #851: fix vex alias tests broken on main

Main is red since #605 (4646693): two commands::vex_consumed tests
assume the name-keyed resolver never returns npm-aliased copies, and
#605 taught it to find them. This fails socket-patch-cli --lib in
coverage and test on every PR. Port #851's test-only fix so this PR
can go green; it no-ops once #851 lands.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8YeCUhdg2tKdqyyY7z3sV

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 5, 2026
* Start fix for #709

Assisted-by: Claude Code:claude-opus-5-5

* Verify gems in out-of-tree bundle config paths

A .bundle/config "path" outside the project (bundle config set
--local path /opt/bundle) is refused as an install root because
apply writes there. That refusal also hid the root from the
read-only checks, so hosted scan gave no stale-install warning and
both the in-run --vex and a later `vex` attested not_affected while
bundler kept loading the unpatched gem from that path.

The refused root is now exposed as a verification-only store: the
hosted stale-install probe and vex's installed-copy lookup read it,
while apply and rollback still never write there. A stale copy
there gets the project-local remedy.

Fixes #709

Assisted-by: Claude Code:claude-opus-5-5

* Test hosted scan over an out-of-tree bundle path

Covers #709 end to end: a stale gem under a .bundle/config path
outside the project now warns with the project-local remedy, and the
same run's --vex does not attest it.

Assisted-by: Claude Code:claude-opus-5-5

* Run the out-of-tree bundle path probe test

The regression test for #709 was nested inside another test function,
so it compiled but never ran. Move it back to module level.

Assisted-by: Claude Code:claude-opus-5-5

* Honor BUNDLE_IGNORE_CONFIG for the bundle path

With BUNDLE_IGNORE_CONFIG set, bundler reads no config file, so a
.bundle/config path is neither an install root nor a root bundler
loads from. Discovery now skips the app config's BUNDLE_PATH in that
case, the same way the cache-path and Gemfile readers already do, so
leftover gems under that path no longer raise a stale-install
warning or fail VEX.

Assisted-by: Claude Code:claude-opus-5-5

* Port #851: fix vex alias tests broken on main

Main is red since #605: two vex_consumed tests assumed the copy
resolver never returns npm-aliased copies, which #605 changed. This
ports the test-only fix from #851 so this PR's CI can go green; it
becomes a no-op once #851 lands.

Assisted-by: Claude Code:claude-opus-5-5

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Start fix for #681

Assisted-by: Claude Code:claude-opus-5-5

* Refuse hosted gem redirect under a mirror

Bundler's mirror.all (or a mirror for the patch-registry source)
sends the per-dep source block the hosted redirect writes to the
mirror, which serves the unpatched upstream gem. The scan reported
the gem redirected and the in-run VEX attested not_affected while
the next bundle install was unpatched or failed CHECKSUMS.

The hosted intake now reads the mirror settings from the bundler app
config and BUNDLE_MIRROR__ALL and, when one captures the patch
registry, leaves the Gemfile pair untouched, attests nothing, and
warns redirect_gem_mirror_overrides_source with the remedy (scope the
mirror to rubygems.org).

Fixes #681

Assisted-by: Claude Code:claude-opus-5-5

* Give each mirror refusal a remedy that clears it

The redirect_gem_mirror_overrides_source detail always advised unsetting
a local mirror.all, which never clears a BUNDLE_MIRROR__ALL from the
environment or a mirror.<source> key for the patch registry. The mirror
model now returns the remedy for the setting it detected, and a test
applies each remedy and checks the next scan passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEpjVvY7X41jPuVuoiCLVz

* fix(gem): block mirror bypasses in hosted attestations

* Check hosted gem pins for Bundler mirrors in every hosted VEX

The mirror refusal flag for embedded VEX was derived only from the
rewrite's redirect_gem_mirror_overrides_source warning, which exists
only when this run had gem candidates. A hosted scan with an empty
catalog, a paid-only gem or a withdrawn offer still rediscovers older
hosted gem pins in its VEX plan, so lockfile inference and
--vex-no-verify could attest them while Bundler fetched unpatched bytes
through a capturing mirror.

Embedded hosted VEX now checks each hosted gem pin in the completed plan
against the project's Bundler mirror settings (using the pin's own
Socket source), on the redirect path and on the hosted scan's empty
JSON and human terminal paths. Verified installed bytes remain valid
evidence; standalone and agent/vendored VEX are unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>

* Port #851: fix vex alias tests broken by the #605 store-copy merge

main went red when #605 taught the name-keyed resolver to find pnpm
store copies, which the #738 alias tests assumed it missed. Same
change as #851; it no-ops once main carries it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEpjVvY7X41jPuVuoiCLVz

* Port #878: route Gradle digests through utils::digest

main went red when Gradle code landed with inline sha1/sha256 calls that
utils::digest::tests::production_digests_go_through_the_helpers rejects.
Same change as #878; it no-ops once main carries it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEpjVvY7X41jPuVuoiCLVz

* Detect Bundler 4.1's quoted mirror keys

Bundler 4.1 double-quotes any .bundle/config key that contains a
colon, so an exact patch-source mirror set with
`bundle config set --local mirror.https://...` is written as
"BUNDLE_MIRROR__HTTPS://...": "...". The mirror check kept the quote
on the key, missed the BUNDLE_MIRROR__ prefix, and let the hosted
scan pin a source Bundler then fetched from the mirror (unpatched)
while VEX attested it.

Parse a quoted config key the way Bundler 4.1 reads it (double-quoted
with its escapes, or single-quoted with doubled quotes) before the
mirror lookup. The gem e2e suite gains an app-config exact-source
mirror driver, which fails on Bundler 4.1.0.beta1 without the fix.

Assisted-by: Claude Code:claude-opus-5-5

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Start fix for #769

Assisted-by: Claude Code:claude-opus-5-5

* Re-vendor Pipenv locks to a newer patch

A Pipenv project vendored at one patch never moved to a newer patch
for the same package: the re-vendor refused with
pypi_pipenv_source_already_exists and the run exited 1, although the
dry run previewed would_revendor.

When the vendor ledger records the Pipfile.lock entry the older patch
wrote, and that entry is unchanged, it is now rewired in place to the
new wheel. The record carries the older entry's pre-vendor registry
original forward, so vendor --revert still restores the user's pin.
Without that record, or after an edit, it still refuses as before.

Refs #769

Assisted-by: Claude Code:claude-opus-5-5

* Re-vendor PyPI installs from an older patch

When a venv was installed from the vendored wheel of an older patch
(pipenv sync after vendoring), re-vendoring to a newer patch skipped
the package as package_not_installed and exited 1: the installed
files are the old patch's bytes, so they failed the new patch's
installed-variant check.

When the vendor ledger holds exactly this package at an older patch
uuid, such an install is now treated like a lock-only checkout: the
pristine wheel comes from the lock, registry or patch service, and the
package is re-vendored. The service download plan makes the same call.

Fixes #769

Assisted-by: Claude Code:claude-opus-5-5

* Keep the ledger-less Pipenv wrappers test-only

check_target_guards and wire_pipenv now have no production caller
(the vendor flow passes the ledger through the _superseding
variants), so clippy flagged them as dead code. Compile them for
tests only and point the docs at the variants production uses.

Refs #769

Assisted-by: Claude Code:claude-opus-5-5

* Port #851's vex alias test fix

Main has been red since 4646693 (#605): two
commands::vex_consumed tests built for #738 assume the name-keyed
resolver never returns npm-aliased copies, which #605 changed. This is
the same test-only change as #851 and becomes a no-op once that lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VSXCFoPbraq7rNKJpXEP2n

* Port #878's Gradle digest routing

Main is red since 1714299 (#865): its
production_digests_go_through_the_helpers guard flags the inline
digests that #646 added in gradle_cache.rs, jvm_jar.rs and
sidecars/maven.rs. This is the same change as #878 and becomes a
no-op once that lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VSXCFoPbraq7rNKJpXEP2n

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Start fix for #410

Assisted-by: Claude Code:claude-opus-5-5

* Fix pip rollback refusing all-hosted requirements

Hosted rollback, remove and the hosted-to-vendored takeover refused a
requirements.txt in which every requirement was a hosted pin (a lone
`six==1.16.0`, or one beside `-e .`). They couldn't tell whether the
original line used pip's hash-checking mode, so the only way back was
version control.

The restore now counts an editable line as unhashed evidence (pip
refuses editables in hash-checking mode). When no other line settles
the mode, it reads the hosted line itself: the rewriter writes
`--hash` only into an already hashed file and otherwise pins by the
url's `#sha256=` fragment. With nothing else in the file to conflict
with, either restored form installs.

Fixes #410

Assisted-by: Claude Code:claude-opus-5-5

* Update restore golden for sole-pin requirements

The golden test asserted that a requirements.txt holding only the
hosted pin is refused as ambiguous, which is the #410 bug. It now
asserts that both the unhashed and hashed sole-pin files round-trip,
and keeps the mixed hashed/unhashed refusal.

Refs #410

Assisted-by: Claude Code:claude-opus-5-5

* Fix vex alias tests broken by store-copy merge

#605 taught the name-keyed npm resolver to probe bundled store
trees, so it now finds aliased copies (node_modules/lp) and a nested
host's store peers itself. Two vex_consumed tests from #738 assumed
that set never held aliases, so main's CI went red after both merged.

The tests now feed the alias-free set explicitly to keep covering
alias expansion, and also check the resolver's own set reaches the
same copies with no duplicates. No production code changes.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 40dac07)

* Route Gradle digests through utils::digest

main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 659ac2c)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Start fix for #831

Assisted-by: Claude Code:claude-opus-5-5

* Keep vendored npm tarballs out of .gitignore

Vendoring into a yarn classic, yarn berry, npm, pnpm or bun project
wrote .socket/vendor/npm/<uuid>/<pkg>.tgz without checking whether git
would commit it. Under Node.gitignore's `*.tgz`, or a `vendor/` or
`.socket/` rule, the scan reported success, the commit dropped the
tarball, and every fresh checkout's install failed.

The shared tarball staging now refuses with vendor_artifact_gitignored
before writing anything when a rule ignores the uuid dir itself. After
writing, it adds <uuid>/.gitignore (re-including the tarball against
rules like `*.tgz`) and .gitattributes, as vlt already does, and checks
the written paths again.

Refs #831

Assisted-by: Claude Code:claude-opus-5-5

* Fail vendor --check on unledgered lock references

When the vendor ledger and manifest were ignored or dropped from a
commit, `vendor --check` found nothing to compare and exited 0, while
the lockfile still pointed at .socket/vendor/<eco>/<uuid>/ and every
fresh install failed. The check now reads the lockfile references
(the same scan repair uses) and reports each one no ledger entry owns
as vendor_ledger_missing.

Refs #831

Assisted-by: Claude Code:claude-opus-5-5

* Document the vendored tarball's uuid .gitignore

The contract's vendoring table now says every npm-family tarball flavor
writes <uuid>/.gitignore and .gitattributes next to the tarball, and
refuses vendor_artifact_gitignored when git would still drop it.

Refs #831

Assisted-by: Claude Code:claude-opus-5-5

* Keep patch uuids out of vendor --check messages

CodeQL flagged the new unledgered-reference message for printing the
patch uuid. The human line and error detail now name only the
ecosystem; the JSON event still carries the uuid and path as repair's
event does.

Refs #831

Assisted-by: Claude Code:claude-opus-5-5

* Port #851: fix vex alias tests broken by the store-copy merge

main's #605 made the name-keyed npm resolver reach alias and peer
copies itself, which broke two vex_consumed tests that assumed an
alias-blind resolver. Same change as #851, ported so this PR's CI
runs green against the current base; it no-ops once #851 lands.

Refs #831

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMZyKmgYNAridSR5eqv999

* Refuse a gitignored vendor dir before the hosted takeover

The tarball gitignore refusal only ran inside stage_patch_pack, which
the hosted->vendored takeover reaches after restore_upstream has
already removed the hosted pin. In a hosted project that ignores
.socket/, vendoring then restored the registry entry and refused,
leaving the package patched in neither mode. The npm takeover
preflight now runs the same uuid-dir probe before the restore, as
vlt's preflight already does.

Refs #831

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMZyKmgYNAridSR5eqv999

* Port #878: route Gradle digests through utils::digest

main's #865 added a test that fails when production code computes
digests inline; the Gradle cache, JVM jar and Maven sidecar code
landed with inline sha1/sha256 calls, so main's coverage and
test-release jobs fail production_digests_go_through_the_helpers.
Same change as #878, ported so this PR's CI runs green against the
current base; it no-ops once #878 lands.

Refs #831

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMZyKmgYNAridSR5eqv999

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Start fix for #364

Assisted-by: Claude Code:claude-opus-5-5

* Refuse hosted yarn classic with an offline mirror

A yarn classic project that sets yarn-offline-mirror (in .yarnrc or
.npmrc) had its lock rewired to the hosted tarball. Yarn looks mirror
tarballs up by file name, and the hosted one has the same name as the
upstream tarball already in the mirror, so every install got the
unpatched bytes and failed the integrity check (or, offline, never
found the patched tarball) while the scan reported success and VEX
attested the patch.

The hosted rewrite now leaves yarn.lock untouched in that case, warns
with redirect_yarn_classic_offline_mirror and points to vendored mode,
which works with a mirror. The dependency is not counted as redirected
or attested. Both config files are read only beside a classic lock.

Fixes #364

Assisted-by: Claude Code:claude-opus-5-5

* Keep mirrored yarn classic vendored on takeover

A vendored-to-hosted takeover reverted the vendored yarn classic
wiring before the hosted rewrite refused the offline mirror, leaving
the package patched in neither mode. The takeover now checks the
mirror first and keeps the package vendored.

Adds a real-yarn e2e (yarn 1.22.22, populated mirror) showing the scan
refuses, writes no attestation, and fresh installs still work online
and offline.

Refs #364

Assisted-by: Claude Code:claude-opus-5-5

* Document the yarn classic offline mirror refusal

Refs #364

Assisted-by: Claude Code:claude-opus-5-5

* Re-bless pdm and poetry rewrite goldens

These goldens hash the Debug text of the whole rewrite result, which
now carries the empty refused_yarn_classic_uuids set. With that field
stripped from the text, the old goldens still match every case, so
only the output digests change; case keys and inputs are identical.

Refs #364

Assisted-by: Claude Code:claude-opus-5-5

* Fix mirror e2e on yarn releases before 1.7

yarn 1.0 to 1.6 install nothing from an offline mirror even without
socket-patch, so the fresh-install leg of the new mirror e2e failed on
the yarn-classic 1.0.2 and 1.6.0 matrix legs. Those releases now pin
that known limitation; the hosted refusal is still checked on every
release.

Refs #364

Assisted-by: Claude Code:claude-opus-5-5

* Detect a .yarnrc offline mirror written with a colon

yarn 1's .yarnrc parser ends an unquoted key at ':', so
`yarn-offline-mirror: ./mirror` and `yarn-offline-mirror:./mirror`
configure the mirror just like `yarn-offline-mirror ./mirror`. The
mirror check only split on whitespace, so either spelling slipped
through and hosted mode still rewired the lock, reproducing #364.

Refs #364

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uQyhodCtdJGrKaD7AAV1n

* Port vex alias test fix from #851

main has been red since #605 taught the name-keyed resolver to return
npm-aliased copies, which broke two vex_consumed tests added by #738.
Port #851's test update so this PR's CI goes green; it no-ops once
#851 lands on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uQyhodCtdJGrKaD7AAV1n

* Port Gradle digest-helper fix from #878

main has been red since #865 added a check that production code
computes digests through utils::digest, while #646's Gradle code
still hashes inline. Port #878's change so this PR's coverage and
test-release go green; it no-ops once #878 lands on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uQyhodCtdJGrKaD7AAV1n

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Start fix for #628, #629

Assisted-by: Claude Code:claude-opus-5-5

* Test hosted berry refusal of mixed package.json

A berry project whose root package.json mixes CRLF and LF is refused
by vendored mode, but hosted mode rewrites it in the majority ending.
These tests cover a fresh hosted scan and the vendored-to-hosted
takeover (#628). They fail until the gate is shared.

Assisted-by: Claude Code:claude-opus-5-5

* Share yarn berry project gates across modes

Hosted and vendored modes each carried their own copy of the yarn
berry project refusals (mixed line endings, cacheKey, .yarnrc.yml
compressionLevel), and the copies drifted: hosted mode never checked
the root package.json, so it silently rewrote a mixed-line-ending
manifest that vendored mode refuses (#628).

The gates now live once in formats/yarn/berry_gates.rs. The vendored
backend and its takeover preflight, the hosted rewriter, the
vendored-to-hosted takeover and the hosted restore all call it and
keep their existing codes. Hosted mode now refuses a mixed
package.json with redirect_yarn_berry_mixed_line_endings before
writing or reverting anything (#629).

Assisted-by: Claude Code:claude-opus-5-5

* Drop CHANGELOG entry from this PR

Release notes are written when a release is cut, from the merged PR
log and the code, so PRs no longer edit CHANGELOG.md.

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

* Port #851: fix vex alias tests broken on main

Since #605 landed, main fails two vex_consumed alias tests because the
name-keyed resolver now finds alias and bundled store copies itself.
This is #851's test-only fix, ported so this PR's CI can go green; it
becomes a no-op once #851 merges.

Assisted-by: Claude Code:claude-opus-5-5

* Port #878: route Gradle digests through utils::digest

main fails socket-patch-core's lib guard test
production_digests_go_through_the_helpers because three Gradle files
still hash inline, which turns coverage, test and test-release red on
this PR. This is the same change as #878 and becomes a no-op once that
lands on main.

Co-Authored-By: Claude <noreply@anthropic.com>

* Drop stale entries from digest pending list

Main went red when the Gradle and Maven digest moves landed: three
files still listed as computing digests inline no longer do, so the
ratchet test fails on every PR. Same change as #1016; it becomes a
no-op once that lands.

Assisted-by: Claude Code:claude-opus-5-5

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
#335) (#700)

* Find Hatch's out-of-tree project environments

Hatch installs a project into envs under its data directory, never
./.venv, so stale-install checks, VEX and agent mode never looked at
the environment hatch run actually uses. Model Hatch's placement
rules (data dir, dirs.env.virtual, flat layouts, explicit env paths,
the project id hash) and add those envs to local venv discovery.

Hosted scans now warn about a stale Hatch env with the remedy that
works (hatch env remove / prune), and vendored Hatch gets the same
check as pypi_hatch_stale_install. A real-Hatch e2e covers both modes
from an existing env through vex and the remedy.

Fixes #335

Assisted-by: Claude Code:claude-opus-5-5

* Name Hatch's remedy in vendored vex and old layouts

Hatch 1.0 to 1.2 keep envs at <name>-<id>/<env>, so discover that
layout too. Vendored vex already warns when the installed tree is
out of sync with the committed artifact; for a Hatch project the
advice to re-run the install does nothing, so name hatch env remove
instead.

Refs #335

Assisted-by: Claude Code:claude-opus-5-5

* Document Hatch existing-env handling

Explain where Hatch keeps environments, which warning each mode
gives for a stale one and the remedy, and how to run the real-Hatch
check.

Refs #335

Assisted-by: Claude Code:claude-opus-5-5

* Match Hatch envs across symlinked path spellings

On macOS /var is a symlink to /private/var, so an activated env and
the discovered one can name the same directory differently, and the
stale-install check then missed it. Compare resolved paths as a
fallback, and leave dirs.env.virtual unresolved as Hatch does (only
an env's explicit path is resolved).

Refs #335

Assisted-by: Claude Code:claude-opus-5-5

* Keep Hatch envs visible under an activated venv

An activated venv that is not one of Hatch's own is never used by
hatch run, yet it returned from discovery before the Hatch envs were
added, so a developer shell with any venv active hid the stale Hatch
env again. Add Hatch's envs in that case too, and have the vendored
probe judge Hatch's env prefixes directly.

Refs #335

Assisted-by: Claude Code:claude-opus-5-5

* Pass the new Pipfile.lock argument in Hatch tests

main added a pipenv_lock parameter to the hosted Python stale-install
probe; the Hatch remedy test from this branch still called it with
the old arity, so the CLI test build broke after the merge.

Refs #335

Assisted-by: Claude Code:claude-opus-5-5

* Keep Hatch envs visible under a uv project env

A Hatch project's pyproject alone reads as a uv project, so a set
UV_PROJECT_ENVIRONMENT returned from discovery before Hatch's envs
were added, hiding the env hatch run uses again. Add them on that
path too.

Refs #335

Assisted-by: Claude Code:claude-opus-5-5

* Add Hatch envs on every discovery path

Instead of appending Hatch's envs at each early return, wrap the
whole local discovery so a recorded PDM/uv env, an activated venv,
Pipenv's or Poetry's resolution all keep the project's Hatch envs
visible to stale-install checks, agent mode and VEX.

Refs #335

Assisted-by: Claude Code:claude-opus-5-5

* Keep Hatch envs out of the Pipenv stale probe

Hatch envs are now part of local discovery, so the vendored Pipenv
probe judged them too and told users to fix a Hatch env with pipenv
sync, which never clears it. Skip Hatch's envs there; the project's
own venv still gets the Pipenv warning.

Refs #335

Assisted-by: Claude Code:claude-opus-5-5

* Port #851: fix vex alias tests after #605

main is red: since the store-copy change (#605) the npm resolver
already returns alias and nested-store copies, so two vex_consumed
tests that assumed an alias-free set fail on main and on this
branch. Same change as #851; it no-ops once main carries it.

Refs #335

Assisted-by: Claude Code:claude-opus-5-5

* Judge the venvs Pipenv resolves in its stale probe

Filtering every Hatch-claimed site out of the vendored Pipenv probe
also dropped a venv the two share (a Hatch env with path = .venv),
which pipenv sync does reinstall, so neither probe warned. Judge
exactly the venvs local discovery resolves before Hatch's envs are
added instead.

Refs #335

Assisted-by: Claude Code:claude-opus-5-5

* Move hatch_env test helper above the tests module

Fixes clippy::items_after_test_module under --all-targets.

Co-Authored-By: Claude <noreply@anthropic.com>

* Find Hatch matrix envs in ~/.virtualenvs

When Hatch's env directory is the shared ~/.virtualenvs, only the env
names the project configures were looked up there. Matrix variants
such as test.py3.11 and the hatch-test.py3.X envs that `hatch test`
creates were never found. A stale install in one of them therefore got
no stale-install warning, and VEX could attest over it.

The lookup now builds the matrix names the way Hatch does (Python
variable first as py<version>, matrix-name-format, <env>. prefix
except for default). It also takes hatch-test.* unless the project
configures its own hatch-test env. Checked against Hatch 1.18.1's
`hatch env show`.

Assisted-by: Claude Code:claude-opus-5-5

* Route Gradle digests through utils::digest

main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 659ac2c)

* Keep hatch-test.* envs when hatch-test is set

A project's [tool.hatch.envs.hatch-test] table is layered over Hatch's
built-in hatch-test config, so the default Python matrix still applies
unless the project sets its own matrix. The ~/.virtualenvs lookup
skipped hatch-test.* whenever the table existed at all, which hid
those envs from the stale-install checks and VEX. It now skips them
only when the project defines its own hatch-test matrix. Checked
against Hatch 1.18.1's `hatch env show`.

Assisted-by: Claude Code:claude-opus-5-5

* Format the Hatch discovery changes

rustfmt the two files this branch touches so they match the
repository's formatting; no behavior change.

Assisted-by: Claude Code:claude-opus-5-5

* Resolve .. before the Hatch in-project check

A relative [dirs.env] virtual such as ../envs still started with the
project path, so discovery treated it as an in-project flat directory.
It then claimed every venv in the parent directory and missed the
nested <name>/<id>/<env> envs Hatch actually uses there. Hatch tests
`root in data_directory.resolve().parents`, so the directory is now
resolved like Python's Path.resolve() first, and the project root
itself no longer counts as inside the project.

Assisted-by: Claude Code:claude-opus-5-5

* Fold .. in Hatch env paths on Windows too

canonicalize returns verbatim \\?\ paths on Windows. In those, `/` is
not a separator and the OS does not fold `..`, so a virtual = "../envs"
joined onto the project root stayed one opaque component. It still
counted as inside the project, and reading the directory found
nothing. Config paths are now joined component by component, and
resolve() folds `.` and `..` before it canonicalizes the longest
existing prefix.

Assisted-by: Claude Code:claude-opus-5-5

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
npm alias discovery (a real dir whose own package.json names
name@version, installed under another key) was written twice: the core
resolver's NpmCrawler::alias_copies, used by apply, rollback and the
VEX installed lookup, and a second BFS in vex_consumed
(npm_alias_copies_reusing, real_subdirs, ALIAS_WALK_MAX_DIRS) that only
hosted VEX ran. Since #605 the resolver already returns the ordinary
aliases, so the walk was a second tree walk per hosted vex run whose only
unique output was drift: the resolver skipped a dir whose name matched
the package case-insensitively, the walk only an exact match. On a
case-sensitive file system node_modules/Left-Pad holding left-pad@1.3.0
was a copy for hosted VEX but invisible to apply.

The resolver now skips only an exact own-name dir (the direct probe's
job) and, for a case-only difference, only the dir the probe already
returned in that visit (same_file identity, which is where a
case-folding file system lands the probe). A case-only alias is a copy
on case-sensitive file systems and one physical dir is still never
recorded twice on macOS or Windows.

vex_consumed loses the walk and the alias merge in
hosted_consumed_copies: npm hosted copies are the resolver's set, plus
the identity fallback (store-expanded) when it found none. The walk's
tests are ported onto find_manifest_package_copies_reusing with the same
alias copies (scoped keys, nested trees, workspace members,
--global-prefix), and a core regression test covers the case-only
alias.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

Agent-mode apply skips a bundled copy inside another vlt/pnpm store entry whenever the package is also installed normally, and VEX attests not_affected

3 participants