Back to Pipenv

Plan: Pipenv Codebase Modernization

docs/dev/modernization-plan.md

2026.8.0112.7 KB
Original Source

Plan: Pipenv Codebase Modernization

Generated: 2026-05-12 Input: docs/dev/modernization-prd.md Branch base: maintenance/code-cleanup-2026-05 (cut from main)

Overview

This plan operationalizes the six initiatives in the modernization PRD as discrete, swarm-executable tasks. Per the scoping decision:

  • Wave 1 + Wave 2 are planned at PR-granularity (Initiatives A, B, C). These are concrete enough for parallel agent execution today.
  • Wave 3 + Wave 4 are scaffolded only (Initiatives D, E, F). Each has a small set of seed tasks; the full plan for these is regenerated after Wave 1+2 lands and the codebase shape is known.

Per the autonomy decision: agents run pytest and create commits on the shared branch. Matt reviews on-branch and merges to main when satisfied. No agent force-pushes, no agent merges to main, no agent touches patched/ or vendor/.

Two PRD corrections were uncovered during ground-truth verification and are encoded as T0 tasks below:

  1. Initiative C describes a state dict that does not exist; the actual pattern is wide individual parameter lists threaded through routines.
  2. markers.py is described in Initiative B as inlined former vendor; it is actually project-owned glue and should be removed from the triage set.

Prerequisites

  • Branch maintenance/code-cleanup-2026-05 exists locally (already cut).
  • python -m pytest tests/unit passes on the branch tip (verified 2026-05-12 in this session: 483 passed, 9 skipped).
  • Agents may use python -m pytest tests/unit and (where feasible) tests/integration subsets. Agents do not run the full integration suite — it requires network access and live PyPI mirrors; that gate is CI's job after PR push.
  • Agents do not modify anything under pipenv/patched/ or pipenv/vendor/.
  • Agents commit with a Co-Authored-By trailer naming the agent and never force-push.

Dependency Graph

T0.1 ── T0.2 (PRD corrections folded in here + agent ops doc)
   │
   ├── Initiative A: T_A.1 ── T_A.2 ── T_A.3 ──[release cut]── T_A.4
   │
   ├── Initiative B: T_B.1 ─┐
   │                  T_B.2 ─┤
   │                  T_B.3 ─┼── T_B.5 ── T_B.6
   │                  T_B.4 ─┘     │
   │                               └── T_B.7 (execute "delete" decisions)
   │
   ├── Initiative C: T_C.2 ── T_C.3 ── T_C.4 ── T_C.5 ── T_C.6 ── T_C.7
   │                                       │
   │                                       ├── T_C.8 (parallel-safe)
   │                                       └── T_C.9 (parallel-safe)
   │
   ├── Wave 3 seed: T_D.1 ── T_D.2
   │                T_E.1 (gated on T_B.7)
   │
   └── Wave 4 seed: T_F.1 (gated on T_E.1)

Tasks

Wave 0 — Corrections & agent operating notes

T0.1: Correct PRD where it diverges from code

  • depends_on: []
  • location: docs/dev/modernization-prd.md
  • description: Apply two corrections discovered during verification:
    1. Initiative C ("Replace the ambient state dict in routines"): rename to "Replace wide threaded parameter lists in routines". Update the "Current state" paragraph to describe the actual pattern — e.g. do_install takes 17 positional/keyword parameters and threads subsets through handle_new_packages (11 params), handle_lockfile (10), do_init (10), etc. The target state and approach do not change materially: a typed RoutineContext dataclass replaces the ambient set of arguments.
    2. Initiative B: remove markers.py from the list of suspect "inlined former vendor" modules. markers.py imports from pipenv.patched.pip._vendor.{distlib,packaging} and is project-owned glue; it does not have a vendor lineage to triage. The triage set remains requirementslib.py, requirements.py, and the URL/path-overlapping helpers in fileutils.py.
  • validation: PRD section text reflects the corrections; git diff shows only those two passages changed; no behavioural code changed.
  • status: Completed
  • log:
    • Initiative C retitled to "Replace wide threaded parameter lists in routines"; current-state paragraph rewritten to cite actual parameter counts (do_install: 17, handle_new_packages: 11, handle_lockfile: 10, do_init: 10). Target state and Approach left untouched.
    • Initiative B current-state list updated: markers.py removed from the suspect set; requirements.py and URL/path helpers in fileutils.py retained alongside requirementslib.py.
    • git diff confirmed only the two intended passages changed.
  • files edited/created:
    • docs/dev/modernization-prd.md

T0.2: Document agent operating procedure

  • depends_on: []
  • location: docs/dev/swarm-ops.md (new)
  • description: Short page (~50 lines) covering: which branch to push to (per-initiative feature branches off maintenance/code-cleanup-2026-05), the required pre-commit checklist (pytest unit suite green, ruff check pipenv/ green, Co-Authored-By trailer present), what's off limits (patched/, vendor/, pipenv/__version__.py, CHANGELOG.md), commit-message conventions (matching existing patterns: refactor: ..., chore: ..., etc.), and how to surface review-requested items (TODO comments tagged TODO(swarm) so they're greppable).
  • validation: File exists, lints clean as markdown, references real commands that succeed on the current tree.
  • status: Completed
  • log: Wrote 95-line operating manual covering all seven required sections (scope/branch, pre-commit checklist, off-limits paths, commit conventions, Towncrier news fragments, review-flagged TODOs, and task-complete definition). Verified python -m pytest tests/unit -x and ruff check pipenv/ both exit 0 on the current tree before documenting them as the validation commands.
  • files edited/created: docs/dev/swarm-ops.md (new)

Wave 1 — Initiative A: URL/path utility consolidation

