Repository navigation
fix: retry npm ERESOLVE with the smallest re-resolve, blocked report otherwise (UPT-107) - #224
Merged
Merged
Conversation
…otherwise (UPT-107)
npm's lock-only install could ERESOLVE from the old lockfile although the
same manifest resolves fresh (enki: react 19 with
@entur/react-component-toggle), and fix stopped with exit 2 while
fix --json printed nothing.
On ERESOLVE, upgradeInstall now retries lock-only without the lockfile
entries of the moved packages, their companions and every locked package
that declares one of them as a peer; only then with the whole lockfile
re-resolved. The step that worked is recorded (lockfile.eresolveRetry,
shown in the summary and the PR body), the parent-commit comparison
starts from the same lockfile, and the lock guard reports churn as usual.
It never uses --legacy-peer-deps or --force. When every step fails, fix
ends with INSTALL_BLOCKED, naming the packages from npm's ERESOLVE report.
fix --json now prints JSON on every exit: { error, blocked? } on a failure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Lucas Braum <lucasbraum@gmail.com>
…107) The enki rerun got past ERESOLVE, ran the agent, then stopped at "Required migration evidence exceeds the PR body budget". That refusal is thrown after report.json is written in the clone, and the clone is removed, so fix --json printed only the error: no cost, no lockfile, no install step. The refusal is now a ReportedError (PR_BODY_TOO_LARGE) that carries the report across the worker boundary; fix --json prints the report with the error on top. Whether the run should stop there at all is left as is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lucas Braum <lucasbraum@gmail.com>
Merged
lucasbraum
added a commit
that referenced
this pull request
Oct 18, 2026
) Closes UPT-106. Closes UPT-102: enki now delivers an agent-led draft, uptide-bench/entur--enki#5. ## Problem `prBody()` threw "Required migration evidence exceeds the PR body budget" when the rank-0 sections alone were over the budget. That threw away the whole finished run. It happened on enki (a8ff50ca, after #224) and on dms-saas (#221). ## Change - **No section is exempt.** `fitPieces` returns `undefined` instead of throwing. Required lists that can grow without bound get a compact form, which is used only when the whole list does not fit. These lists still go before anything droppable. - Lockfile churn (outside the upgrade, and parent churn): counts per kind of change (added, removed, version changed, re-resolved at the same version), then the first 10 packages. - Manual sites under "Worth a look": grouped by rule, with a count and the first 5 locations each. - Remaining type errors: the first 10, then a count. - **Full lists** stay in report.json. The Action's job summary now lists every breaking site (it was capped at 50). Compact forms link to it through a new `runUrl` field, set from `GITHUB_SERVER_URL`/`GITHUB_REPOSITORY`/`GITHUB_RUN_ID` when the run is in Actions. - **Minimal body.** If the compact form still does not fit, the body is a fixed-size summary: the verdict, the checks (types, tests, builds, lint, clean install), the counts (sites by outcome, lockfile, files, decisions), the branch and commit, and where the full report is. It opens with a "Shortened description" notice. - **The run is kept.** `fix()` and the isolated run's `finish()` write report.json before the body, and nothing throws for size any more, so a finished run always keeps its commits and report.json. `uptide pr` sends the shortened body instead of refusing. - **Decision lines** (second commit, found by the enki rerun). An agent decision named every file in its task's scope. The verification-feedback task was scoped to 317 files, so its 2 decisions came to 46 KB. A decision now names the files the task changed, five at most, and counts the rest. ## Fixtures Written first; both failed on main with the throw. - 300 manual sites (3 rules) and 100 + 100 churned lockfile entries. At the 60 k budget every section is present: lockfile sections, risk table, What changed, Worth a look, Decisions, Verification details, Run details. Manual sites are compact and the lockfile lists are whole. At 8 k the lockfile lists are compact too: "100 entries: 25 added, 25 removed, 25 re-resolved at the same version, 25 version changed", with the first 10 packages. - The same report with an oversized decision renders the minimal body. - A 317-file task with one changed file: the decision names only that file. Without diffs it names 5 files and "312 more files". ## enki rerun (#169 harness) Uptide arm only, claude-sonnet-5-5, default $1 cap, one run: 60949226. | | | |---|---| | Exit | 0, draft delivered (branch `uptide/react-19.2.0`, verified commit `3f05f25`) | | PR body | 804 chars: minimal fallback (on 06bab33). With 16b2abe the same stored report renders whole at 34,718 chars (77,681 before), no shortening | | Checks (harness) | install 0 · typecheck 0 · lint 0 · test 0 | | #220 clean install | `npm ci --ignore-scripts --no-audit --no-fund` passed | | Sites | 3 by rule, 8 by the agent, 1 manual; verification PASS, 0 new type errors | | Cost / wall time | $0.17 (estimate $0.54) · 160 s | | Fork CI | uptide-bench/entur--enki#5: npm ci, `npm run check` and tests passed. The job fails at SonarQube Scan (no `SONAR_TOKEN` in the fork), so build is skipped. Fork PRs #1–#4 fail the same way | The run used the build of 06bab33. The fork PR's body is therefore the minimal form; the 34.7 KB figure is a $0 re-render of the stored report.json with 16b2abe. The report records `uptideDirty` because the harness files under `bench/` were copied in untracked, as in earlier reruns. ## Left as is - Lockfile churn is not in the job summary. The run-report schema (`uptide.run/v1`) has no lockfile field, so the full churn list is only in report.json. - `ReportedError` and `PR_BODY_TOO_LARGE` stay. Nothing throws `PR_BODY_TOO_LARGE` any more, but `fix --json` still uses `ReportedError` for reporting any failure after report.json is written. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Lucas Braum <lucasbraum@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
lucasbraum
pushed a commit
that referenced
this pull request
Oct 18, 2026
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or [setup this action to publish automatically](https://github.com/changesets/action#with-publishing). If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## uptide@0.15.1 ### Patch Changes - [#220](#220) [`cf86d65`](cf86d65) Thanks [@lucasbraum](https://github.com/lucasbraum)! - With an assisting agent, a root pin Uptide cannot update safely (a nested override such as `overrides > pkg > peerDependencies > react`) no longer stops `fix` with exit 2: the run continues as an agent-led draft that leaves the pin as written and names it, with its exact path and value, at the top of the PR body. The agent-led peer reason now names each blocker's first release that accepts the target, or says none does. An agent-led draft must pass the repository's clean install (`npm ci` or the manager's frozen install) before delivery; if it does not, the PR says so at the top. Without an agent, with `--no-llm`, or on a safety refusal, `fix` still exits 2. - [#232](#232) [`e4a2885`](e4a2885) Thanks [@lucasbraum](https://github.com/lucasbraum)! - `uptide fix` spends less wall time on test runs and dependency copies: - The agent's own `run_tests` check runs only its runner's related tests when the `test` script builds first and then runs a script that is just the runner (`pnpm run build && pnpm run test:unit`). The gate after the agent's edits and the final verification run exactly what they ran before. - The upgrade's install moves the installed `node_modules` out of its temporary worktree instead of copying it. - Each phase of the fix report lists the repository's commands it ran (installs, generators, builds, type-checks, tests, lint, format) with their count and summed wall time. An agent-led run's clean install has its own phase. - [#223](#223) [`2dc2162`](2dc2162) Thanks [@lucasbraum](https://github.com/lucasbraum)! - `fix` no longer stops with "baseline tests modified the working tree" when the test run only writes new untracked files, such as a junit `test-report.xml` that is not gitignored. Those files are recorded as test artifacts (`testArtifacts` in the report), kept out of every commit, the agent's diff and the pending-changes check, and removed before each later test run, `uptide verify` included. A change to a tracked file during baseline tests still refuses the run. - [#215](#215) [`abddcb3`](abddcb3) Thanks [@lucasbraum](https://github.com/lucasbraum)! - `check` no longer lists possible impact (type changes nothing confirmed as breaking) on its first screen: each package says it in one line, `+N possible impact in M files, not counted as breaking`, and `--details` and `--json` still have every site. A package whose types were partly verified no longer repeats its coverage line in the heading, and a monorepo's unbuilt workspace chains are one count on that line. - [#213](#213) [`2de3a18`](2de3a18) Thanks [@lucasbraum](https://github.com/lucasbraum)! - A companion pack that moves with the lead's fix (react beside next) now gets its own scan context and inventory, and the PR body carries a section with its review notes. - [#212](#212) [`733cfbe`](733cfbe) Thanks [@lucasbraum](https://github.com/lucasbraum)! - `pack test --write` records how many breaking sites a finding landed on at any severity and as breaking, out of all breaking sites it scored, and the pack tables show both ("Found, any severity", "Found as breaking") in place of the single recall column. Packs recorded before it show `—`. - [#224](#224) [`09f68df`](09f68df) Thanks [@lucasbraum](https://github.com/lucasbraum)! - `fix` on npm no longer stops when the lock-only install hits ERESOLVE from the old lockfile. It retries without the lockfile entries of the moved packages, their companions and the packages whose peers they satisfy, and only then with the whole lockfile re-resolved; the report says which step worked, and the lockfile check reports the churn as usual. It never uses `--legacy-peer-deps` or `--force`. When every step fails, `fix` ends blocked, naming the conflicting packages from npm's ERESOLVE report. `fix --json` now prints JSON on every exit, a failure included; when the run already has its report (a PR body over GitHub's size limit), that report comes with the error. - [#221](#221) [`ff8de8e`](ff8de8e) Thanks [@lucasbraum](https://github.com/lucasbraum)! - `fix` no longer rejects an agent patch for test files the parent commit already failed, whichever test scope ran them: when the test script builds first and the build failed on the error being fixed, the start had no test results to compare with, so a correct patch was reverted every round. The patch's tests also run with the pack's follow-ups in place (a test asserting the old Stripe API version), which `fix` still applies and reports after the agent. When a PR body cannot fit, the stored report.json the error points to is now written first. `check` and `fix` now also analyze files that reach a package's wrapper through a barrel (`import { getStripeClient } from '../stripe'` with `export * from './client'`). Edited files are formatted with oxfmt and dprint as well as biome and prettier, from the workspace that holds them, with its own configuration, else the root's. The stripe pack adds `request-options`: request options passed in the params slot (`subscriptions.cancel(id, { idempotencyKey })`), which SDK 22 rejects, move to the third argument. It runs assisted until a ground-truth repository exercises it. - [#226](#226) [`448c3cb`](448c3cb) Thanks [@lucasbraum](https://github.com/lucasbraum)! - `uptide fix` judges consumed workspace builds on the final head, not on the tree right after the bump. A build that failed after the bump and passes once the edits land no longer starts a feedback round, adds an "Agent-led draft" reason or shows "Build failed" in the PR body. The builds run again only when the tree changed since their last result, and not at all when the verification build already built those workspaces. The fix report's `buildRuns` gives the number of builds the run executed and their wall time. - [#218](#218) [`13e343b`](13e343b) Thanks [@lucasbraum](https://github.com/lucasbraum)! - `check` folds a compile error into the breaking site it comes from when the compiler traces its value there (an `errors.map(e => e.message)` under a `result.error.errors` that zod 4 no longer types, and the narrowing that depended on it). The rule's line shows `+N downstream errors`, `--details` lists them under their site, and `--json` carries them as that finding's `downstream`. They are no longer separate breaking sites. `fix` still verifies against every error. - [#214](#214) [`195ae10`](195ae10) Thanks [@lucasbraum](https://github.com/lucasbraum)! - A rewrite rule below 80% precision over at least 5 scored ground-truth sites now runs as assisted in `fix` (react-router `dom-imports`); smaller samples (next `middleware-proxy`) stay mechanical and are flagged in the compatibility table. Pack status is unchanged. - [#218](#218) [`13e343b`](13e343b) Thanks [@lucasbraum](https://github.com/lucasbraum)! - pack test runs a ground-truth repository's code generators (prisma generate, local graphql-codegen) after its install, as fix does. It fetches the Prisma engines the locked version needs itself, so generators stay off the network. A repository whose generator still fails is incomplete and fails the pack, unless its ground truth declares `withoutGenerated` - [#227](#227) [`ae175a9`](ae175a9) Thanks [@lucasbraum](https://github.com/lucasbraum)! - A migration whose evidence is larger than GitHub's PR description limit no longer stops `fix` with "Required migration evidence exceeds the PR body budget" and loses the run. Long required lists get a compact form when they do not fit. Lockfile churn shows counts per kind of change and the first packages. Manual sites are grouped by rule, with counts and the first locations. Remaining type errors show the first entries. Every full list stays in report.json and, in the Uptide Action, in the job summary, which the description links to. If even the compact form does not fit, the description is a short summary that says so at the top: the verdict, the checks, the counts and where the full report is. The branch and report.json are always kept. An agent's decisions under "Decisions for you" name the files the agent changed, five at most, and count the rest, instead of every file in the task's scope. A verification-feedback task scoped to the whole repository made each of its decisions a list of hundreds of files. - [#219](#219) [`12dc612`](12dc612) Thanks [@lucasbraum](https://github.com/lucasbraum)! - `fix zod` rewrites `z.string().uuid()` to `z.guid()` by rule without `--include-deprecated`: zod 4's `uuid()` also checks the RFC version and variant, so ids zod 3 accepted stop validating, and `check` now counts the site as breaking. `fix` no longer reports false pre-existing type errors on a TypeScript 7 repository whose `lib` names a library newer than its `target` (`lib: ["ES2023"]` under `ES2022`). Co-authored-by: uptide-release[bot] <338861287+uptide-release[bot]@users.noreply.github.com>
lucasbraum
added a commit
that referenced
this pull request
Oct 18, 2026
…LVE retry (UPT-109) (#234) Closes UPT-109 once the enki check below is done. Refs UPT-107 (#224). ## What and why In the 0.15.1 pilot (#169), enki's Uptide PR installed and passed tests, but the type-check failed: 3 files import `@react-types/datepicker`, which enki uses without declaring it, and the new lockfile no longer had it. **Cause.** The UPT-107 "moved entries" retry drops every package that declares `react` as a peer, and that includes `@react-aria/datepicker`. It re-resolved from 3.15.2 to 3.17.1, and 3.17.1 no longer depends on `@react-types/datepicker`. Nothing required that package any more, so npm pruned it. **Fix** (`managers/upgrade.ts`, npm only): - After a retry step succeeds, compare the top level of the old and new lockfile (root and workspaces). Then scan tracked `.[cm]?[jt]sx?` files for bare `from` / `import` / `require` specifiers. A package that left the top level and is imported is a dropped import. - If there is one, the same step runs again. It keeps the dropped package and every locked package that requires it, transitively, at their locked versions. Moved packages are never kept. - If that retry hits ERESOLVE, the first retry's lockfile stands. - `lockfile.undeclared` (`{ name, version, kept }`) lists each one. The CLI summary and the PR body show it under "Imported but not declared": the package is imported but not declared in package.json, and it was kept at a version or the new lockfile no longer has it. Both suggest adding it to package.json. - The same report also covers a normal install with no retry, where the only possible outcome is reporting it. - `package.json` is never changed. On enki this should keep `@react-aria/datepicker` (its react peer range already accepts 19) and its requirers at their old versions, and so keep `@react-types/datepicker` 3.13.2. ## Tests - `eresolve.test.ts`, stand-in npm (now resolves dependencies transitively and prunes unrequired entries, like npm): - **kept**: `picker` peers on the moved `ui`, so the first retry drops it. `picker` 1.1.0 no longer brings `picker-types`, which `src/date.ts` imports. The second retry starts with `picker` and `picker-types` locked, and the lockfile keeps both at 1.0.0. The report has `undeclared: [{ picker-types, 1.0.0, kept: true }]`, and `package.json` is unchanged. - **cannot keep**: `picker` 1.0.0 conflicts too, so the second retry hits ERESOLVE. The first retry's lockfile stands (`picker` 1.1.0, no `picker-types`), with `kept: false`. - Both failed on main before the fix. - `format-fix.test.ts`, `report.test.ts`: the summary and PR body lines for both kept and not kept. - Locally: `pnpm lint` (only the existing warning/info from main), `pnpm typecheck`, core 2296 passed, cli 517 passed (after `pnpm build`, which the notices and telemetry tests need). ## Not done - **enki not re-run.** No bench runs for this PR, so the type-check result on enki is still unconfirmed. Running the #169 harness's Uptide arm on enki would confirm it. - pnpm and yarn are not covered. The retry is npm only, and their strict layouts already break undeclared imports. - The import scan is a regex over JS/TS files. It does not read `.vue` / `.svelte` files or `/// <reference types>`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Lucas Braum <lucasbraum@gmail.com> Co-authored-by: Claude Opus 5.5 <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.
Closes UPT-107. Refs UPT-102 (not closed: enki delivered no draft and did not end blocked; see below).
What and why
On enki (UpgradeBench, #169),
npm install --package-lock-onlyfailed with ERESOLVE from the parent's lockfile, although the same manifest resolves fresh.fixstopped with exit 2, andfix --jsonprinted nothing.Staged retry (
managers/upgrade.ts). For npm only, and only on ERESOLVE, the lock-only install is retried from smaller lockfiles, smallest re-resolve first:--legacy-peer-depsand--forceare never used (the tests assert it on every install call).lockfile.eresolveRetry({ step, dropped, conflicts }). It shows in the text summary and the PR body ("Lockfile re-resolved").parentLock) starts from the same reduced lockfile. So the lock guard reports churn as usual: what the fresh resolve moves at the parent too shows asparentChurn, not as the upgrade's.fixends withINSTALL_BLOCKED(InstallBlockedError). Its message lists the package lines from npm's ERESOLVE report (While resolving / Found / Could not resolve dependency / Conflicting peer dependency /peer … from …). It crosses the worker boundary intact.fix --jsonon every exit. Exit 0 and 1 already printed the report. On a failure (exit 2, and exit 1 from aCliError),fix --jsonnow prints{ error: { code, message } }. A blocked install addsblocked: { attempts, conflicts }. This is scoped tofix:pralready writes its own error JSON and then rethrows.Second commit, found by the rerun. The paid enki run got past ERESOLVE and ran the agent. It then stopped at "Required migration evidence exceeds the PR body budget". That refusal is thrown after
report.jsonis written in the clone, and the clone is then removed. So the JSON had the error but no report: no cost, no lockfile. The refusal is now aReportedError(PR_BODY_TOO_LARGE) carrying the report, andfix --jsonprints the report witherroron top. Whether the run should stop there at all is unchanged here (follow-up).Tests
managers/eresolve.test.tsuses a stand-in npm: a small registry, keeps locked versions, and fails with npm's real ERESOLVE text (synthetic names,fixtures/lockfiles/eresolve/npm-11.txt) while a conflict holds.toggle, ui, ui-domand keepsunrelatedlocked; toggle re-resolves to 1.1.0.parentChurn, andoutOfScopeis empty.INSTALL_BLOCKEDafter exactly 3 attempts, with the conflict lines.cli.test.ts:fix --jsonexit 2 prints JSON for a blocked install (withblocked), for any other failure, and for aReportedError(with the report). The text output carries the same message.format-fix.test.ts: the summary names the retry step.pnpm lint,pnpm typecheck,pnpm test: green locally (core 2271, cli 515).pnpm public:checkwith the denylist: clean.enki rerun (#169 harness, Uptide arm only, claude-sonnet-5-5, $1 cap)
Run
a8ff50ca, from this branch's first commit (fd3037f).Required migration evidence exceeds the PR body budget(after install, agent and verification)--json{ error }(before the second commit it carried no report)npm ci --ignore-scriptspassed in the replay; the paid run's result was lost with its report$0 replay. The same CLI and flags were run on a copy of the run's clone, with
--keep-tempand an invalid API key so every agent call fails. It takes the same agent-led plan: react, react-dom 19.2.0, @types/react, @types/react-dom,@entur/react-component-toggle1.0.0, react-leaflet 5.0.0, unsafe@reach/*overrides left alone. The lock-only install hit ERESOLVE (Found: @entur/react-component-toggle@0.0.3/Conflicting peer dependency: react@19.2.0). Step 1 resolved it after dropping the entries of 82 react-peer packages; step 2 was not needed. It ended at exit 1 with a 23 KB PR body. In the paid run, the agent's edits pushed the required (never-trimmed) evidence past GitHub's 65,536-character limit.UPT-102 stays open: enki now installs, but its draft is refused at the PR body.
🤖 Generated with Claude Code