ADR-platform-007
third_party vendor pattern: method repos vendored into workspace/nodesets/_upstream/ + _vendored/; env repos stay as submodules
⚠ Partially superseded (2026-06-30). The "env-class repos stay as submodules" layer (decision 1 below) is no longer in effect: all
third_party/repos are now pinned install-time clones, not git submodules. The former env submodules (habitat-lab,VLN-CE,Matterport3DSimulator) are cloned and pinned byscripts/install/lib/thirdparty.sh— the single place their commit IDs now live — invoked from the per-envinstall_ac_*.shscripts;.gitmoduleswas deleted. The privatevla_workspacerepo was severed entirely:lerobot+liberoclone from their public upstreams, andopenpi-client+ the LIBERO norm_stats are vendored into thepolicy_vlanodeset. Decision 2 (method repos →_upstream/), decision 3 (runtime code →_vendored/), and the install-time-clone convention itself remain in force.
Context
Pre-2026-05-18 the third_party/ tree mixed three kinds of upstream code: (1) env-class repos that are genuine runtime dependencies (habitat-lab + VLN-CE pip-install editable, Matterport3DSimulator builds .so via CMake, ManiSkill2_real2sim + SimplerEnv + vla_workspace are required by VLA nodesets); (2) method-class repos cited by individual reasoning nodesets (MapGPT, NavGPT, Open-Nav, SmartWay-Code, VoxPoser, open-eqa, explore-eqa, tooleqa) — most cited for prompts / constants / model defs already reimplemented verbatim inside workspace/nodesets/; (3) reference-only clones under zz_just_for_refer/ kept for human reading (18 submodules: ADAS / AFlow / AdaPlanner / AgentSquare / CA-Nav-code / ComfyUI / ComfyUI_frontend / DetAny3D / EvoAgentX / GroundingDINO / MaAS / NavGPT-2 / STRIDER-Nav / UniDepth / dify / langgraph / self-refine / vln-zero).
Pain points: (a) git clone --recurse-submodules took minutes and pulled GB of reference code that no runtime touches; (b) every method nodeset that hardcoded third_party/<repo>/<path> in sys.path.insert or os.path.join defaults was tied to upstream HEAD drift — pinning happened only at submodule commit level, not at the file-content level; (c) the zz_just_for_refer/ subtree never had documented purpose, and several entries (NavGPT inside it, an Open-Nav top-level clone shadowing the one under zz_just_for_refer/) had drifted to inconsistent states; (d) the workspace/ standalone policy (memory: feedback_workspace_standalone) was nominally enforced but in practice the engines in workspace/nodesets/server/{opennav_waypoint,smartway_waypoint}/ did runtime sys.path.insert(0, third_party/<repo>) — a tight coupling that meant deleting third_party/<repo> silently broke method nodesets at first import.
Decision
Adopt a three-layer convention separating attribution, vendored source, and external installs:
- Env-class repos stay as submodules under
third_party/. They are runtime dependencies that need pip-installable layouts or CMake builds — vendoring would duplicate large mature codebases. Six entries remain post-cleanup:habitat-lab,VLN-CE,Matterport3DSimulator,ManiSkill2_real2sim,SimplerEnv,vla_workspace. - Method-class repos move to
workspace/nodesets/_upstream/<name>/. Each entry is a metadata directory containingfetch_upstream.sh(clones the pinned upstream commit to a sibling./upstream/, gitignored viaworkspace/nodesets/_upstream/*/upstream/) plus aLICENSEcopy where the upstream provides one. Optionalfetch_data.shfor runtime-data fetch (used bynavgpt,explore-eqa). Nine entries:mapgpt,voxposer,navgpt,open-eqa,tooleqa,explore-eqa,prismatic-vlms,open-nav,smartway-code. - Runtime code that must be importable lives inside the consuming nodeset as
_vendored/<upstream-subdir>/. Leading-underscore prefix means the nodeset registry skips this directory during scan. Engines dosys.path.insert(0, <nodeset>/_vendored)with the same string-match imports as before, preserving byte-identical upstream code. Currently used for Open-Nav'swaypoint_prediction/(6 .py files) and SmartWay-Code'swaypoint_predictor/(17 .py files including a bareimport utilssibling pattern that requires a second sys.path insert). - External Python packages install directly from pinned upstream commits:
pip install git+https://<upstream>.git@<commit>. Used ininstall_ac_hmeqa.shforprismatic-vlms, replacing the previous editable install from a nested submodule. zz_just_for_refer/drops all 18 submodule entries. Three local untracked dirs (claudecode,DiscussNav,agentcanvas-refs) survive as gitignored personal reference material.- Reference clones happen on-demand, not by default.
fetch_upstream.shdefaults to placing the clone inside the project atworkspace/nodesets/_upstream/<name>/upstream/(predictable path, survives reboots, gitignored) — overridden via$1. No upstream code is pulled atgit clonetime for any method repo.
Alternatives
(a) Keep all method repos as submodules, just flatten zz_just_for_refer/ — rejected because the underlying coupling (runtime sys.path.insert(0, third_party/<repo>) tied nodesets to a directory that fresh clones might not have populated) stays unchanged; the registry of "things to clone before this works" remains opaque.
(b) Vendor everything: copy entire upstream repos into workspace/nodesets/<name>/_vendored/ — rejected because most method nodesets cite only prompts / constants that the user has already reimplemented verbatim inline (MapGPT, VoxPoser). Vendoring the full repos would 10× the workspace diff with code nobody runs.
(c) Pure pip install git+<url>@<commit> for every method repo — rejected because most method repos are research codebases without a setup.py / pyproject.toml that pip can install. Even the ones with installable shape don't expose the modules the nodesets import (the imports target waypoint_prediction/ sub-tree files, not a top-level package).
(d) Submodule the method repos but lazily init only on first use — rejected: doesn't address the "where does the upstream code live for re-syncing" question, and adds a runtime branch ("if not initialized, init now") that's hard to reason about in server-mode nodesets running under conda activation hooks.
(e) Keep zz_just_for_refer/ as a real "human reading" submodule farm — rejected because (1) submodules induce clone-time pulls regardless of recursion flag presence in user habits; (2) zero of the 18 reference entries had any functional import in workspace/; (3) on-demand fetch via the same fetch_upstream.sh pattern serves the same purpose with zero default cost.
(f) Store the LICENSE files at the repo root instead of co-located with each _upstream/<name>/ — rejected because per-directory LICENSE is the audit-friendly convention (compliance tooling expects to find it next to the vendored content).
Rationale
The env-vs-method split is the load-bearing distinction. Env repos are infrastructure: they need pip-installable layouts, CMake builds, or simulator binaries. Vendoring them would be a bad-faith fork of a mature codebase. Method repos cite prompts, constants, or single-purpose model defs — vendoring is cheap, locks the project against silent upstream drift, and makes workspace/ truly standalone (per feedback_workspace_standalone memory). The two classes have different lifecycles and deserve different storage.
_upstream/<name>/ as metadata-only directory is the cheapest possible attribution layer. It always lists the URL + pinned commit + license, always provides a one-command path to re-fetch the upstream snapshot for diffing, and never imposes clone-time cost. The pattern scales linearly: adding a new method nodeset adds one directory with two-to-three small files.
_vendored/<upstream-subdir>/ co-located with the consuming nodeset keeps Python's namespace flat. sys.path.insert(0, nodeset/_vendored) works identically whether the consumer is in ac-vlnce conda env (Open-Nav) or ac-smartway conda env (SmartWay-Code). The leading-underscore registry-skip convention prevents accidental double-registration. Byte-identical upstream copies make audit + re-sync trivial (diff -r _vendored/X _upstream/X/upstream/X).
pip install git+<url>@<commit> for packageable upstreams (Prismatic VLM) keeps the install path declarative and lockable — the commit pin is in the install script, the package lands in site-packages the standard way, and editable rebuilds happen via _upstream/<name>/fetch_upstream.sh + pip install -e ./upstream when local edits are needed.
Three-phase migration (4 batches) bounded blast radius: batch 1 (MapGPT + VoxPoser + spatialx) was zero-risk validation — no runtime use, only docstring updates. Batch 2 (NavGPT + open-eqa + tooleqa) moved data references. Batch 3 (explore-eqa + prismatic-vlms) introduced the pip install git+ idiom. Batch 4 (Open-Nav + SmartWay-Code) vendored the only real code subtrees, with a Python smoke test of sys.path + find_spec + instantiation before deleting the submodules.
On-demand fetch destination = ./upstream/ sibling, not /tmp/: predictable, survives reboots, lives next to the LICENSE + fetch script for review convenience, gitignored via a single .gitignore glob (workspace/nodesets/_upstream/*/upstream/).
Affected docs
Migration delivered across 6 commits: 2d012d3 (batch 0: flatten zz_just_for_refer + drop 18), eefafc0 (batch 1), b53eab5 (fetch-script destination fix), 0af0a9f (batch 2), a9f3ced (batch 3), b617c21 (batch 4). Per-doc updates:
.claude/PROJECT_OVERVIEW.md—third_party/row rewritten to describe the env-only +_upstream/+_vendored/layout..gitignore— addedworkspace/nodesets/_upstream/*/upstream/rule; dropped stale spatialx/tooleqa lines.scripts/install/install_ac_hmeqa.sh— Prismatic VLM install switched topip install git+https://....scripts/data/fetch_dataset_openeqa.sh— open-eqa-v0.json fetched via curl from pinned commit.scripts/install/install_ac_smartway.sh— vendored-subtree existence check.scripts/data/fetch_episodes_vln.sh— HM-EQA data sourced from_upstream/explore-eqa/upstream/data/.workspace/nodesets/server/{opennav_waypoint/__init__,smartway_waypoint/__init__,smartway_waypoint/_engine,smartway_perception/__init__,matterport3d}.py— defaults + docstrings.workspace/nodesets/_upstream/<9 dirs>/{fetch_upstream.sh,LICENSE,fetch_data.sh}— new metadata directories.workspace/nodesets/server/{opennav_waypoint,smartway_waypoint}/_vendored/— 6 + 17 vendored .py files.docs/pages/developer-guide/nodesets/{explore-eqa,hmeqa}.html— references updated.architect-site/reference/fastsmartway/algorithm.html— references updated.- Three
.claude/notes/.claude/memory/.claude/analysisfiles — path refs updated.
Carry-over follow-ups (not yet resolved): (i) workspace/nodesets/_upstream/tooleqa/fetch_upstream.sh has a TODO marker — upstream URL needs confirmation (local clone's origin pointed at the user's own AgentCanvas fork, not the canonical paper release); (ii) NavGPT R2R precomputed-observation data is missing in the upstream submodule itself — graphs that consume matterport3d.graph_get_observation already handle the None path via online BLIP-2 + Faster R-CNN, so this is by-design fallback rather than a bug.
Changelog
2026-05-18 20:04: Initial draft documenting the third_party vendor pattern (4-batch migration completed earlier this session — commits2d012d3,eefafc0,b53eab5,0af0a9f,a9f3ced,b617c21).