T_A.1: Inventory & canonical-home decision doc

  • depends_on: [T0.1, T0.2]
  • location: docs/dev/initiative-a-inventory.md (new, temporary)
  • description: Produce a short inventory table for the module-level symbols of pipenv/utils/internet.py (top-level defs and the PackageIndexHTMLParser class — public symbols include is_valid_url, is_pypi_url, replace_pypi_sources, create_mirror_source, download_file, get_host_and_port, get_url_name, is_url_equal, proper_case, write_credentials_netrc, get_requests_session, plus the underscore-prefixed helpers _strip_credentials_from_url and _read_existing_netrc_content which are imported from elsewhere in pipenv/ and so are effectively public) and pipenv/utils/fileutils.py (module-level symbols including is_file_url, is_valid_url, url_to_path, normalize_path, normalize_drive, path_to_url, open_file, temp_path, create_tracked_tempdir, check_for_unc_path). For each symbol record: caller count (greppable), suggested canonical home (internet for URL/scheme concerns; fileutils for filesystem path concerns), and overlap notes. Mark is_valid_url as the explicit duplicate. Mark URL↔path conversion helpers (url_to_path, path_to_url, is_file_url) as candidates to keep in fileutils.py (they're filesystem-flavored URL handling) but to be cross-referenced from internet.py.
  • validation: Doc exists, table is complete, every public symbol appears exactly once with a canonical-home decision.
  • status: Completed (commit 39d80a17; re-committed after filesystem freeze corrupted the original commit's object writes — content was intact in the working tree)
  • log: Wrote 132-line inventory covering 13 internet.py symbols and 10 fileutils.py symbols with caller counts and canonical-home decisions. Surfaced a separate, latent duplicate path_to_url in pipenv/utils/shell.py:104 with zero internal callers (different implementation than fileutils' version); flagged for T_A.2 sweep rather than a new task.
  • files edited/created: docs/dev/initiative-a-inventory.md (new)

T_A.2: Move is_valid_url to canonical home with deprecation signal

  • depends_on: [T_A.1]
  • location: pipenv/utils/internet.py, pipenv/utils/fileutils.py, news/ (new fragment).
  • description: is_valid_url already exists in both modules with identical implementation. Per T_A.1's decision, keep the implementation in pipenv/utils/internet.py. In pipenv/utils/fileutils.py, replace the local definition with a shim that re-exports the canonical function and emits a DeprecationWarning on use, so any external consumer importing pipenv.utils.fileutils.is_valid_url gets a visible signal during the deprecation window. Intra-module call sites in fileutils.py (notably the call at pipenv/utils/fileutils.py:160) are updated in this task to import the canonical name directly from internet, so they do not trip the warning themselves. Add a news fragment under news/ documenting the deprecation per the project's Towncrier convention (.removal.rst or .deprecation.rst — match existing fragments in the directory). Do not change any non-fileutils caller's import line in this task.
  • validation:
    • grep -n "def is_valid_url" pipenv/ returns exactly one result.
    • python -c "import warnings; warnings.simplefilter('error'); from pipenv.utils.fileutils import is_valid_url; is_valid_url('https://x/y')" raises DeprecationWarning.
    • python -c "from pipenv.utils.fileutils import is_valid_url" still succeeds (no ImportError).
    • Intra-module call at the old pipenv/utils/fileutils.py:160 site exercises the canonical internet import (verifiable by reading the diff).
    • Unit suite green; new news fragment is recognized by the project's Towncrier config.
    • Strict-mode caveat (added post-execution): A plain python -W error::DeprecationWarning -m pytest tests/unit cannot pass in this environment because pytest_asyncio emits a PytestDeprecationWarning during pytest_configure (which subclasses DeprecationWarning). Use the pytest-scoped form instead: python -m pytest tests/unit -x -W "ignore::pytest.PytestDeprecationWarning" -W "error::DeprecationWarning".
  • status: Completed (commit b144f64c; sweep follow-up 3a81cce1 removed the dead duplicate path_to_url in pipenv/utils/shell.py that T_A.1 and T_B.3 had flagged)
  • log: Replaced the local is_valid_url def in fileutils.py with a DeprecationWarning-emitting shim that re-exports the canonical function from pipenv.utils.internet. Rewired the intra-module call in open_file to use a module-level from pipenv.utils.internet import is_valid_url as _is_valid_url alias so the shim isn't tripped by pipenv itself. Added news fragment news/+initiative-a-is-valid-url.removal.rst. TDD: added test_is_valid_url_fileutils_shim_emits_deprecation in tests/unit/test_utils.py (RED → GREEN). T_A.3 was folded into this commit because the strict-mode validation revealed pipenv/utils/requirementslib.py:23 would trip the shim — the one-line caller migration was needed to keep the validation passing. The sweep commit 3a81cce1 also kept the from pipenv.utils.shell import normalize_drive re-export (with # noqa: F401) because tests/integration/test_cli.py:10 binds to pipenv.utils.shell.normalize_drive externally.
  • files edited/created:
    • pipenv/utils/fileutils.py (shim, intra-module rewire)
    • pipenv/utils/requirementslib.py (import retarget — folded T_A.3)
    • pipenv/utils/shell.py (sweep: dead duplicate path_to_url removed)
    • tests/unit/test_utils.py (new deprecation-warning test)
    • news/+initiative-a-is-valid-url.removal.rst (new Towncrier fragment)

T_A.3: Update internal callers to canonical import

  • depends_on: [T_A.2]
  • location: every file in pipenv/ (excl. patched/, vendor/) that currently does from pipenv.utils.fileutils import ..., is_valid_url, .... At plan-write time the only such site is pipenv/utils/requirementslib.py:23, where the import line is from pipenv.utils.fileutils import is_valid_url, normalize_path, url_to_path — a multi-symbol import.
  • description: At each site, surgically remove only the is_valid_url name from the fileutils import and add an is_valid_url import from pipenv.utils.internet. Other names co-imported from fileutils (e.g. normalize_path, url_to_path in requirementslib.py) must be preserved on their original import line — this is not a wholesale line replacement.
  • validation:
    • grep -rn "from pipenv.utils.fileutils import" pipenv/ | grep is_valid_url returns nothing under non-patched, non-vendor paths.
    • python -c "from pipenv.utils.requirementslib import is_valid_url, normalize_path, url_to_path" or equivalent caller-specific assertion confirms the other names still resolve. (Adapt this check to whichever callers the plan-execution agent found.)
    • Running the unit suite produces no DeprecationWarning from the T_A.2 shim being tripped by an internal caller (use the pytest-scoped -W form noted in T_A.2's validation block).
    • Unit suite green.
  • status: Completed (folded into T_A.2 — commit b144f64c)
  • log: Folded into T_A.2 by necessity: the only internal site importing is_valid_url from pipenv.utils.fileutils was pipenv/utils/requirementslib.py:23, a multi-symbol import line. Retargeting that single name (while preserving the co-imported normalize_path and url_to_path on the same line) was required for T_A.2's strict-mode -W error::DeprecationWarning validation to pass. The task brief explicitly authorised this fold-in.
  • files edited/created:
    • pipenv/utils/requirementslib.py (one-line import retarget)

T_A.4: Remove is_valid_url re-export shim

  • depends_on: [T_A.3]
  • location: pipenv/utils/fileutils.py.
  • description: Shim deleted along with the parallel pipenv.project.SourceNotFound re-export (T_D.2). Maintainer call: pipenv's stable API is the CLI, so a deprecation window protects nothing — internal-only re-exports with DeprecationWarning shims add noise without value. Removed outright; in-module call sites already used the canonical pipenv.utils.internet / pipenv.utils.sources imports.
  • status: Completed
  • log: Removed is_valid_url shim + its unit test; removed SourceNotFound re-export from pipenv.project and updated the two real importers (pipenv/utils/indexes.py, tests/unit/test_sources.py) to pull from pipenv.utils.sources.
  • files edited/created:
    • pipenv/utils/fileutils.py (shim + alias import deleted)
    • pipenv/project.py (SourceNotFound dropped from sources import)
    • pipenv/utils/indexes.py (import path updated)
    • pipenv/utils/sources.py (docstring cleaned up)
    • tests/unit/test_utils.py (shim-only test deleted)
    • tests/unit/test_sources.py (import path updated)

Wave 1 — Initiative B: Inlined-vendor triage

These four triage tasks (T_B.1–T_B.4) are parallel-safe because each writes to its own sub-file, not to a single shared document. T_B.5 concatenates them into the consolidated triage doc. Do not share an output file between parallel agents.

T_B.1: Triage pipenv/utils/requirementslib.py

  • depends_on: [T0.1, T0.2]
  • location: pipenv/utils/requirementslib.py (read-only); decision recorded in docs/dev/initiative-b-triage-requirementslib.md (new, temporary, this task's own output file).
  • description: Audit the 740-line module. For each public symbol (strip_ssh_from_git_uri, add_ssh_scheme_to_git_uri, is_vcs, is_editable, is_star, convert_entry_to_path, is_installable_file, get_setup_paths, prepare_pip_source_args, get_package_finder, PathAccessError, get_path, default_visit, dict_path_enter, dict_path_exit, remap, merge_items, get_pip_command, unpack_url, get_http_url):
    • Identify whether the implementation has a live upstream (the original requirementslib package on PyPI is archived; the dict-tree walkers get_path / remap / dict_path_enter / dict_path_exit / default_visit come from boltons.iterutils — that one is live).
    • List every internal caller (grep).
    • Recommend one of adopt, vendor, or delete for each symbol or coherent group of symbols. The boltons.iterutils dict-walker group is a natural "vendor" group (move under pipenv/vendor/boltons/ or replace with a small purpose-specific helper). VCS-URI helpers and is_* predicates are natural "adopt" candidates.
    • Note any symbol with no internal callers as a delete candidate.
  • validation: Triage doc has an entry per symbol with a recommendation and a caller list.
  • status: Completed (commit 7b92b6aa; re-committed after filesystem freeze corrupted the original commit's object writes — content was intact in the working tree)
  • log: 20 public symbols audited. Recommendations roll up to: 7 boltons.iterutils dict-walkers form a coherent vendor group (move under pipenv/vendor/boltons/ or replace with a purpose-specific helper); VCS-URI helpers and is_* predicates are adopt; symbols with zero callers flagged as delete candidates for T_B.7.
  • files edited/created: docs/dev/initiative-b-triage-requirementslib.md (new)

T_B.2: Triage pipenv/utils/requirements.py

  • depends_on: [T0.1, T0.2]
  • location: pipenv/utils/requirements.py (read-only); decision recorded in docs/dev/initiative-b-triage-requirements.md (new, temporary, this task's own output file).
  • description: Audit the 395-line module's nine public functions (redact_netloc, redact_auth_from_url, normalize_name, import_requirements, add_index_to_pipfile, requirement_from_lockfile, requirements_from_lockfile, requirement_from_pipfile, requirements_from_pipfile). Same shape as T_B.1: caller list, upstream provenance (the redact_* pair are pip-internal copies; normalize_name overlaps dependencies.pep423_name — flag this duplication), recommendation. This module is likely mostly adopt (it's the lockfile <-> Pipfile bridge), but several functions belong conceptually in dependencies.py and Initiative E will move them.
  • validation: Triage doc entries complete; overlaps with dependencies.py (especially normalize_name / pep423_name) are explicitly flagged.
  • status: Completed (commit cf7695f4)
  • log: 9 public functions audited. Recommendations: 6 adopt (project-owned Pipfile/lockfile bridge), 2 vendor (redact_netloc / redact_auth_from_url — deliberate divergent forks of pip-internal helpers preserving env-var auth placeholders and the git SSH user; cannot be replaced by pip imports without a behaviour change), 1 delete (normalize_name — overlaps with dependencies.pep423_name; flagged for Initiative E merge). Also surfaced: pep423_name's guard predicate is almost certainly buggy (any(... not in name ...) makes the else branch dead — flag for Initiative E); BAD_PACKAGES constant used in 3 routines (Initiative E co-location); module-level add_index_to_pipfile name-collides with Project.add_index_to_pipfile (Initiative E rename candidate).
  • files edited/created: docs/dev/initiative-b-triage-requirements.md (new)

T_B.3: Triage URL/path overlap in pipenv/utils/fileutils.py

  • depends_on: [T0.1, T0.2]
  • location: pipenv/utils/fileutils.py (read-only); decision recorded in docs/dev/initiative-b-triage-fileutils.md (new, temporary, this task's own output file).
  • description: Narrow scope: review the symbols flagged in T_A.1 as "filesystem- flavored URL handling" (is_file_url, url_to_path, path_to_url). Confirm they belong in fileutils.py (not internet.py) per the domain-boundary argument, or recommend a move with caller-update plan. This is the smallest of the triages and may be a 5-line entry in the doc.
  • validation: Triage doc entry exists and is internally consistent with T_A.1.
  • status: Completed (commit 7606f7a6)
  • log: All three target symbols (is_file_url, url_to_path, path_to_url) stay in fileutils.py per the domain-boundary rule (each one's heavy concept is the Path side; a URL utility knowing nothing about local paths could not implement them). Caller counts: is_file_url 0 external (only used inside fileutils.open_file); url_to_path 2 (both in requirementslib.py); path_to_url 0 external. Adjacent finding (relayed to T_A.1): pipenv/utils/shell.py:104 defines a second path_to_url with a different implementation and zero callers.
  • files edited/created: docs/dev/initiative-b-triage-fileutils.md (new)

T_B.4: Confirm markers.py is owned, not vendored

  • depends_on: [T0.1]
  • location: pipenv/utils/markers.py (read-only); recorded in docs/dev/initiative-b-triage-markers.md (new, temporary, this task's own output file — single paragraph is fine).
  • description: Per T0.1, this module is not actually inlined-former-vendor. Record the disposition explicitly so future readers don't re-open this question: "Owned project glue over pipenv.patched.pip._vendor.distlib and pipenv.patched.pip._vendor.packaging. Refactor freely under Initiative E if useful." No further triage required.
  • validation: Closeout paragraph present in triage doc.
  • status: Completed (commit d98b18e3)
  • log: Verified markers.py:9-10 imports parse_marker from pipenv.patched.pip._vendor.distlib.util and Marker from pipenv.patched.pip._vendor.packaging.markers — confirms project-owned glue, no separate vendor lineage. Disposition recorded so the question stays closed; refactor freely under Initiative E if useful.
  • files edited/created: docs/dev/initiative-b-triage-markers.md (new)

T_B.5: Synthesize triage doc + open Initiative B execution issues

  • depends_on: [T_B.1, T_B.2, T_B.3, T_B.4]
  • location: docs/dev/initiative-b-triage.md (new, consolidates the four per-module sub-files into a single readable document); GitHub Issues (one per adopt/vendor/delete group with multiple items). The four sub-files are deleted from the working tree as part of this task once their content is incorporated.
  • description: Assemble the four sub-audits into a single readable triage document with a summary table at the top: symbol or group → decision → execution owner → linked issue/PR. Open GitHub issues for each multi-symbol execution group so the work is trackable outside this plan.
  • validation: Triage doc has a clean summary table; every "adopt" and "vendor" decision has a linked issue; the four per-module sub-files no longer exist in the working tree. Note: per orchestrator amendment, gh issue create is NOT invoked by the agent — issue text is drafted in code-fenced blocks inside the consolidated doc for the maintainer to copy-paste after review.
  • status: Completed (commit d3320613)
  • log: Consolidated 4 sub-files into docs/dev/initiative-b-triage.md (713 lines). Summary table at top: 32 symbol/group rows sorted by decision (8 delete, 6 vendor, 18 adopt). Per-module sections preserve each sub-file's substantive findings. Seven code-fenced gh issue create blocks drafted at the bottom for the maintainer: (1) delete dead code in requirementslib, (2) vendor boltons.iterutils subset, (3) document redact_* fork provenance, (4) adopt Pipfile/lockfile bridge in requirements.py, (5) adopt schema predicates in requirementslib, (6) adopt-or-replace decision for unpack_url/get_http_url, (7) bundled single-item cleanups + Initiative E hand-offs. Cross-cutting flags (normalize_name/pep423_name overlap; suspected pep423_name bug; add_index_to_pipfile name collision; BAD_PACKAGES co-location) routed to Initiative E. NO issues opened.
  • files edited/created:
    • docs/dev/initiative-b-triage.md (new, consolidated)
    • docs/dev/initiative-b-triage-requirementslib.md (deleted)
    • docs/dev/initiative-b-triage-requirements.md (deleted)
    • docs/dev/initiative-b-triage-fileutils.md (deleted)
    • docs/dev/initiative-b-triage-markers.md (deleted)

T_B.7: Execute Initiative B decisions (Wave 1c)

  • depends_on: [T_B.5]
  • location: pipenv/utils/requirementslib.py, pipenv/utils/requirements.py, pipenv/utils/dependencies.py, pipenv/utils/locking.py, pipenv/project.py, pipenv/utils/resolver.py, tests/unit/test_requirementslib.py (new), tests/unit/test_dependencies.py, tests/unit/test_core.py, news/.
  • description: Originally scoped to "delete decisions only" — expanded under maintainer direction to execute the full set of triage decisions inline (no GitHub issues opened; the triage doc is the working record). Five work units launched as Wave 1c:
    • W1 — delete dead symbols in requirementslib.py. Verification surfaced that the triage's claim of "0 callers" for prepare_pip_source_args was stale (real caller in dependencies.py); scoped down to 6 symbols; the seventh routed to W3 for divergence investigation.
    • W2 — replace 6 inlined boltons.iterutils primitives with a purpose-built ~30-line recursive dict-merge helper. merge_items's public signature preserved.
    • W3a — provenance docstrings on redact_netloc and redact_auth_from_url.
    • W3bunpack_url / get_http_url: ADOPT path. Side-by-side found two load-bearing divergences from patched-pip (unpack_url returns File(...) for VCS links where pip returns None; get_http_url uses globally_managed=False where pip uses True). Kept the requirementslib copies with provenance docstrings.
    • W3cprepare_pip_source_args: REPLACE path. Migrated dependencies.py to import from the canonical indexes.py copy; deleted the requirementslib copy. The indexes.py version is strictly better (preserves port in trusted-host args; raises on missing URL; the Pipfile schema guarantees URL presence at the sole call site, so divergence #2 is unreachable).
    • W4 — fix latent bug in pep423_name (dead-else branch); consolidate normalize_name into pep423_name (4 callers migrated; all pass plain names so the migration is observably equivalent for them).
    • W5 — collapse is_editable duplicate. The dependencies.py:1503 copy turned out to be dead code; every active caller used requirementslib.is_editable. Made dependencies.py canonical (per long-term-home decision); migrated imports.
  • validation:
    • All five work units committed: 5a84f5ac (W1), de6628b8 (W2), e874e9d0 (W4), 842939a4 (W5), dc58f7a6 (W3a), 2d897a0c (W3b), 8a7d0a79 (W3c).
    • python -m pytest tests/unit -x: 516 passed, 9 skipped (baseline was 484; +32 new pinning / TDD tests across W2, W4, W5).
    • ruff check pipenv/: clean.
    • pipenv/utils/requirementslib.py: 740 lines → 274 lines (63% reduction).
    • News fragments added for the two behaviour-affecting changes (pep423_name bug fix; prepare_pip_source_args migration).
    • One latent bug found and left unfixed (with a TODO(swarm)-style flag in W2's reporting): the old boltons-based dict_path_exit silently produced an empty tomlkit.items.Array when reassembling nested containers. The new helper preserves array contents. This is technically an observable behaviour fix triggered only by an unusual Pipfile shape (same extras = ["..."] in two merged category dicts).
  • status: Completed
  • log: Wave 1c executed inline per maintainer's "execute, don't track" directive: no GitHub issues opened; the triage doc serves as the working record. The seven Wave-1c commits net out at roughly -470 lines in requirementslib.py, +30 lines of purpose-built helper, +30 lines of new tests, two correctness fixes, zero new vendored surface, zero new external dependencies. Per the parallel-agent operating discipline established in T0.2, agents used explicit git commit -- <files> to avoid sweep-up across the wave.
  • files edited/created:
    • pipenv/utils/requirementslib.py (heavy: -466 lines net across W1 + W2 + W3b docstrings + W3c + W5)
    • pipenv/utils/dependencies.py (W3c imports, W4 pep423_name fix, W4 caller migrations, W5 canonical is_editable)
    • pipenv/utils/requirements.py (W3a docstrings, W4 normalize_name removal)
    • pipenv/utils/locking.py (W4 caller migration, W5 import retarget)
    • pipenv/project.py (W4 caller migrations)
    • pipenv/utils/resolver.py (W4 caller migration, import update)
    • tests/unit/test_requirementslib.py (new — W2; 8 tests)
    • tests/unit/test_dependencies.py (W4 +12 tests, W5 +12 tests)
    • tests/unit/test_core.py (W4 docstring update)
    • news/+pep423-name-scheme-guard.bugfix.rst (new — W4)
    • news/+initiative-b-prepare-pip-source-args.trivial.rst (new — W3c)

T_B.6: Document owned-vs-vendored policy

  • depends_on: [T_B.5]
  • location: docs/dev/contributing.md (existing); cross-linked from CONTRIBUTING.md.
  • description: Add a short section ("Owned code vs. vendored code") stating the policy: code lives either under pipenv/utils/ and elsewhere in pipenv/ (owned; refactor freely; project conventions and test coverage apply) or under pipenv/vendor/ (do not edit; resync from upstream per the vendoring tooling). There is no third state. Inlining upstream code into pipenv/utils/ without owning it is explicitly out of policy going forward.
  • validation: Section exists, links to docs/dev/initiative-b-triage.md, and is reachable from CONTRIBUTING.md.
  • status: Completed (commit aa459f65)
  • log: Added 58-line "Owned code vs. vendored code" section to docs/dev/contributing.md. Covers the two states, the no-third-state rule, and the deliberate-forks exception with the Wave 1c examples (redact_* in requirements.py; unpack_url/get_http_url in requirementslib.py). Top-level CONTRIBUTING.md got a pointer paragraph rather than a duplicate copy.
  • files edited/created:
    • docs/dev/contributing.md (+58 lines)
    • CONTRIBUTING.md (+6 lines pointer)
  • files edited/created:

Wave 2 — Initiative C: Typed routine context

Note: The "PRD reframe applied" marker is folded into T0.1 per review feedback (it was a no-op). What was previously T_C.1 is removed; Wave 2 begins at T_C.2.

T_C.2: Inventory routine parameters

  • depends_on: [T0.1, T0.2]

  • location: docs/dev/initiative-c-params.md (new, temporary); reads pipenv/routines/*.py and pipenv/core.py.

  • description: Produce a parameter inventory across the routine entry points. Inclusion threshold: any function taking more than 3 parameters besides project. Functions known to qualify (verified against current code): do_install (17), handle_new_packages (11), handle_lockfile (10), do_init (10), do_install_validations, install_build_system_packages, do_install_dependencies, batch_install_iteration, batch_install, plus analogues in update.py, uninstall.py, lock.py, sync.py. Trivial-arity helpers (e.g. _target_marker_environment with 2 params, handle_missing_lockfile with 5) are listed in an appendix but do not drive context-shape decisions. For each in-scope parameter record: name, type (best-effort if un-annotated), default, semantic group (e.g. "install policy" for pre/deploy/skip_lock; "target environment" for system/ allow_global/python/pypi_mirror; "package selection" for packages/editable_packages/pipfile_categories/dev; "execution control" for extra_pip_args/site_packages/ requirementstxt/index/ignore_pipfile). The semantic groups become the candidate shape for the dataclass(es).

  • validation: Doc table covers every named parameter on every in-scope routine; semantic-group assignment is internally consistent; trivial-arity helpers are in the appendix, not the main table.

  • status: Completed (commit 0c60c580)

  • log: 240 parameter rows across 23 in-scope functions in pipenv/routines/. Scope finding: pipenv/core.py no longer exists (earlier modernization decomposed it into routines/ + utils/); inventory sourced exclusively from pipenv/routines/.

    Semantic-group distribution: package_selection 54, execution_options 57, target_env 50, install_policy 36, other 34, state_flags 9.

    Cross-cutting findings for T_C.3:

    • pypi_mirror + system/allow_global universal (~36 rows) — strongest signal for a shared target_env group.
    • (packages, editable_packages, categories) shape duplicated in 5+ routines, with a parallel (package_args, pipfile_category, category) rename inside update.py's helpers (worth normalizing in RoutineContext).
    • install_policy flags (pre, deploy, skip_lock, ignore_pipfile, clear, lock_only) consistently travel as a packet — best candidate for a nested dataclass.
    • state_flags (9 rows) is small and incoherent — recommend leaving as call-site args rather than folding into RoutineContext.
    • other group is dominated by per-call workflow plumbing (lockfile, procs, reverse_deps, requested_packages) — NOT RoutineContext material; belongs in per-routine operation objects.
  • files edited/created:

    • docs/dev/initiative-c-params.md (new, 240-row table + analysis)

T_C.3: Design RoutineContext proposal

  • depends_on: [T_C.2]

  • location: docs/dev/initiative-c-design.md (new, temporary).

  • description: Propose the dataclass shape. Likely structure: one top-level RoutineContext containing nested frozen dataclasses InstallPolicy, TargetEnv, PackageSelection, ExecutionOptions, matching the semantic groups from T_C.2. Each nested dataclass is frozen=True. Mutation happens via dataclasses.replace. Construction helper: a from_cli(...) classmethod that wraps the current per- command parameter list and is the only place CLI defaults are materialized. Include a concrete example of do_install rewritten against the proposed type. Get sign-off from Matt before T_C.4 lands.

  • validation: Design doc exists, includes type signatures and one rewritten call site for illustration, has a "decisions needed" section for any open choices.

  • status: Completed (commit 49f6892f) — awaits maintainer sign-off before T_C.4 begins

  • log: 782-line design proposal. 32 fields across four nested frozen dataclasses (TargetEnv: 5, InstallPolicy: 8, PackageSelection: 10, ExecutionOptions: 9). Includes the concrete do_install migration example (17 params → (project, ctx)). The agent executed the proposed dataclasses at runtime as a sanity check — RoutineContext.from_cli(...) constructs cleanly, dataclasses.replace produces a new context without mutating the original, derived properties (allow_global derives from system) work.

    Seven decision questions for maintainer sign-off: field grouping correctness, Project placement (separate first arg vs absorbed into ctx), naming, module location (pipenv/routines/context.py vs alternatives), from_cli vs __init__, uninstall-flag placement, helper-method bundling.

    Seven open implementation questions for follow-up: categories defaults, BAD_PACKAGES co-location, requirements_directory layering, Pipfile-vs-lockfile category aliases, audit/scan scope (deferred to a sibling AuditOptions per the design), None vs () for extra_pip_args, backwards-compat shim for external API consumers.

  • files edited/created:

    • docs/dev/initiative-c-design.md (new, 782 lines)

T_C.4: Introduce RoutineContext alongside existing params

  • depends_on: [T_C.3]
  • location: pipenv/routines/context.py (new); does not modify existing routine signatures yet.
  • description: Add the dataclass module. Add tests covering construction, replace-based mutation, and from_cli defaults. No existing call site is modified in this task — this is pure additive scaffolding.
  • validation: New module + tests in tests/unit/test_routine_context.py; unit suite green; no other production file changed.
  • status: Not Completed
  • log:
  • files edited/created:

T_C.5: Migrate do_install to consume RoutineContext

  • depends_on: [T_C.4]
  • location: pipenv/routines/install.py, pipenv/cli/command.py (the CLI entry point that calls do_install).
  • description: Change do_install to accept a single RoutineContext parameter (plus project, which stays as the first positional until Initiative D reshapes it). Have the CLI entrypoint construct the context via RoutineContext.from_cli(...). Internal helpers (handle_new_packages, handle_lockfile, do_init) keep their current signatures in this PR — they're migrated in subsequent tasks.
  • validation:
    • do_install signature is (project, ctx: RoutineContext).
    • Unit suite green.
    • pipenv install --help exits 0 (verifies the CLI wiring imports cleanly).
    • Smoke install runs cleanly: from a temporary directory containing only the fixture at tests/pypi/-equivalent style, run pipenv install --skip-lock requests==2.28.0 (or whichever fixture package is present in tests/test_artifacts/ and known to install without network). Document the chosen fixture in the PR description.
    • If no local-only fixture is available, drop the smoke install requirement and rely on CI integration tests to catch regressions post-push; record this fallback in the PR description.
  • status: Completed (commit b5419c1c)
  • log: do_install signature reduced from 17 params to (project, ctx). CLI entry point cmd_install in pipenv/cli/command.py constructs the context via RoutineContext.from_cli(...) and passes do_install(state.project, ctx). Body of do_install accesses ctx.target_env.*, ctx.install_policy.*, ctx.package_selection.*, ctx.execution_options.* and continues to pass individual args to unchanged child helpers (T_C.6 migrates them). Editable-path normalization preserved by folding back into the context via dataclasses.replace. 11 new pinning tests in tests/unit/test_do_install_context_routing.py. No backwards-compat shim — old signature deleted wholesale per T_C.3 §9 sign-off.
  • files edited/created:
    • pipenv/routines/install.py
    • pipenv/cli/command.py
    • tests/unit/test_do_install_context_routing.py (new, 11 tests)

T_C.6: Migrate handle_new_packages and handle_lockfile

  • depends_on: [T_C.5]

  • location: pipenv/routines/install.py.

  • description: Same migration pattern for the two largest internal helpers in install.py. They consume ctx rather than threading individual parameters.

  • validation: Both helpers take (project, ctx); unit suite green.

  • status: Completed (commit 776bab9d)

  • log: handle_new_packages reduced from 11 params to (project, ctx, *, perform_upgrades=True) (3 params; perform_upgrades stays as a call-site intent kwarg). handle_lockfile reduced from 10 params to (project, ctx). do_install updated to pass ctx directly. do_init (T_C.7 territory) call site bridged via inline RoutineContext.from_cli(...) — bridge will collapse naturally when T_C.7 migrates do_init end-to-end.

    Test updates: tests/unit/test_do_install_context_routing.py had 4 tests refactored to assert against ctx fields plus 2 new signature-pin tests added (TestHandleNewPackagesSignature, TestHandleLockfileSignature). 575 tests pass.

  • files edited/created:

    • pipenv/routines/install.py
    • tests/unit/test_do_install_context_routing.py

T_C.7: Migrate do_init, do_install_validations, do_install_dependencies

  • depends_on: [T_C.6]

  • location: pipenv/routines/install.py.

  • description: Same migration pattern for the remaining install.py entry points and their batch helpers. After this task, install.py is fully on RoutineContext.

  • validation: No function in install.py takes more than 3 positional / keyword arguments besides project and ctx; unit suite green.

  • status: Completed (commit 969fa575)

  • log: Seven helpers migrated: do_init (9 → 0 non-ctx params), do_install_validations (10 → 1 requirements_directory), do_install_dependencies (9 → 1 requirements_dir), handle_outdated_lockfile (9 → 2 old_hash, new_hash kw), handle_missing_lockfile (4 → 0), batch_install (10 → 4 call-state args), batch_install_iteration (8 → 4 call-state args). install_build_system_packages left at 3 non-project params (at the threshold). T_C.6's bridge inside do_init collapsed. sync.py got a similar inline-from_cli bridge added (T_C.9 collapses it).

    Two pre-existing issues fixed as side-effects:

    • Dead local in do_install_validations: pre = project.settings.get("allow_prereleases") was assigned and never read.
    • Subtle bug in do_install_dependencies: was mutating its own ignore_hashes parameter mid-loop; refactor computes effective_ignore_hashes locally so the frozen ctx is never mutated.

    batch_install/batch_install_iteration retain wide arity for deps_list/procs/sources/etc. — these are the "other" group per the design that belongs to a future BatchInstall object (deferred per T_C.4 §3 / T_C.3 sign-off). Flagged with TODO(swarm) at pipenv/routines/install.py:763.

    Test updates: 6 new signature-pin tests + 5 existing tests adjusted. 584 tests pass.

  • files edited/created:

    • pipenv/routines/install.py
    • pipenv/routines/sync.py (inline bridge for T_C.9)
    • tests/unit/test_do_install_context_routing.py

T_C.8: Migrate pipenv/routines/update.py

  • depends_on: [T_C.4] (parallel-safe with T_C.5–T_C.7 once T_C.4 has landed; reviewers should sequence behind install.py to keep diff review bandwidth manageable)

  • location: pipenv/routines/update.py.

  • description: Same migration pattern for do_update and its helpers.

  • validation: Unit suite green; same shape guarantee as T_C.7.

  • status: Completed (commit 0add7dfb)

  • log: do_update reduced from 17 params to (project, ctx). Two wide-arity helpers also migrated: _process_package_args and _resolve_and_update_lockfile (both now (project, ctx, *kw...) where the keyword-only args are upgrade-internal data flow, not user-facing inputs). cmd_update in pipenv/cli/command.py constructs the context via from_cli.

    --outdated and --dry-run collapse: RoutineContext carries no outdated field; cmd_update ORs both flags into install_policy.dry_run and do_update mirrors the historical outdated = outdated or bool(dry_run) derivation. Behaviour-preserving.

    Scope-creep (necessary): pipenv/routines/install.py:handle_new_packages calls do_update after adding new packages. Without a backwards-compat shim (per T_C.3 §9), the only options were (a) leave do_update's old signature (violates acceptance criteria) or (b) update the single call site in install.py. Took (b).

    Out of scope and deferred with TODO(swarm): the upgrade() function in update.py (10 params; called from cmd_upgrade which is itself T_C.8-out-of-scope). It now constructs an internal helper context to feed the migrated helpers but its own signature is unchanged.

    Test updates: 14 new tests in tests/unit/test_do_update_context_routing.py (signature pins + flag routing); existing tests/unit/test_update.py and tests/integration/test_update.py adjusted to the new shapes. 598 tests pass.

  • files edited/created:

    • pipenv/routines/update.py
    • pipenv/cli/command.py
    • pipenv/routines/install.py (single call-site swap)
    • tests/unit/test_update.py, tests/unit/test_do_update_context_routing.py (new), tests/integration/test_update.py

T_C.9: Migrate pipenv/routines/uninstall.py, lock.py, sync.py

  • depends_on: [T_C.4] (parallel-safe with T_C.5–T_C.8 in principle; smaller routines, may be a single PR rather than three)

  • location: pipenv/routines/uninstall.py, pipenv/routines/lock.py, pipenv/routines/sync.py.

  • description: Same migration pattern for the remaining routines that consume multi-parameter contexts. Routines that take only a project (e.g. graph, clean, clear, shell — verify) are out of scope.

  • validation: Unit suite green; all routines that thread > 3 params consume RoutineContext instead.

  • status: Completed (commit 58ca629b). Companion bug fix: commit 4f5fb81c.

  • log: Three routines migrated:

    • do_lock 9 params → (project, ctx).
    • do_sync 11 params → (project, ctx). T_C.7's inline RoutineContext.from_cli(...) bridge inside do_sync collapsed; T_C.8's bridge for the do_sync calls from update.py also collapsed.
    • do_uninstall 12 params (including a misnamed Click ctx=None passthrough) → (project, ctx). The Click-context passthrough was dropped per the design's "CLI usage-error rendering lives at the CLI layer" — the single use site PipenvUsageError("No package provided!", ctx=ctx) just omits the kwarg.
    • do_purge left unchanged (only 3 non-project params, below the threshold).

    Internal callers migrated in the same PR: install.py (handle_outdated_lockfile, handle_missing_lockfile), uninstall.py (post-uninstall lock branch), clean.py (ensure_lockfile), outdated.py (do_outdated), update.py (do_sync sites, collapsing T_C.8's bridge), cli/command.py (cmd_lock, cmd_sync, cmd_uninstall via RoutineContext.from_cli).

    Pre-existing bug surfaced during T_C.9's test fixturing: do_lock referenced old_lock_data unconditionally after a contextlib.suppress(KeyError) assignment, raising UnboundLocalError on first-lock / new-category paths. Fixed in follow-up commit 4f5fb81c (one-line initialization).

    Test updates: 22 new tests in tests/unit/test_lock_sync_uninstall_context_routing.py (signature pins + flag routing for all three migrated routines + do_purge shape); existing tests in test_locking_no_mutation.py and test_do_update_context_routing.py adjusted to the new shapes. 620 tests pass.

    Initiative C is structurally complete for the five pipenv/routines/*.py modules in T_C.2's inventory. Routine entry points that legitimately take only (project, ...few-args...) (e.g. graph, clean, clear, shell) remain on their old signatures per the threshold.

  • files edited/created:

    • pipenv/routines/{lock,sync,uninstall,install,clean,outdated,update}.py
    • pipenv/cli/command.py
    • tests/unit/test_lock_sync_uninstall_context_routing.py (new, 22 tests)
    • tests/unit/{test_locking_no_mutation,test_do_update_context_routing}.py
    • Companion bug fix: pipenv/routines/lock.py (commit 4f5fb81c)

Wave 3 — Seed tasks (full plan regenerated after Wave 1+2 lands)

T_D.1: Inventory Project responsibilities

  • depends_on: [T0.1, T0.2]

  • location: docs/dev/initiative-d-inventory.md (new, temporary).

  • description: Cluster the methods of pipenv/project.py (1850 lines) into the five proposed subsystems (Pipfile, Lockfile, Sources, VenvLocator, Settings) plus a residual "coordinator" group. Identify cross- subsystem references that would become tight coupling after extraction. Identify which subsystem should be extracted first (PRD suggests Sources for self-containedness; verify or revise).

  • validation: Inventory table exists; first extraction target identified with rationale.

  • status: Completed (commit 7297c8f3) — awaits maintainer sign-off before T_D.2 begins

  • log: 434-line inventory covering 93 public members of Project (41 @property, 1 @cached_property, 48 def, 2 @classmethod, 1 @staticmethod). Bucket distribution: Pipfile 38, Sources 16, VenvLocator 13, Lockfile 13, coordinator 7, helper 3, Settings 3. Sums to 93.

    First-extraction recommendation: Sources (confirms PRD's nomination). Smallest of the four "real" subsystems (Pipfile / Lockfile / Sources / VenvLocator), single outbound write (add_index_to_pipfile), no env-var coupling on its core methods.

    Six decision questions for maintainer sign-off: subsystem-split correctness, module location, first-target sign-off, deprecation-warning timing for the delegating-wrapper window, helper-bucket disposition, and resolution of the self.pipfile reference at project.py:586.

    Notable side-finds flagged for review:

    • Likely bug at pipenv/project.py:586: self.pipfile.get(...) references an attribute that doesn't exist on Project (likely meant parsed_pipfile or _pipfile). Worth investigating independent of the extraction.
    • Orphan-looking _read_pyproject at project.py:782: surfaced during the inventory walk; flagged for potential dead-code cleanup.
  • files edited/created:

    • docs/dev/initiative-d-inventory.md (new, 434 lines)

T_D.2: First extraction (Sources)

  • depends_on: [T_D.1]

  • location: pipenv/utils/sources.py (new) + pipenv/project.py

    • every internal caller of the migrating methods.
  • description: Extract Sources as a single new module under pipenv/utils/sources.py (matches the existing pipenv/utils/pipfile.py precedent per T_D.1 sign-off §8.2). Per the T_D.1 §8.4 sign-off, this task does NOT land delegating wrappers as a holding pattern. Extract Sources AND migrate every internal project.<sources-method> caller to project.sources.<method> (or whatever access shape we settle on) in the same PR. No DeprecationWarning, no shipped wrapper, no two-phase rollout — the maintainer's standing posture is that pipenv's only stable API is the CLI, so internal Python surface changes do not need a deprecation window.

    Helper-bucket methods (path_to, prepend_hash_types, get_file_hash) stay on Project for this task; revisit during per-subsystem extractions (T_D.1 §8.5).

  • validation: pipenv/project.py lost roughly the right number of lines (≥ 80 net reduction expected for Sources plus its callers' imports retargeting); new module has its own test file; every former project.<sources-method> caller now uses the new path; unit suite green.

  • status: Completed (commit 497d9716)

  • log: Sources extracted to pipenv/utils/sources.py (414 lines). pipenv/project.py shrunk from 1816 → 1531 lines (-285, far above the 80-line floor).

    Naming-collision resolution: the pre-existing project.sources property (returning a list of source dicts) was renamed to project.sources.all; project.sources itself now returns the Sources subsystem instance via @cached_property. Other renames follow the same dotted pattern: project.sources_defaultproject.sources.default, project.pipfile_sources()project.sources.pipfile_sources(). SourceNotFound was moved to pipenv/utils/sources.py and re-exported from pipenv/project.py to keep from pipenv.project import SourceNotFound working (one external caller in utils/indexes.py).

    16 new tests in tests/unit/test_sources.py. Caller migrations across pipenv/{environment,resolver}.py, pipenv/routines/install.py, pipenv/utils/{dependencies,indexes,requirements,resolver,virtualenv}.py, plus two test files updated to match. Pipenv install --help exits 0.

    Helper-bucket methods (path_to, prepend_hash_types, get_file_hash) stayed on Project per the T_D.1 sign-off §8.5.

  • files edited/created:

    • pipenv/utils/sources.py (new, 414 lines)
    • pipenv/project.py (heavy: -285 lines net)
    • pipenv/environment.py, pipenv/resolver.py, pipenv/routines/install.py, pipenv/utils/{dependencies,indexes,requirements,resolver,virtualenv}.py (caller migrations)
    • tests/unit/test_sources.py (new, 16 tests)
    • tests/unit/{test_resolver_regressions,test_utils}.py (test updates)

T_D.3 .. T_D.6: Remaining Project subsystem extractions (regenerated)

Regenerated 2026-05-12 after T_D.2 proved the extraction pattern. Per the T_D.1 §8 sign-off: no delegating wrappers, no DeprecationWarning, no two-phase rollout — extract AND migrate every internal caller in the same PR.

Parallelism reality: each of the four remaining subsystem extractions heavily modifies pipenv/project.py and migrates internal callers across many files. Two parallel extractions would race on project.py. The maintainer asked for "as parallel as possible"; in practice that collapses to strict serialization for the four tasks. Each is ~1 working session of agent time; total expected: 4 sequential tasks.

Order recommendation: smallest-and-simplest first to keep the pattern tight, then ramp up. The four tasks in proposed order:

  1. T_D.3 — Settings (smallest, lowest risk)
  2. T_D.4 — VenvLocator
  3. T_D.5 — Lockfile (deferred pylock-awareness per maintainer sign-off: extract Pipfile.lock-only behaviour; pylock support addressed in 2027 when the format flip happens)
  4. T_D.6 — Pipfile (largest, most coupled)

If a different order is preferred at execution time, it doesn't affect the design — the four are independent of each other once the project.sources precedent is in place.

T_D.3: Extract Settings subsystem

  • depends_on: [T_D.2]

  • location: pipenv/utils/settings.py (new) + pipenv/project.py

    • every internal caller of the migrating methods.
  • description: Extract the 3 Settings-classified methods from Project into a new Settings class under pipenv/utils/settings.py. Access via @cached_property on Project. T_D.1 §2 cluster identified Settings as a tight 3-method bucket: settings-related accessors that compose self.s.PIPENV_* env-var values with the [pipenv] section of the Pipfile.

  • validation: pipenv/project.py lost the 3 methods (and any associated state); new module has its own test file; every caller updated; unit suite green.

  • status: Completed (commit 0b8b5830)

  • log: Extracted Settings to pipenv/utils/settings.py (130 lines). pipenv/project.py:settings, Project.update_settings, and Project.use_pylock removed; Project.settings becomes a @cached_property returning a Settings(self) instance.

    Naming-collision resolution: Option A (Mapping-shaped subsystem). Settings subclasses collections.abc.MutableMapping, so every existing project.settings.get(key, default) / key in project.settings / project.settings[key] caller works unchanged — the subsystem instance has .get, __getitem__, __iter__, __len__, __contains__. Only the writer (update_settings) and the typed use_pylock accessor needed caller-site migration. This was a materially smaller migration than T_D.2's sources rename and produces a cleaner public surface.

    Migration sites: pipenv/routines/install.py (2 sites: update_settings(...)settings.update(...), project.use_pylockproject.settings.use_pylock), pipenv/project.py (2 internal references), and tests/integration/test_pylock.py (2 sites). Zero .get(...) callers needed migration.

    10 new tests in tests/unit/test_settings.py.

  • files edited/created:

    • pipenv/utils/settings.py (new, 130 lines)
    • pipenv/project.py (-5 lines net; 1526 → from 1531 after T_D.2)
    • pipenv/routines/install.py (2 migration sites)
    • tests/unit/test_settings.py (new, 10 tests)
    • tests/integration/test_pylock.py (2 migration sites)

    (T_D.3's commit accidentally swept in T_E.2's install.py import hunks due to the parallel-collision pattern — see the commit log. Net state is correct; both task acceptance criteria satisfied in the final tree.)

T_D.4: Extract VenvLocator subsystem

  • depends_on: [T_D.3]

  • location: pipenv/utils/venv_locator.py (new) + pipenv/project.py

    • every internal caller of the migrating methods.
  • description: Extract the 13 VenvLocator-classified methods from Project. Per T_D.1 §6.1: VenvLocator is currently read-only (no writers in this bucket; venv creation happens in routines/), so no split into Locator + Bootstrap is needed.

  • validation: same shape as T_D.3.

  • status: Completed (commit cb349450).

  • log: Third Initiative D extraction. The 13 VenvLocator-classified methods + 4 private helpers (_sanitize, _get_virtualenv_hash, _pipfile_venv_in_project, _which) moved into a new pipenv.utils.venv_locator.VenvLocator class accessed via the @cached_property Project.venv_locator.

    Naming-collision resolution: Option B (rename the API surface), matching the T_D.2 Sources pattern. The virtualenv_ prefix on the old Project surface drops because the subsystem itself is named venv_locator:

    • project.virtualenv_locationproject.venv_locator.location
    • project.virtualenv_existsproject.venv_locator.exists
    • project.virtualenv_nameproject.venv_locator.name
    • project.virtualenv_src_locationproject.venv_locator.src_location
    • project.virtualenv_scripts_locationproject.venv_locator.scripts_location
    • project.download_locationproject.venv_locator.download_location
    • project.proper_names_db_pathproject.venv_locator.proper_names_db_path
    • project.is_venv_in_project()project.venv_locator.is_venv_in_project()
    • project.get_location_for_virtualenv()project.venv_locator.get_location()
    • project.finders / .finderproject.venv_locator.finders / .finder
    • project.which(...) / .python(...)project.venv_locator.which(...) / .python(...)
    • project._which(...)project.venv_locator._which(...)

    The three __init__-set cache attributes (_virtualenv_location, _download_location, _proper_names_db_path) moved to VenvLocator instance state. The proper_names and register_proper_name methods stayed on Project (Pipfile-bucket per T_D.1 inventory) but now read the proper-names DB path through self.venv_locator.proper_names_db_path.

    Caller migration in the same PR per T_D.1 §8.4 sign-off — no holding-pattern wrappers. ~38 sites migrated across pipenv/cli/command.py, pipenv/routines/{graph,install,lock, shell,uninstall,update}.py, pipenv/utils/{pip,pipfile,project, resolver,shell,virtualenv}.py, tests/integration/test_project.py, and three tests/unit/ mock-side migrations.

    pipenv/project.py shrinks by 245 net lines (1526 → 1281). VenvLocator is 431 lines in pipenv/utils/venv_locator.py. Module-level imports purged from project.py: base64, fnmatch, operator, re, find_windows_executable, get_workon_home, is_virtual_environment, looks_like_dir, system_which, virtualenv_scripts_dir.

    Behaviour-preserving: every method's logic is a relocation. Same env-var precedence (VIRTUAL_ENV short-circuit, then PIPENV_VENV_IN_PROJECT, then Pipfile [pipenv] setting, then .venv autodetect). Same Pipfile-hash seed in _get_virtualenv_hash. Same mkdir-on-access for src_location / download_location / proper_names_db_path.

    17 new tests in tests/unit/test_venv_locator.py covering the constructor, the @cached_property accessor, env-var-vs-Pipfile precedence for is_venv_in_project, the VIRTUAL_ENV short-circuit, location caching, mkdir-on-access semantics, PIPENV_CUSTOM_VENV_NAME / PIPENV_PYTHON name hooks, and which / _which fallback paths. Full unit suite green (677 passed, 9 skipped).

  • files edited/created:

    • pipenv/utils/venv_locator.py (new, 431 lines)
    • pipenv/project.py (-245 lines net; 1526 → 1281)
    • pipenv/cli/command.py (6 migration sites)
    • pipenv/routines/{graph,install,lock,shell,uninstall,update}.py (10 migration sites)
    • pipenv/utils/{pip,pipfile,project,resolver,shell,virtualenv}.py (15 migration sites)
    • tests/integration/test_project.py (1 migration site)
    • tests/unit/test_credential_safety.py, tests/unit/test_install_error_context.py, tests/unit/test_utils.py (mock-side migrations)
    • tests/unit/test_venv_locator.py (new, 17 tests)

T_D.5: Extract Lockfile subsystem (Pipfile.lock-only)

  • depends_on: [T_D.4]
  • location: pipenv/utils/lockfile.py (new) + pipenv/project.py
    • every internal caller.
  • description: Extract the 13 Lockfile-classified methods from Project. Per T_D.1 §8.1 maintainer sign-off: pylock.toml support is NOT folded into this extraction. The new Lockfile subsystem handles only the legacy Pipfile.lock format. When pylock.toml is promoted to first-class (anticipated 2027), the boundary set up here will either grow a per-format dispatch layer or sprout a sibling PylockLock class — that's a future-T_D.5 decision, not this one's. Add TODO(pylock) tags at the format-detection seams so they're greppable in 2027.
  • validation: same shape as T_D.3; plus TODO(pylock) tags present at the future format-detection seams.
  • status: Completed (2026-05-12)
  • log:
    • 2026-05-12: Extracted Lockfile subsystem to pipenv/utils/lockfile.py (354 lines). 13 methods relocated: lockfile_location -> location, lockfile_exists -> exists, any_lockfile_exists -> any_exists, pylock_location / pylock_exists / pylock_output_path (unchanged names), lockfile_content -> content, lockfile_package_names -> package_names, lockfile(categories=...) -> as_dict(categories=...) (callable-method to subsystem-method rename to free up project.lockfile for the subsystem accessor), get_lockfile_meta -> meta, get_lockfile_hash -> hash, load_lockfile -> load, write_lockfile -> write. The orchestrating get_or_create_lockfile stays on Project (coordinator bucket per T_D.1 §2). pipenv/project.py shrinks by 173 net lines (1281 -> 1108). 10 distinct # TODO(pylock): annotations placed at the format-detection seams in the new module. 17 new unit tests in tests/unit/test_lockfile.py; full unit suite green (816 passed, 9 skipped). pipenv lock smoke test produces a valid Pipfile.lock.
  • files edited/created:
    • pipenv/utils/lockfile.py (new, 354 lines)
    • tests/unit/test_lockfile.py (new, 228 lines, 17 tests)
    • pipenv/project.py (-173 net lines)
    • pipenv/help.py, pipenv/utils/sources.py, pipenv/routines/{audit,check,clean,install,lock,requirements,scan,sync,uninstall,update}.py
    • tests/integration/{test_install_markers,test_lockfile,test_pylock}.py, tests/unit/{test_do_update_context_routing,test_lock_sync_uninstall_context_routing,test_pylock}.py

T_D.6: Extract Pipfile subsystem (largest, most coupled)

  • depends_on: [T_D.5]

  • location: pipenv/utils/pipfile.py (existing — already contains a partial Pipfile scaffolding) + pipenv/project.py + every internal caller.

  • description: Extract the 38 Pipfile-classified methods from Project. This is the biggest of the four extractions and the most coupled (cache invalidation via _parsed_pipfile_mtime_ns, writer methods consumed by Sources/Settings, hash computation consumed by Lockfile).

    Cross-subsystem references (per T_D.1 §3):

    • Pipfile.write_toml must invalidate Sources and Settings cached property handles on Project (since both Sources writers and Settings writers route through it).
    • Lockfile.get_lockfile_meta needs Pipfile.calculate_pipfile_hash. These are constructor-injected (or Sources(project=...) style) rather than via module-level imports.

    Helper-bucket methods (path_to, prepend_hash_types, get_file_hash) per T_D.1 §8.5 — orchestrator's standing recommendation: revisit AFTER T_D.6 lands, when Project is at its leanest. By that point most helpers will plausibly want to move to pipenv/utils/ as free functions; but it's a judgement call to make with the fullest picture.

  • validation: same shape as T_D.3; plus project.py is at its intended lean shape (probably ≤ 600 lines after this); plus a paragraph in the commit message recording the helper-bucket disposition (move out or keep) and rationale.

  • status: Completed (commit 3a160cb5) — Initiative D structurally done. pipenv/project.py now 491 lines (down from 1848 at the start of Initiative D, a 73% reduction). 38 Pipfile-bucket methods moved to pipenv/utils/pipfile.py:Pipfile (the new subsystem); the legacy plette-wrapper dataclass renamed to PlettePipfile to free the unqualified name. 42 new unit tests pin the extracted behaviour (cache invalidation, write→re-read, package add/remove, build-system parsing, hash computation, mtime-driven re-parse, etc.). 858 unit tests pass (up from 816 T_D.5 baseline). All project.X(...) callers migrated to project.pipfile.X(...) in the same PR per T_D.1 §8.4 (no wrappers). Commit message records helper-bucket disposition recommendations: leave path_to on Project; defer-move prepend_hash_types and get_file_hash to a small pipenv/utils/hashing.py follow-up; coordinators (create_pipfile, get_or_create_lockfile, get_environment, environment, installed_packages, installed_package_names) stay on Project.

  • log:

    • 2026-05-12 — Initiative D extraction sequence complete. The five subsystems (Sources, Settings, VenvLocator, Lockfile, Pipfile) are independent modules accessed via @cached_property on Project. Helper-bucket cleanup is a low-priority follow-up.
  • files edited/created:

    • pipenv/utils/pipfile.py (legacy PipfilePlettePipfile; new Pipfile subsystem class + extracted methods)
    • pipenv/project.py (492 lines; only coordinators + helpers remain)
    • pipenv/utils/locking.py (single-caller rename for the plette-wrapper)
    • 25 production caller files + 14 test caller files migrated
    • tests/unit/test_pipfile_subsystem.py (42 new tests)

T_E.1: Define canonical requirement-model API target

  • depends_on: [T_B.7]

  • location: docs/dev/initiative-e-design.md (new, temporary).

  • description: With the Initiative B triage decisions in hand, propose the canonical API on dependencies.py. Identify which symbols from requirementslib.py and requirements.py will move, which will be vendored, and which will be deleted. Sequence the moves. Full execution plan is regenerated after this lands.

  • validation: Design doc lists every in-scope symbol with a target location and sequencing notes.

  • status: Completed (commit 8ff77143) — awaits maintainer sign-off before T_E.2 begins

  • log: 692-line proposal. Post-Wave-1c symbol counts verified by grep: dependencies.py 54 public symbols (canonical home); requirements.py 8 funcs + BAD_PACKAGES (9 import statements across 8 files); requirementslib.py 7 public symbols (was 20 pre-Wave-1c, now down to 4 importing files); markers.py ~30 helpers + 2 classes (3 importing files).

    Sequenced into 6 follow-up tasks (T_E.2 .. T_E.7):

    • T_E.2: move 6 Pipfile/lockfile bridges + BAD_PACKAGES from requirements.pydependencies.py; renames add_index_to_pipfileadd_index_to_pipfile_with_trust_check.
    • T_E.3: move is_vcs, add_ssh_scheme_to_git_uri, merge_items, get_pip_command from requirementslib.pydependencies.py.
    • T_E.4: relocate unpack_url/get_http_url pip-internal fork pair (new pipenv/utils/unpack.py preferred); deletes empty requirementslib.py.
    • T_E.5: move BAD_PACKAGES co-location (folded into T_E.2).
    • T_E.6: add_index_to_pipfile rename (folded into T_E.2).
    • T_E.7: optional rename requirements.pyredact.py once it shrinks to two redact_* forks.

    Recommendations:

    • markers.py stays unchanged (owned glue, no upstream lineage, limited cross-module reuse — folding wouldn't reduce ambiguity).
    • redact_netloc/redact_auth_from_url stay in requirements.py (pip-internal forks with load-bearing behavioural divergence; no thematic fit with the requirement model).
    • normalize_name/pep423_name overlap already resolved under Wave 1c (commit e874e9d0); explicitly out of scope.

    8 decision questions for maintainer sign-off (canonical home shape, filename fates, rename target, unpack home, markers fold-in question, redact stay-or-move, test-pinning approach).

  • files edited/created:

    • docs/dev/initiative-e-design.md (new, 692 lines + 50-line sign-off addendum)

T_E.2: Move Pipfile/lockfile bridges to dependencies.py

  • depends_on: [T_E.1]

  • location: pipenv/utils/requirements.py (source), pipenv/utils/dependencies.py (destination), and caller files: pipenv/utils/{pipfile,locking}.py, pipenv/routines/{install,update,audit,requirements,clean,uninstall,graph}.py.

  • description: Per T_E.1 sign-off §3: move 6 Pipfile/lockfile bridge functions plus BAD_PACKAGES from requirements.py into dependencies.py (canonical home). Rename the module-level add_index_to_pipfile to add_index_to_pipfile_with_trust_check to disambiguate from Project.add_index_to_pipfile. Caller migration in the same PR per the no-shim posture.

  • validation: every moved symbol importable from pipenv.utils.dependencies; requirements.py reduced to only the redact_* fork pair; unit suite green.

  • status: Completed (commit caa72db6)

  • log: Seven symbols moved:

    • import_requirements (~60 lines)
    • add_index_to_pipfileadd_index_to_pipfile_with_trust_check (~17 lines; rename only, body identical)
    • requirement_from_lockfile (~88 lines; local imports of is_editable_path/is_star/normalize_vcs_url dropped since they're now intra-file)
    • requirements_from_lockfile (~13 lines)
    • requirement_from_pipfile (~109 lines; same local-import drop)
    • requirements_from_pipfile (~21 lines)
    • BAD_PACKAGES constant tuple (~7 lines)

    Caller migrations across 9 files: pipfile.py, locking.py, routines/{install,update,audit,requirements,graph,clean,uninstall}.py, plus 3 integration test files (test_import_requirements.py, test_requirements.py, test_lockfile.py).

    requirements.py is now 72 lines holding only the two redact_* fork functions plus their provenance docstrings (per T_E.1 §6 sign-off). T_E.7 will optionally rename this file to redact.py per T_E.1 §7 sign-off if it still makes sense.

    21 new tests in tests/unit/test_dependencies_bridges.py: 7 import-shape pins, 2 sanity checks that old paths are gone, 12 light behaviour pins covering string/dict/star/version paths and the trusted-host vs untrusted-host branches of add_index_to_pipfile_with_trust_check.

    Subtle pre-existing quirk surfaced (not fixed): an unconditional == prefix in requirement_from_lockfile's bare-version-string branch produces === when the input already starts with ==. Preserved as-is for behaviour-preservation; documented as a future cleanup candidate.

    (T_D.3 ran in parallel and committed first; T_D.3's commit accidentally swept in T_E.2's pipenv/routines/install.py caller-import hunks. T_E.2 detected the swept-up state and committed the remaining 14 files cleanly. Net final tree is correct.)

  • files edited/created:

    • pipenv/utils/dependencies.py (gains 7 moved symbols, +579 lines net for the move)
    • pipenv/utils/requirements.py (-361 lines; now 72 lines holding only the redact pair)
    • pipenv/utils/{pipfile,locking}.py (caller imports)
    • pipenv/routines/{install,update,audit,requirements,graph,clean,uninstall}.py (caller imports + 1 rename site each in install/update)
    • tests/unit/test_dependencies_bridges.py (new, 21 tests)
    • tests/integration/{test_import_requirements,test_requirements,test_lockfile}.py (test imports)

T_E.3: Move predicates and helpers out of requirementslib.py

  • depends_on: [T_E.2]

  • location: pipenv/utils/requirementslib.py (source), pipenv/utils/dependencies.py (destination), plus caller files pipenv/utils/{locking,pipfile}.py, pipenv/project.py, tests/unit/{test_requirementslib,test_utils,test_dependencies_bridges}.py.

  • description: Per T_E.1 sign-off §3: move four single-line/short helpers (is_vcs, add_ssh_scheme_to_git_uri, merge_items, get_pip_command) from requirementslib.py into dependencies.py (canonical home). merge_items brings its two private helpers (_merge_into, _new_container_like) with it. Caller migration in the same PR per the no-shim posture. After this commit requirementslib.py contains only the unpack_url/get_http_url pip-internal fork pair (plus the VCS_SCHEMES constant they need); T_E.4 will relocate that pair to a new pipenv/utils/unpack.py and delete the empty requirementslib.py shell.

  • validation: every moved symbol importable from pipenv.utils.dependencies; requirementslib.py reduced to the two pip-internal forks; unit suite green for the moved-symbol tests; the four removed symbols no longer exist on pipenv.utils.requirementslib.

  • status: Completed

  • log: Four symbols moved (six lines of code including the two private helpers that travel with merge_items):

    • is_vcs (~14 lines) — placed alongside extract_vcs_url since it shares VCS-URL semantics. Uses the existing typing.Mapping import in dependencies.py (works with isinstance in Py3.9+); is_valid_url newly imported from .internet.
    • add_ssh_scheme_to_git_uri (~14 lines) — placed with is_vcs (intra-module dependency; the existing cross-module import in dependencies.py becomes a local reference).
    • merge_items (~33 lines) + _merge_into (~17 lines) + _new_container_like (~17 lines) — placed just before import_requirements (the next dict-merge-shaped routine).
    • get_pip_command (~9 lines) — placed near determine_package_name (its only in-tree caller, intra-module after the move); InstallCommand newly imported.

    Caller migrations across 5 files:

    • pipenv/utils/locking.py (folded is_vcs, merge_items into the existing from pipenv.utils.dependencies import (...) block)
    • pipenv/utils/pipfile.py (same — folded into existing dependencies import block)
    • pipenv/project.py (one-line late-import edit at _get_vcs_packages; narrow scoped to that import line only to avoid T_D.4 conflicts)
    • tests/unit/test_requirementslib.py (docstring + import re-target)
    • tests/unit/test_utils.py (the test_is_vcs late import)

    requirementslib.py shrank from 275 lines to 144 (-131 lines). After T_E.3 only one importer of requirementslib remains in the in-tree codebase (dependencies.py for unpack_url); T_E.4 will close that out.

    10 new tests in tests/unit/test_dependencies_bridges.py: 4 import-shape pins, 1 sanity check that the old paths are gone, and 5 light behavioural pins (is_vcs mapping + string-with-ssh branches, add_ssh_scheme_to_git_uri round-trip, merge_items recursive last-write-wins + empty-list contract, get_pip_command returns a usable InstallCommand).

  • files edited/created:

    • pipenv/utils/dependencies.py (gains 4 moved symbols + 2 private helpers; new InstallCommand + is_valid_url imports; drops the cross-module requirementslib import for the moved symbols)
    • pipenv/utils/requirementslib.py (-131 lines; now 144 lines holding only the unpack_url/get_http_url fork pair + the VCS_SCHEMES constant they need)
    • pipenv/utils/{locking,pipfile}.py (caller imports folded into the existing dependencies import block)
    • pipenv/project.py (one-line late-import edit at _get_vcs_packages)
    • tests/unit/test_dependencies_bridges.py (extended with 10 T_E.3 tests; module docstring updated)
    • tests/unit/test_requirementslib.py (docstring + import re-target to the new location)
    • tests/unit/test_utils.py (one-line late-import edit in test_is_vcs)

T_E.4: Relocate the pip-internal fork pair; delete requirementslib.py

  • depends_on: [T_E.3]

  • location: pipenv/utils/requirementslib.py (source, deleted), pipenv/utils/unpack.py (destination, new), plus caller file pipenv/utils/dependencies.py and test files tests/unit/{test_unpack,test_dependencies_bridges}.py.

  • description: Per T_E.1 sign-off §6 question 4 ("APPROVED as proposed: new pipenv/utils/unpack.py"), relocate the two pip-internal fork helpers (unpack_url, get_http_url) plus the local VCS_SCHEMES set out of requirementslib.py and into a new pipenv/utils/unpack.py. The new module's top-level docstring records the pip-internal-fork provenance and points at the design docs (initiative-b-triage, initiative-e-design §T_E.4). The load-bearing per-function provenance commentary (VCS-link divergence in unpack_url; globally_managed=False in get_http_url) is preserved verbatim from the source module.

    After the move, requirementslib.py has zero remaining symbols and is deleted in the same commit. The sole in-tree caller (pipenv/utils/dependencies.py:41) is migrated to import from the new location. This is the final structural move of Initiative E; the only remaining E task is the optional T_E.7 requirements.pyredact.py rename.

    VCS_SCHEMES placement: kept alongside unpack_url in unpack.py (not promoted to pipenv/utils/constants.py). Reason: zero cross-module callers (only unpack_url reads it). The constants.py VCS_SCHEMES is a distinct list of vcs+transport strings that does NOT include the bare git/hg/svn/bzr schemes the unpack set needs, so the two cannot be unified without changing the semantics of one of the consumers.

  • validation: pipenv/utils/requirementslib.py no longer exists; unpack_url and get_http_url importable from pipenv.utils.unpack; sole caller pipenv/utils/dependencies.py imports from the new location; unit suite green; zero in-tree importers of pipenv.utils.requirementslib remain (excluding negative-assertion test calls that explicitly check the module is gone).

  • status: Completed

  • log: Three symbols moved:

    • unpack_url (~63 lines incl. provenance docstring) — relocated verbatim; no behavioural change.
    • get_http_url (~38 lines incl. provenance docstring) — relocated verbatim; no behavioural change.
    • VCS_SCHEMES (25-element set used only by unpack_url) — relocated verbatim; intentionally kept distinct from the pipenv.utils.constants.VCS_SCHEMES list, which serves a different consumer set (dependencies.determine_vcs_specifier, is_vcs).

    Caller migrations (1 file):

    • pipenv/utils/dependencies.py (one-line import edit: from pipenv.utils.requirementslib import unpack_urlfrom pipenv.utils.unpack import unpack_url)

    pipenv/utils/requirementslib.py deleted (was 144 lines after T_E.3; now zero — the file does not exist).

    8 new tests in tests/unit/test_unpack.py:

    • 5 import-shape pins (unpack_url and get_http_url importable from the new home; VCS_SCHEMES is a set with the bare-scheme entries; legacy requirementslib module is gone; dependencies sources unpack_url from the new home).
    • 3 behavioural smoke tests (VCS-link File-not-None return; bare git scheme dispatches to the VCS branch via our local set; get_http_url constructs TempDirectory with globally_managed=False).

    Also updated 1 T_E.3-era test in tests/unit/test_dependencies_bridges.py: the test_old_requirementslib_module_no_longer_exports_moved_symbols test was rewritten to assert the module itself is gone (a strict superset of the prior symbol-by-symbol check, which would now crash on ModuleNotFoundError at import time).

    Initiative E structural work is complete after T_E.4. T_E.5 and T_E.6 were folded into T_E.2. T_E.7 (optional rename requirements.pyredact.py) is the only remaining task and has no dependents.

  • files edited/created:

    • pipenv/utils/unpack.py (new — 174 lines, all pip-internal-fork provenance preserved)
    • pipenv/utils/requirementslib.py (deleted)
    • pipenv/utils/dependencies.py (one-line import edit)
    • tests/unit/test_unpack.py (new — 8 tests)
    • tests/unit/test_dependencies_bridges.py (the T_E.3 "old module symbols are gone" test rewritten to "module itself is gone")

T_F.1: Document current subprocess resolver protocol

  • depends_on: [T_E.1] (gated on E's design so we know what data shape will cross the boundary)

  • location: docs/dev/initiative-f-protocol.md (new, temporary).

  • description: Write down what currently crosses the in-process / subprocess boundary in pipenv/resolver.pypipenv/utils/resolver.py: the JSON shape, the argv contract, the environment variables, the exit codes. This is a doc-only PR; no code change. Used as input for the typed-schema design in subsequent tasks.

  • validation: Doc describes every field crossing the boundary with type and provenance.

  • status: Completed (commit 2a7aa81b)

  • log: 588-line protocol reference covering all 9 required sections. Coverage counts: 10 argv elements (3 dead-surface flags called out); 11 explicit env vars + 4 implicit; 12 JSON top-level keys per entry; 10 divergent in-process-vs-subprocess helper pairs flagged for T_F.2+ fold-in; 11 decisions deferred to T_F.2.

    Substantive findings worth surfacing independently of T_F.2:

    • No subprocess timeout at pipenv/utils/resolver.py:1222 (c.wait()) — a hung mirror means pipenv hangs forever. This is a real user-facing reliability issue independent of the protocol-typing refactor.
    • No schema version, no envelope, no success/failure discriminator in the current protocol. Non-zero exit is the only failure signal; resolution-impossible vs subprocess-crash are indistinguishable to the parent.
    • Three dead argv flags in pipenv/resolver.py: --parse-only, --pipenv-site, and positional packages (always shadowed by --constraints-file in production). Cruft to remove independent of T_F.2.
    • The biggest fold-target between in-process and subprocess paths is two competing requirement-output formatters: Entry.get_cleaned_dict (subprocess side, pipenv/resolver.py:288-320) vs format_requirement_for_lockfile (parent side, pipenv/utils/locking.py:46-160). They overlap heavily.
    • The PIPENV_RESOLVER_PARENT_PYTHON "in-process" branch is a debug bypass, not architectural cleanliness — both paths still call resolver.resolve_packages and diverge only in serialization.
  • files edited/created:

    • docs/dev/initiative-f-protocol.md (new, 588 lines)

T_F.2: Typed resolver subprocess protocol — design (sign-off gate)

  • depends_on: [T_F.1]

  • location: docs/dev/initiative-f-typed-design.md (new, permanent until superseded by T_F.3 execution).

  • description: Design-only proposal for the typed ResolverRequest / ResolverResponse pair that T_F.3 will introduce. Resolves the 11 decisions deferred to T_F.2 by F.1 §9, picks the canonical replacement for the two competing requirement formatters (Entry.get_cleaned_dict and format_requirement_for_lockfile — a third unified LockedRequirement.from_install_requirement constructor that lives in the schema module), specifies the one-shot migration (no backwards-compat shim, per T_C.3 §9 / T_E.1 §6), and surfaces 10 numbered open questions for the maintainer.

  • validation: Doc-only PR; python -c "import ast; ast.parse(open('pipenv/resolver.py').read())" still works because no production code was touched. Doc is 400–800 lines and follows the T_C.3 / T_E.1 sign-off addendum shape.

  • status: Completed (commit 921212e5); awaits maintainer sign-off before T_F.3 execution begins.

  • log: 719-line design doc covering envelope, discriminator, canonical formatter, migration path, resolution of F.1's 11 deferred decisions, test plan, and 10 sign-off questions.

    Headline decisions proposed:

    • Stdlib @dataclass(frozen=True) only — no new vendored dependencies (no pydantic, no msgspec, no attrs).
    • schema_version: int = 1 as the first field on both envelopes; mismatch is a hard reject (structured InternalError response + exit non-zero).
    • Discriminated ResolverSuccess | ResolutionError | InternalError union written to --response-file on exit 0; non-zero exit reserved for genuine subprocess crashes.
    • Single --request-file <path> tempfile carries all input; --pre, --clear, --system, --verbose, --category, --constraints-file, --resolved-default-deps-file, --parse-only, --pipenv-site, positional packages, and the which() stub all deleted in T_F.3.
    • PIPENV_RESOLVER_PYTHON_VERSION, PIPENV_EXTRA_PIP_ARGS, PIPENV_SITE_DIR env-var hops fold into typed request fields.
    • Two output formatters collapse into one new LockedRequirement.from_install_requirement constructor; both Entry.get_cleaned_dict and format_requirement_for_lockfile are deleted.
    • In-process branch (PIPENV_RESOLVER_PARENT_PYTHON=1) preserved untouched in T_F.3; fold deferred to T_F.4 / T_F.5.
    • Wall-clock timeout reserved on schema (deadline_seconds) but not enforced in T_F.3.

    Open questions for sign-off (10): schema module home (resolver-as- package vs. utils file); schema-version-mismatch behaviour; to_lockfile_dict return type; news-fragment policy; no_binary field vs. recompute; whether T_F.3 also folds the in-process branch; constraints comma-escape regression test; schema-version bump policy on additive fields; Diagnostics.resolver_log vs. stderr; one or two tempfiles.

  • files edited/created:

    • docs/dev/initiative-f-typed-design.md (new, 719 lines)

T_F.3: Execute the typed resolver subprocess schema

  • depends_on: [T_F.2]

  • location: pipenv/resolver/ (new package — schema.py, main.py, __init__.py); pipenv/utils/resolver.py; pipenv/utils/locking.py; pyproject.toml (console-script entry); tests under tests/unit/test_resolver_schema.py, tests/unit/test_resolver_parent_dispatch.py, tests/unit/test_resolver_protocol_smoke.py, tests/integration/test_resolver_protocol.py; golden fixtures under tests/unit/fixtures/resolver_schema/ and tests/integration/fixtures/resolver_protocol/; news/T_F.3.behavior.rst.

  • description: Single-PR execution of the T_F.2 design. Introduces the typed ResolverRequest / ResolverResponse envelope + discriminated ResolverResult; folds Entry.get_cleaned_dict and format_requirement_for_lockfile into the new unified LockedRequirement.from_install_requirement constructor; drops the three env-var hops and the dead argv flags; rewrites both the subprocess entry and the parent dispatcher around the typed envelope; migrates the in-process branch to the new types (without folding it with the subprocess branch — that's T_F.4); pins the JSON wire shape with a golden-fixture integration test + a PIPENV_REGEN_PROTOCOL_FIXTURES regen mechanism. Execution structured as a 4-wave swarm (docs/dev/initiative-f-execution-plan.md).

  • validation:

    • 758 unit tests pass (was 677 baseline pre-T_F.3). +81 tests from the schema + parity + dispatch + comma-in-marker + protocol-smoke + parent-dispatch suites.
    • Integration test pins the JSON wire shape (3 consecutive runs ~1.1s each).
    • pipenv lock smoke produces byte-identical lockfiles via subprocess AND PIPENV_RESOLVER_PARENT_PYTHON=1 in-process paths.
    • Acceptance greps clean: zero Entry.get_cleaned_dict, zero format_requirement_for_lockfile, zero PIPENV_RESOLVER_PYTHON_VERSION/PIPENV_EXTRA_PIP_ARGS/PIPENV_SITE_DIR references, zero --parse-only/--pipenv-site/--constraints-file/--resolved-default-deps-file/--category argv references in pipenv/resolver/main.py.
  • status: Completed (commits listed below; branch maintenance/code-cleanup-phase3-resolver-typed-schema-2026-05).

  • log: Wave A (foundation): 85993ca4 schema module + canonical formatter + 27 golden snapshots (16 format_requirement_for_lockfile cases + 11 entry_get_cleaned_dict cases); A2 absorbed into A1 because Python's import system makes pipenv/resolver.py and pipenv/resolver/ mutually exclusive. Re-export shim at pipenv/resolver/__init__.py kept every legacy import path working.

    Wave B (parallel 3-agent): 5e6eca82 B3 (lockfile writer + 17 → 20 ported test cases); d1563a1e B1+B2 (subprocess entry + parent rewrite collapsed into one commit due to pre-commit stash race — functionally clean; both reviewer-friendly diffs reflected in commit message). bfbecba4 later fixed a wave-B bug where _main imported the schema before _ensure_modules() populated sys.path.

    Wave C (parallel 3-agent): 0c4a11a9 schema from_lockfile_dict touch-up; de3edea9 C1 + C3 (45 new schema tests including parity-against-golden + comma-in-marker per Q7); 706b7ab9 C2 integration test + golden request/response fixtures + PIPENV_REGEN_PROTOCOL_FIXTURES regen branch; e891c888 C4 news fragment.

    Wave D: this entry.

    Bugs surfaced + fixed along the way: bootstrap-order bug in _main (bfbecba4) that affected any subprocess invocation lacking pipenv on sys.path — wave-B unit smokes missed it because python -m pipenv.resolver.main invocation pre-populates the path; the integration test caught it.

    Out of scope (intentionally; deferred to follow-up tasks): T_F.4 (fold in-process and subprocess branches into one implementation per the PRD acceptance criterion), wall-clock-timeout enforcement (RequestMetadata.deadline_seconds reserved on schema but not enforced), populating Diagnostics.resolver_log (reserved-but-empty per Q9), pluggable alternative resolver backends (uv etc — design shape preserves the option per typed-design §6a but explicitly not in F).

  • files edited/created:

    • pipenv/resolver/__init__.py (new, re-export shim — trimmed after Wave B)
    • pipenv/resolver/main.py (moved from pipenv/resolver.py; rewritten to consume ResolverRequest and produce ResolverResponse)
    • pipenv/resolver/schema.py (new, 14 dataclasses + SCHEMA_VERSION + from_install_requirement + from_lockfile_dict / to_lockfile_dict)
    • pipenv/utils/resolver.py (parent-side rewrite — typed envelope build + dispatch + in-process-branch type migration)
    • pipenv/utils/locking.py (deleted format_requirement_for_lockfile; prepare_lockfile consumes Sequence[LockedRequirement])
    • pyproject.toml (scripts.pipenv-resolver = "pipenv.resolver.main:main")
    • tests/unit/test_resolver_schema.py (new, 62 tests covering invariants, envelope round-trip, golden-fixture parity, dispatch, schema-version-mismatch, VCS pins, comma-in-marker regression)
    • tests/unit/test_resolver_parent_dispatch.py (new, parent-side typed-dispatch tests)
    • tests/unit/test_resolver_protocol_smoke.py (new, subprocess-side typed-envelope smoke)
    • tests/unit/fixtures/resolver_schema/{format_requirement_for_lockfile,entry_get_cleaned_dict}/*.json (27 golden snapshots)
    • tests/integration/test_resolver_protocol.py (new, JSON wire-shape canary with regen branch)
    • tests/integration/fixtures/resolver_protocol/{request,response}.json (new, committed goldens)
    • news/T_F.3.behavior.rst (new, one-line behavior fragment)
    • tests/unit/test_dependencies.py, tests/unit/test_resolver_regressions.py, tests/unit/test_locking_no_mutation.py (legacy Entry / process_resolver_results test cases removed; mock updated for new return shape)
    • tests/unit/test_utils.py (17 format_requirement_for_lockfile cases ported to 20 typed cases)
    • tests/unit/test_core.py (docstring reference updated)
    • docs/dev/initiative-f-execution-plan.md (entry-by-entry log of each wave commit)

T_F.4: Fold in-process and subprocess resolver paths into one implementation

  • depends_on: [T_F.3]

  • location: pipenv/resolver/core.py (new); pipenv/resolver/main.py (subprocess adapter shrinks); pipenv/utils/resolver.py (in-process adapter); tests/unit/test_resolver_core.py (new, 6 tests).

  • description: Completes the PRD acceptance criterion "one resolver implementation, two thin adapters" for Initiative F. T_F.3 introduced the typed ResolverRequest / ResolverResponse schema but left two near-duplicate plumbing chains: the subprocess entry's _main (marker patch + resolve_packages call + ResolverResponse wrap + exit codes) and the in-process debug bypass at PIPENV_RESOLVER_PARENT_PYTHON=1 (its own marker patch + resolve_packages + exception propagation).

    This task extracts the shared logic into pipenv.resolver.core.resolve_for_pipenv(request) -> ResolverResponse — a single function that applies the python_marker_override via a context manager, dispatches to resolve_packages through sys.modules (so existing stub patterns keep working), and ALWAYS returns a typed response (never raises). Both adapters become thin wrappers:

    • Subprocess (pipenv/resolver/main.py:_main): read request → call resolve_for_pipenv → write response → exit code on result.kind. The duplicate _apply_python_marker_override helper is deleted.
    • In-process (pipenv/utils/resolver.py:_resolve_in_process): new ~28-line helper that dispatches on response.result.kind to return locked entries or raise ResolutionFailure / RuntimeError. The PIPENV_RESOLVER_PARENT_PYTHON arm of venv_resolve_deps is now one delegating line; the orphaned from pipenv import resolver import is removed.

    The unified path leaves explicit room for two queued follow-ups: request.metadata.deadline_seconds (T_F.6 timeout enforcement) and Diagnostics.resolver_log (T_F.7 structured logging). Neither is implemented here; both fields are already on the schema and the fold target consumes/emits them so future patches are additive.

  • validation:

    • 764 unit tests pass (758 pre-fold + 6 new in test_resolver_core.py covering success → ResolverSuccess, ResolutionFailure → ResolutionError, arbitrary exception → InternalError, marker override applied + restored).
    • Integration test tests/integration/test_resolver_protocol.py (JSON wire-shape canary) still passes — the wire schema is untouched.
    • Manual pipenv lock smoke on a tiny Pipfile produces byte-identical lockfiles (same _meta.hash) via the default subprocess path AND PIPENV_RESOLVER_PARENT_PYTHON=1.
  • status: Completed (commit 22921044; branch maintenance/code-cleanup-phase4-resolver-followups-2026-05).

  • log:

    • 2026-05-12 — Fold landed in one commit. Sibling agent T_F.5a (pluggable-backends design doc) ran concurrently on the same branch with no file collision (T_F.5a touched docs/dev/ only; T_F.4 touched code only).
  • files edited/created:

    • pipenv/resolver/core.py (new, ~250 lines: resolve_for_pipenv, _patched_marker_environment context manager, _apply_request_env, _dispatch_resolve_packages)
    • pipenv/resolver/main.py (subprocess adapter — _main now reads/ writes JSON around a single resolve_for_pipenv call; _apply_python_marker_override deleted; net -54 lines)
    • pipenv/utils/resolver.py (in-process adapter — new _resolve_in_process helper; PIPENV_RESOLVER_PARENT_PYTHON branch in venv_resolve_deps reduces to one delegating line; orphaned from pipenv import resolver import removed)
    • tests/unit/test_resolver_core.py (new, 6 tests for the fold target)

T_F.5a: Pluggable resolver backends — design (sign-off gate)

  • depends_on: [T_F.3]

  • location: docs/dev/initiative-f-backends-design.md (new, 948 lines).

  • description: Design-only document specifying the T_F.5 architecture: a Backend protocol with pip/uv implementations under pipenv/resolver/backends/, a name-keyed registry with dispatch precedence (CLI > env > Pipfile > default), single- Pipfile.lock output with _meta.resolver_backend discriminator, system-uv (no vendoring) posture, and an 8-task execution plan (T_F.5.1 .. T_F.5.8). Resolves the four open questions from initiative-f-typed-design.md §6a (opt-in section, dispatch mechanism, lockfile-format discriminator, vendoring posture) and surfaces 10 numbered sign-off questions gating T_F.5 execution. Maps the WIP origin/uv-backend commit 22359624 (~969-line pipenv/utils/uv.py + ~241-line patches across routines/environments) to the post-T_F.3-schema shape; the port shrinks to ~450 lines of backends/uv.py + ~40 lines for a new LockedRequirement.from_uv_package constructor.

    Key recommendations (each requires sign-off before T_F.5 execution starts): Pipfile opt-in via [pipenv] resolver_backend = "<name>"; CLI flag --backend NAME; lockfile remains single Pipfile.lock with additive _meta.resolver_backend field (uv-backend lockfiles only; pip omits); missing-backend behaviour fails loud with install instructions; cross- backend re-locking is full re-resolve (lockfile is input-only); no new schema fields (LockedRequirement.resolver_backend / Diagnostics.resolver_name deferred); uv is detected via shutil.which, never vendored; test matrix runs a representative subset under both backends with full dual-backend coverage on a nightly cron; news fragment category is .feature.

    Explicitly out of scope for T_F.5: uv as a wheel-install backend (resolve-only); PEP 751 pylock.toml emission; entry-point plugin discovery; poetry/conda/pdm backends; performance benchmarking; Diagnostics.resolver_log population; structured ConflictRecord extraction from uv stderr; pipenv → uv direct-invocation (still goes pipenv → pipenv-resolver → uv until T_F.4 fold lands).

  • status: Completed (commit 727b6540; awaits maintainer sign-off before T_F.5 execution begins).

  • log:

    • 2026-05-12 — Design doc landed (948 lines). 10 sign-off questions numbered for maintainer answer. Sibling agent T_F.4 (in-process / subprocess fold) running concurrently on the same branch; no file collision (T_F.4 touches pipenv/resolver/main.py + pipenv/utils/ resolver.py + new pipenv/resolver/core.py; T_F.5a touches docs only).
  • files edited/created:

    • docs/dev/initiative-f-backends-design.md (new, 948 lines)

T_F.5: Pluggable resolver backends — execution

  • depends_on: [T_F.5a maintainer sign-off]
  • location: NEW pipenv/resolver/backends/__init__.py, pipenv/resolver/backends/base.py, pipenv/resolver/backends/pip.py; pipenv/resolver/core.py (dispatcher); pipenv/resolver/schema.py (ResolverOptions.backend additive field); pipenv/utils/settings.py (Settings.resolver); pipenv/utils/resolver.py (plumbing); pipenv/environments.py (PIPENV_RESOLVER); pipenv/cli/options.py
    • pipenv/cli/command.py (--resolver NAME flag); pipenv/routines/context.py (ExecutionOptions.resolver); pipenv/utils/pylock.py (TODO(T_F.8) marker); news/T_F.5.feature.rst; tests/unit/test_resolver_backends.py.
  • description: Re-scoped to scaffolding only per the maintainer sign-off in initiative-f-backends-design.md (2026-05-12, answer 8). The uv backend port and dual-backend test matrix become a future initiative (T_F.8 or similar). What lands in this commit: the Backend protocol, the pip backend wrapping the existing resolve flow, the registry, the --resolver NAME / PIPENV_RESOLVER / [pipenv] resolver precedence chain, the fail-loud error path for unknown / unavailable backends, and a news fragment.
  • status: Complete (scaffolding shipped 2026-05-12). The uv backend port and dual-backend test matrix become T_F.8 (or similar) in a later iteration.
  • log:
    • 2026-05-12 — Scaffolding landed: backends subpackage + PipBackend + dispatcher + CLI/env/Pipfile plumbing + 9 unit tests. Wire shape unchanged (ResolverOptions.backend is suppressed on the wire when empty so the C2 protocol fixture passes byte-for-byte; no integration fixture regen needed).
  • files edited/created:
    • NEW pipenv/resolver/backends/__init__.py
    • NEW pipenv/resolver/backends/base.py
    • NEW pipenv/resolver/backends/pip.py
    • pipenv/resolver/core.py
    • pipenv/resolver/schema.py
    • pipenv/cli/options.py
    • pipenv/cli/command.py
    • pipenv/routines/context.py
    • pipenv/utils/settings.py
    • pipenv/utils/resolver.py
    • pipenv/utils/pylock.py
    • pipenv/environments.py
    • NEW news/T_F.5.feature.rst
    • NEW tests/unit/test_resolver_backends.py

T_F.6: Enforce wall-clock timeout via request.metadata.deadline_seconds

  • depends_on: [T_F.3, T_F.4]
  • location: pipenv/utils/resolver.py (subprocess deadline plumbing
    • new _resolve_deadline_seconds helper), pipenv/resolver/core.py (_wall_clock_deadline SIGALRM guard for the in-process branch), news/T_F.6.behavior.rst, tests/unit/test_resolver_timeout.py.
  • description: Replaces the reserved-but-unenforced RequestMetadata.deadline_seconds slot (queued in T_F.3 design Q11 / §5 decision 11) with a fully wired enforcement path. Resolution precedence: [pipenv] resolver_timeout_seconds in the Pipfile > PIPENV_RESOLVER_TIMEOUT_S env var > default (1800s, set by Setting). The resolved deadline is stamped on every ResolverRequest.metadata.deadline_seconds by _build_resolver_request so the wire envelope carries the same value the parent uses for subprocess.wait(timeout=...). The existing phase-2 hotfix (PIPENV_RESOLVER_TIMEOUT_S knob; commit 577a12a8) is preserved as the env-var precedence rung — its subprocess.TimeoutExpired path now reads from the new request-carried deadline, with a fallback to project.s.PIPENV_RESOLVER_TIMEOUT_S for back-compat with any caller not updated to thread the deadline through. The in-process debug branch (PIPENV_RESOLVER_PARENT_PYTHON=1) enforces the same deadline via a signal.SIGALRM guard installed by a new _wall_clock_deadline context manager in resolver/core.py; on expiry, resolve_for_pipenv returns an InternalError variant whose message names the elapsed deadline. Windows skips the in-process guard (no SIGALRM); the subprocess path remains the production enforcement path on all platforms. User-facing timeout error message updated to name BOTH the env var and the new Pipfile setting.
  • validation:
    • 7 new tests in tests/unit/test_resolver_timeout.py cover: subprocess TimeoutExpiredResolutionFailure with both overrides named; default deadline falls back to PIPENV_RESOLVER_TIMEOUT_S; Pipfile setting wins over env-backed default; invalid Pipfile values fall back; _build_resolver_request stamps metadata.deadline_seconds; resolve() honours the request-carried deadline; in-process branch returns InternalError on SIGALRM-mediated timeout.
    • Existing test_resolver_regressions.py timeout tests continue to pass unchanged.
    • Full unit suite green (780 passed, 9 skipped).
  • status: Completed (branch maintenance/code-cleanup-phase4-resolver-followups-2026-05).
  • log:
    • 2026-05-12 — Wired deadline through _build_resolver_requestrequest.metadata.deadline_secondsresolve()subprocess.wait. Added SIGALRM-based in-process guard. Sibling agent T_F.7 (Diagnostics.resolver_log) ran concurrently on the same two files with no logical collision: T_F.6 owns timeout wiring and the deadline-related lines; T_F.7 owns log capture.
  • files edited/created:
    • pipenv/utils/resolver.py (_resolve_deadline_seconds helper; _build_resolver_request stamps metadata.deadline_seconds; resolve() accepts deadline_seconds= keyword; _run_resolver_subprocess threads request deadline through; timeout error message names both overrides)
    • pipenv/resolver/core.py (_wall_clock_deadline context manager; resolve_for_pipenv wraps the resolve in the SIGALRM guard)
    • tests/unit/test_resolver_timeout.py (new, 7 tests)
    • tests/unit/test_resolver_parent_dispatch.py (existing fake_resolve stubs widened to **_kwargs to accept the new keyword)
    • news/T_F.6.behavior.rst (new)

T_F.7: Populate Diagnostics.resolver_log with structured resolve records

  • depends_on: [T_F.3, T_F.4]

  • location: pipenv/resolver/core.py (capture handler + context manager + integration into resolve_for_pipenv), pipenv/utils/resolver.py (verbose-mode surfacing), new tests/unit/test_resolver_diagnostics.py.

  • description: Replaces the reserved-but-empty Diagnostics.resolver_log slot (queued in T_F.3 design Q9 / §8) with a structured logging-handler capture wired into the unified resolve_for_pipenv driver. A new _BoundedListHandler (capped at 500 records) is attached to the pipenv and pip._internal.resolution loggers for the duration of the resolve via a _capture_resolver_log context manager that restores handler state and original level on exit (even on exception). Captured records are formatted as [LEVELNAME] message strings and land on response.diagnostics.resolver_log; truncation appends a ... (N records elided) sentinel.

    Parent-side surfacing in pipenv/utils/resolver.py: a new _surface_resolver_log(response, project) helper iterates the records and prints each via err.print when project.s.is_verbose() is true. Stderr behaviour in non-verbose mode is unchanged — the structured log is a complement, not a replacement (per Q9). Pip download chatter (pip._internal.network / pip._internal.operations.prepare) is intentionally NOT captured; stderr remains the appropriate channel for that.

    Both adapters (in-process debug bypass + subprocess) share the same capture because it lives inside resolve_for_pipenv. The schema field already existed (no schema bump); JSON round-trip preserves the records via the existing to_json_dict / from_json_dict envelope.

  • validation:

    • 9 new tests in tests/unit/test_resolver_diagnostics.py cover: pipenv logger capture; pip._internal.resolution logger capture; [LEVELNAME] message formatting; empty resolve yields empty tuple; handler removed after resolve (clean exit and exception path); flood test asserts 500-record cap + records elided sentinel; Diagnostics dataclass + tuple typing; JSON round-trip preserves the records.
    • Full unit suite green: 780 passed, 9 skipped.
  • status: Completed (branch maintenance/code-cleanup-phase4-resolver-followups-2026-05).

  • log:

    • 2026-05-12 — Wired logging capture into resolve_for_pipenv; surfaced verbose-mode log in pipenv/utils/resolver.py. Sibling agent T_F.6 (deadline enforcement) ran concurrently on the same two files; the resolve-flow integration commits landed under T_F.6's two commits (165bdb2f, e550e7f3) which include the T_F.7 _BoundedListHandler / _capture_resolver_log / _surface_resolver_log helpers. This T_F.7 commit completes the bookkeeping (new test file + plan entry).
  • files edited/created:

    • pipenv/resolver/core.py (_BoundedListHandler, _capture_resolver_log context manager, integration into resolve_for_pipenv success / resolution-error / internal-error branches)
    • pipenv/utils/resolver.py (_surface_resolver_log helper; call sites in _run_resolver_subprocess and _resolve_in_process; _resolve_in_process gains optional project parameter for the verbose surface)
    • tests/unit/test_resolver_diagnostics.py (new, 9 tests)

Parallel Execution Groups

WaveTasksCan Start When
0T0.1, T0.2Immediately
1aT_A.1, T_B.1, T_B.2, T_B.3, T_B.4Wave 0 complete
1bT_A.2T_A.1 complete
1cT_A.3T_A.2 complete
1dT_B.5T_B.1, T_B.2, T_B.3, T_B.4 complete
1eT_B.6, T_B.7T_B.5 complete (parallel-safe)
1fT_A.4T_A.3 complete + next release cut (held)
2aT_C.2Wave 0 complete
2bT_C.3T_C.2 complete
2cT_C.4T_C.3 complete + reviewer sign-off on design
2dT_C.5T_C.4 complete
2eT_C.6T_C.5 complete
2fT_C.7T_C.6 complete
2gT_C.8, T_C.9T_C.4 complete (parallel-safe with T_C.5–7 in principle; sequence behind for review bandwidth)
3-seedT_D.1Wave 0 complete
3-pocT_D.2T_D.1 complete
3-seedT_E.1T_B.7 complete
4-seedT_F.1T_E.1 complete
4-designT_F.2T_F.1 complete (design only; awaits sign-off)

Maximum parallelism at any moment is bounded by review bandwidth, not by the graph. The graph allows ~5 tasks to run concurrently in the early phase (the four B-triage tasks plus T_A.1); the practical limit is whatever Matt can review in a day. Suggested concurrency: 2–3 in flight at a time.

Testing Strategy

  • Per-task gate (agent-side): python -m pytest tests/unit -x green before the agent commits and pushes.
  • Per-PR gate (CI-side): full CI suite green before merge to maintenance/code-cleanup-2026-05.
  • Wave gate (Matt-side): a smoke pipenv command (pipenv install, pipenv lock, pipenv update) against a representative fixture Pipfile before the branch merges to main.
  • Coverage-gap prework: any task that refactors a code path with thin unit coverage adds tests first in the same PR — explicitly enumerated in the task validation when known. The currently-known gap is pipenv/utils/requirements.py and pipenv/utils/requirementslib.py (no dedicated test files); to be addressed during T_B.5 / T_E.1.

Risks & Mitigations

  • Risk: A triage decision in T_B.5 contradicts an assumption baked into T_A.* (e.g. is_valid_url's canonical home). Mitigation: T_A.1 produces its decision independently of T_B; if they conflict on a symbol, T_A's decision wins for URL/path scope and T_B's wins for everything else. Conflict is recorded in the triage doc.
  • Risk: Wave-2 routine migrations break a CLI invocation that the unit tests don't cover (e.g. a rare flag combination). Mitigation: Each migration PR includes the smoke-fixture run as validation. T_C.4 lands the dataclass without changing any call site, so issues caught later can be reverted at the migration PR without losing the scaffolding.
  • Risk: Initiative B's "vendor" decisions require moving files into pipenv/vendor/, which is normally vendor-tooling territory; an agent doing this naively could break the vendor sync. Mitigation: Any "vendor" execution PR is owned by Matt or @oz123, not by a swarm agent. The triage task can still be done by an agent.
  • Risk: Wave-3 seed tasks (T_D.1, T_E.1) get stale before their full plans are regenerated. Mitigation: Seed tasks produce time-stamped inventory docs; the follow-up plan-regeneration step explicitly checks for code drift.
  • Risk: Maintainer-time evaporation mid-wave (per PRD §8 risk table). Mitigation: Every task above is independently revertible and independently useful. The chain T_A.1 → T_A.2 → T_A.3 shrinks duplication on its own even if the rest of the plan never executes.
  • Risk: Agent commits skip the news-fragment convention. pipenv uses Towncrier fragments under news/ for user-visible changes. Behaviour-preserving refactors don't require a fragment, but the deprecation in T_A.2 and the removal in T_A.4 do. An agent that doesn't know the convention may land the change without the fragment and fail CI later, or worse, get merged with no user-facing notice. Mitigation: T0.2 (swarm-ops doc) explicitly enumerates which task categories require news fragments and links to an existing fragment as a template. Any task whose description mentions news/ includes the fragment in the same commit, not as a follow-up.
  • Risk: Hidden user-visible change slips through an agent PR. Waves 1–2 are advertised as behaviour-preserving, but a refactor could accidentally alter logging, error messages, or exit codes that users depend on. Mitigation: T0.2's checklist requires every agent PR to grep its own diff for print(, logger., raise , sys.exit( and report any matches in the PR description; reviewer (Matt) treats any such match as a flag requiring deliberate sign-off.

Out of Scope (Plan-Level)

  • Anything under pipenv/patched/ or pipenv/vendor/. Initiative B may move into pipenv/vendor/ under maintainer supervision; agents do not modify existing contents.
  • Version bumps, CHANGELOG, release tooling.
  • Pipfile / Pipfile.lock format changes.
  • Anything in the PRD's §9 (Out of scope) section: performance work, feature parity with uv, CLI changes, new features.