Repository navigation
Fix agent mode skipping npm-aliased copies (#356) - #738
Merged
Mikola Lysenko (mikolalysenko) merged 4 commits intoOct 5, 2026
Merged
Conversation
Assisted-by: Claude Code:claude-opus-5-5
An npm alias such as "lp": "npm:left-pad@1.3.0" installs the real
left-pad@1.3.0 at node_modules/lp. Agent mode only looked for the
package at node_modules/left-pad, so:
- an alias-only project got package_not_installed from apply;
- a project with a plain copy and an alias patched only the plain
copy, reported success, and vex attested not_affected while
require('lp') still loaded the unpatched file.
The resolver now also treats a real package dir whose own
package.json names the patched name@version as a copy of it, under
any dir name (plain or scoped). Links still never count, so pnpm's
isolated layout and workspace links are unchanged. apply, rollback
and vex all share this resolver, so all three now cover alias copies
under npm, yarn, Bun and pnpm's hoisted linker.
Fixes #356
Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
The installed-tree lookup now returns npm alias installs itself, so hosted VEX's own alias walk mostly finds paths that lookup already returned. Merge them without duplicates. The dispatcher test that pinned the old "alias is missing" behavior now expects the resolver to find node_modules/lp directly. Refs #356 Assisted-by: Claude Code:claude-opus-5-5
Mikola Lysenko (mikolalysenko)
marked this pull request as ready for review
October 4, 2026 02:19
Collaborator
Author
|
BugBot review Generated by Claude Code |
There was a problem hiding this comment.
✅ 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 07824bd. Configure here.
Collaborator
Author
|
Ready for review — head
Generated by Claude Code |
Tanmay Singla (Tanmay182003)
approved these changes
Oct 5, 2026
Mikola Lysenko (mikolalysenko)
deleted the
agent/fix-npm-agent-alias-copies
branch
October 5, 2026 11:34
Mikola Lysenko (mikolalysenko)
pushed a commit
that referenced
this pull request
Oct 5, 2026
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
This was referenced Oct 5, 2026
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
#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)
This was referenced Oct 5, 2026
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
Mikola Lysenko (mikolalysenko)
pushed a commit
that referenced
this pull request
Oct 5, 2026
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
This was referenced Oct 5, 2026
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
#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)
This was referenced Oct 5, 2026
Mikola Lysenko (mikolalysenko)
pushed a commit
that referenced
this pull request
Oct 5, 2026
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
Mikola Lysenko (mikolalysenko)
pushed a commit
that referenced
this pull request
Oct 5, 2026
main has been red since #605 (4646693). Two vex_consumed tests from #738 assumed the name-keyed copy resolver never returns npm-aliased copies, and #605 taught it to. This ports #851's tests-only fix unchanged so this PR's coverage and macOS test jobs can go green; it no-ops once #851 lands on main. Assisted-by: Claude Code:claude-opus-5-5
5 tasks done
Mikola Lysenko (mikolalysenko)
pushed a commit
that referenced
this pull request
Oct 5, 2026
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
6 tasks done
Mikola Lysenko (mikolalysenko)
pushed a commit
that referenced
this pull request
Oct 5, 2026
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
Mikola Lysenko (mikolalysenko)
added a commit
that referenced
this pull request
Oct 5, 2026
* Start fix for #806, #821 Assisted-by: Claude Code:claude-opus-5-5 * Unwind uv vendoring after a relock After vendoring, an ordinary uv relock (`uv add --dev x`, `uv add y`) re-serializes the lock arrays that hold our element: the dev group's requires-dev line and `[manifest] overrides`. Revert matched those arrays by their exact recorded text, so it saw drift and kept uv.lock wired, but still reverted pyproject.toml. The pair then failed `uv sync --locked` while `vendor --revert` reported success. Revert now finds our unchanged element inside the live array under the same key and restores or removes just that element, rendering the array the way uv writes it. A pair gate also writes neither file when any record is genuinely drift-kept, so pyproject.toml and uv.lock always stay consistent. Fixes #806, #821. Assisted-by: Claude Code:claude-opus-5-5 * Test uv revert after a relock with real uv Vendor six, run the uv command that re-serializes the lock array around our element (`uv add --dev zipp` for a dev group, `uv add idna` beside user overrides), then revert. Both files must be unwired with no drift warning, and `uv lock --check` must pass. Refs #806, #821. Assisted-by: Claude Code:claude-opus-5-5 * Document uv revert after a relock Refs #806, #821. Assisted-by: Claude Code:claude-opus-5-5 * Anchor uv array reverts on their key A [manifest] overrides record holds the bare array, and the old convergence shortcut searched the whole lock for it. When the root requires-dist happened to match the user's overrides array, revert treated our element as already gone, left it in uv.lock and deleted the artifact it points at. Every whole-array record now reverts through its own key: an untouched array is restored verbatim, otherwise just our element is. Refs #806. Assisted-by: Claude Code:claude-opus-5-5 * Fail closed when a uv lock array can't be read Revert treated any miss locating a whole-array record as convergence, including a key spelled differently or an unbalanced array. A lock that still routed through the vendored wheel could then lose the wheel. Only a key or section that is provably absent now counts as converged. Anything unreadable is drift, which keeps both files and the artifact. Refs #806, #821. Assisted-by: Claude Code:claude-opus-5-5 * Start fix for #840 Assisted-by: Claude Code:claude-opus-5-5 * Test uv revert after a declaration edit A vendored uv revert writes back the lock specifier it recorded when vendoring. If the user changed the package's requirement in pyproject.toml in the meantime, the lock no longer matches and `uv sync --locked` fails. These tests pin the expected behaviour for requires-dist, requires-dev groups and [manifest] constraints. Refs #840 Assisted-by: Claude Code:claude-opus-5-5 * Re-derive uv specifiers on vendored revert When six is vendored, uv.lock records it as a path source with no version specifier. If the user then changes six's requirement in pyproject.toml (uv add "six>=1.16"), the lock stays byte-identical, and vendor --revert, remove and rollback wrote back the specifier recorded at vendoring time. The revert reported success, but `uv sync --locked` then failed. The revert now writes the specifier pyproject.toml declares now, using the same derivation the hosted unwind uses. This covers requires-dist (each extra separately), requires-dev groups and [manifest] constraints. An unchanged declaration still restores byte-for-byte. When uv's spelling can't be derived, such as a multi-clause range whose clause order varies between uv releases, the revert keeps both files and warns vendor_lock_entry_drifted instead of breaking the lock. Fixes #840 Assisted-by: Claude Code:claude-opus-5-5 * Document uv revert after a declaration edit Refs #840 Assisted-by: Claude Code:claude-opus-5-5 * Adapt uv specifier re-derivation to main #625 on main changed the uv declaration reader to report each optional-dependencies member's extra. The merge of main into this branch no longer compiled. Use that reader for requires-dist instead of the local extras walk. Dev groups now also pick up main's group-name normalization and include-group expansion. Refs #840 Assisted-by: Claude Code:claude-opus-5-5 * Pick the declaration a uv lock entry mirrors When a package is declared twice, for example under two environment markers, or directly and through an include-group, the revert kept the recorded specifier whenever any one declaration still matched it. Edit just one of them and the stale pin came back, with the same broken `uv sync --locked` as #840. The revert now picks the declaration by the entry's own marker, as the hosted unwind does. Declarations that still disagree after that are treated as drift, and both files are kept. Refs #840 Assisted-by: Claude Code:claude-opus-5-5 * Tighten uv revert specifier re-derivation Two cases Bugbot found on the vendored uv revert: - A same-name declaration that isn't a plain version range, such as an extra pinned with ===, stopped every entry from following its edited declaration. Now only the entry whose own declaration is unreadable keeps its recorded spelling. - After a bound was dropped, the restored { name = "six" } element also matched another dependency entry in uv.lock, so a drifted wiring could pass as already reverted. The check now looks only in the root unit's requires-dist array. Refs #840 Assisted-by: Claude Code:claude-opus-5-5 * Format the uv revert re-derivation changes Assisted-by: Claude Code:claude-opus-5-5 * Port #851: fix vex alias tests broken on main main has been red since #605 (4646693). Two vex_consumed tests from #738 assumed the name-keyed copy resolver never returns npm-aliased copies, and #605 taught it to. This ports #851's tests-only fix unchanged so this PR's coverage and macOS test jobs can go green; 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
* Fix vendored gem rewrite breaking positional args Vendoring a gem declared with a splat, constant or method-call version (`gem "rack", *V`, `gem "rack", VERSION`, `ENV.fetch(...)`) wrote that argument after the new `path:` keyword. Ruby rejects that, so every later `bundle` command failed to parse the Gemfile even though vendor reported success and VEX attested the patch. The exact pin supersedes these constraints, so they are now dropped like quoted ones. Keyword options such as `require: false` and a trailing comment still follow `path:`. A real-bundler e2e checks the rewritten Gemfile installs frozen and loads the vendored copy. Fixes #847 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)
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
#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
…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 #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 #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 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 #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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
LLM Description written by Claude Code:claude-opus-5-5
Fixes #356
Summary
Agent mode now treats an npm alias install (
"lp": "npm:left-pad@1.3.0", which puts the realleft-pad@1.3.0atnode_modules/lp) as an installed copy ofpkg:npm/left-pad@1.3.0.apply,rollbackandvexall go through the same resolver, so all three now cover alias copies under npm, yarn, Bun and pnpm's hoisted linker.Before: an alias-only project got
package_not_installedfromapply. A project with a plain copy plus an alias got only the plain copy patched,success, and a VEXnot_affectedstatement whilerequire('lp')loaded unpatched code.Root cause
NpmCrawler::find_by_purls(crates/socket-patch-core/src/crawlers/npm_crawler.rs) only probes<node_modules>/<purl-name>, andvisit_resolver_dirrequires the dir name to equal thepackage.jsonname. An alias dir's name never equals its package's name, so it was never a candidate.Change
visit_resolver_dir(importer-tree visits only) addsalias_copies: real package dirs, plain or under a@scope, whose ownpackage.jsonname@versionis a pending target while the dir is named otherwise. It also checks nestednode_modules, because the BFS visits them.npm linktarget, so pnpm's isolated layout and Fix agent mode patching linked first-party source (#626) #634's first-party-link handling are unchanged. A dir whose name is its own package name (in any ASCII case) is left to the direct probe, so one physical dir is never recorded twice on case-insensitive filesystems.get,vendor) still pick it.npm_crawler/oracle.rs) mirrors the rule, so the randomized tree tests keep comparing like with like. Their generator already produces alias installs.vex_consumed::npm_alias_copies) now mostly re-finds paths the installed-tree lookup already returns, so its results are merged without duplicates.Wrappers (
npm/,pypi/,gem/) only dispatch to the binary, so they need no change.Test evidence
Red→green (each new test was run with the
alias_copiescall disabled, then enabled):package_not_installed)npm_crawler::tests::find_by_purls_resolves_an_alias_only_installnpm_crawler::tests::find_by_purls_returns_alias_copies_beside_the_plain_copynpm_crawler::tests::find_by_purls_resolves_scoped_alias_installs--vex)e2e_embedded_vex::apply_vex_patches_npm_alias_copiesvexrefuses while an alias copy is unpatchede2e_vex::verify_mode_requires_npm_alias_copies_patchednpm_crawler::tests::find_by_purls_does_not_take_a_link_as_an_alias_copyReal toolchains (Linux, hand-staged manifest + blobs,
apply --offline --vex):left-pad+lp+@x/pad: applied 3, every copy patched,require('lp')loads patched bytes, VEXnot_affected.rollback --offlinerestores all three. With onlynode_modules/left-padpatched,vexexits 1:omitting pkg:npm/left-pad@1.3.0 … (not_applied).node-linker=hoisted(lp+left-pad): applied 2, both patched.lp+left-pad): applied 2, both patched.Local checks:
cargo clippy --workspace --all-features -- -D warnings: clean.cargo fmt: changed hunks are formatted.mainitself is not fmt-clean (498 diffs), so I only formatted my own hunks.cargo test --workspace --all-features: 213 suites ok. The 12 failures are all permission/write-failure tests (chmod 0555-based, e.g.covgap_commands_vendor::*_state_write_failure_*,vlt_healunremovable-lock,copy_treerelax loop) that cannot fail as root in this sandbox. They don't touch the resolver, and CI runs them as non-root. The 13th,ecosystem_dispatch::tests::npm_crawl_snapshot_matches_the_crawls_it_replaces, pinned the old "alias is missing" behavior and is updated in 07824bd.node --test npm/socket-patch/bin/socket-patch.test.mjs: 4/4.Follow-ups (not in this PR)
vex_consumed::npm_alias_copies(the hosted-VEX alias walk) is now largely redundant with the resolver and could be deleted in favour of it. Left in place to keep this change small.dependenciesmirror for aliases) is a different code path (the hosted lock rewriter), not this resolver.Note
Medium Risk
Changes core npm package discovery used by apply, rollback, and VEX; incorrect alias/link handling could patch wrong dirs or miss copies, but behavior is heavily tested and scoped to importer-tree alias detection.
Overview
Fixes #356 by teaching the npm installed-tree resolver to treat npm alias installs (e.g.
"lp": "npm:left-pad@1.3.0"→ real package atnode_modules/lp) as additional copies of the target PURL, alongside plainnode_modules/<name>paths.NpmCrawler::find_by_purls/visit_resolver_dirnow runsalias_copieson importer-treenode_modules: it scans real package dirs (including scoped layouts) whosepackage.jsonname@versionmatches a pending target while the directory name differs, skips symlinks and dirs that already match their package name (avoids double-counting), and keeps plain copies first in the result order. The async oracle mirrors the same rule for randomized equivalence tests.apply,rollback, andvexall use this resolver, so alias-only trees no longer getpackage_not_installed, mixed plain+alias trees get every copy patched/verified, and VEX no longer attestsnot_affectedwhilerequire('lp')still loads pristine bytes. Hosted VEX path merging dedupes alias walk results against paths the resolver already returned. CLI_CONTRACT documents alias installs as supported copies.Coverage adds unit tests for alias-only, plain+alias+nested, scoped aliases, and “links are not alias copies”, plus e2e tests for
apply --vexandvexverify mode; snapshot/dispatch tests expectleft-padresolved vianode_modules/lp.Reviewed by Cursor Bugbot for commit 07824bd. Configure here.
Generated by Claude Code