open-swe/docs/upstream-sync/domain-reorg/reorg-scoping-report.md
Adam Moussa 373d888426
docs: land reorg in ledger, fix path refs, ship plan artifacts (C7)
C7 of the domain-reorg adoption (build plan docs/upstream-sync/domain-reorg/
reorg-build-plan.md, step C7):

- CLAUDE.md / AGENTS.md: retarget architecture path references to the new
  layout — graph entrypoints via agent.graphs.* shims, the agent/api/ +
  agent/webhooks/*_routes.py FastAPI split (webapp.py now a shim),
  agent/review/ package, tests/<domain>/ test paths, and the new-graph/
  test conventions. README.md already pointed at docs/ (C1) — no change.
- triage.jsonl: flip 8356eb34 (#1726) to landed on branch
  refactor/domain-reorg-adoption; add re-triage notes to the 8 unblocked
  rows (#1732/#1761/#1744/#1748/#1742 clean, #1736/#1758/#1760 near-clean).
  triage.md regenerated via make triage-render.
- Ship the plan, scoping report, move-map artifacts, and the
  domain-reorg-adoption workflow so the exercise is reproducible.

Memory + Confluence handled out-of-band (not in this commit): project
memory updated with the new module layout; Confluence check found no IT
page documents the repo module map — no Confluence change required.
2026-07-17 15:01:45 -04:00

25 KiB
Raw Blame History

Scoping report — adopting upstream domain reorg 8356eb34 (#1726) as a file-move exercise

Date: 2026-07-17. Repo: /Users/adammoussa/Documents/repositories/seahaven/open-swe (branch dev, clean). Strategy scoped: fork file contents, upstream layout — replay the reorg's moves onto the fork's own tree; never take upstream file contents. Merge-base with upstream/main: f6c215ff. Reorg commit sits 91 upstream commits past the merge-base. Current truthful conflict surface (git merge-tree --write-tree --name-only HEAD upstream/main): 140 conflicted paths (docs/upstream-sync/domain-reorg/merge-tree-now.txt).


1. Move-map (git diff-tree -r -M50 --name-status 8356eb34^ 8356eb34)

298 entries total (raw file: docs/upstream-sync/domain-reorg/movemap-m50.txt):

class count meaning
R100 pure renames 150 byte-identical moves (mostly tests/* → tests/<domain>/, ui/src/{components,lib}/agents/* → ui/src/features/*, agent/reviewer_*/review_style_* → agent/review/)
R<100 rename+edit 77 scores R062–R099; every sampled edit is an import-path / monkeypatch-target rewrite only (see §Spot-checks)
M in-place edits 49 shim-ification (webapp.py 2007-line reduction, server.py constant extraction), import rewires in agent/tools/*, agent/webhooks/*, evals/, ui/src/routes/*, config files
A true adds 21 all thin structure: agent/graphs/{agent,analyzer,chat,reviewer,scheduler}.py (re-export shims), agent/runtime/{constants,execution,sandbox}.py (constants + delegating wrappers around agent.server), agent/api/{app,health}.py, agent/webhooks/{common,github_routes,linear_routes,slack_routes}.py, agent/{providers,resources,review,runtime,graphs,api}/__init__.py
D true deletes 1 ui/src/components/agents/AgentPromptBar.tsx — a 2-line re-export shim, re-added at ui/src/features/agents/components/AgentPromptBar.tsx with the path retargeted (A/D pair is cosmetic, not a content loss)

Content-consolidations (real redistribution rather than move): effectively one — the FastAPI layer. agent/webapp.py (upstream pre-reorg ~2k lines) is split into agent/webhooks/common.py (shared dispatch/verify helpers), agent/api/app.py (composition), agent/api/health.py (/health + /webhooks/run-complete), and per-source *_routes.py; webapp.py becomes a 4-line compatibility shim (from .api.app import app). Secondary micro-extractions: server.py → runtime/constants.py (DEFAULT_LLM_MODEL_ID, DEFAULT_LLM_MAX_TOKENS, DEFAULT_RECURSION_LIMIT, MODEL_CALL_RECURSION_LIMIT) and runtime/execution.py (graph_loaded_for_execution); prompt.py → default_prompt.md loaded via importlib.resources from agent/resources/. The "consolidate reviewer modules" sub-commit is not a content merge — it is nine 1:1 module moves into agent/review/ (R098–R100) plus import rewires.

langgraph.json does change: graph entrypoints retarget from agent.server:traced_agent / agent.reviewer:... / agent.analyzer:... / agent.chat:... / agent.scheduler:... to agent.graphs.<name>:<same symbol>. The targets are pure re-export shims (e.g. agent/graphs/agent.py = from agent.server import get_agent, traced_agent), so behavior is unchanged, but the deployment manifest strings are not. http.app stays agent.webapp:app (via the compat shim). See §Hazards (d).


2. Collision set (move-map × fork divergence)

Fork divergence since merge-base: 322 paths (130 A / 188 M / 3 D / 1 R; raw: docs/upstream-sync/domain-reorg/fork-divergence-status.txt). Fork-added files split against upstream trees: 67 are shared (cherry-picked, exist upstream pre-reorg → move-map applies) and 64 are TRUE fork-only (exist nowhere upstream).

Headline classification:

bucket count
MECHANICAL — fork-touched, R100 pure rename → git mv fork's version 57
MECHANICAL-trivial — moved by reorg, fork never touched → git mv (identical either way) 127
HAND-CARRY (light) — fork-touched, R<100 → git mv + replicate upstream's import-rewrite pattern on fork content 34
HAND-CARRY (real) — fork-modified AND reorg-modified-in-place (M×M) 34 (≈10 substantive, rest are 1–3-line import seds)
FORK-ONLY placement decisions 64 files, of which 13 test files need explicit new homes; nearly all fork-only source stays put (reorg does not move agent/tools/, agent/utils/, agent/middleware/, agent/integrations/, agent/dashboard/, agent/webhooks/ handler modules)
moved upstream but absent from fork (added in the 91 interim commits — no-op for us) 9

2a. MECHANICAL (fork-touched, pure rename — the bulk)

git mv fork's file to the new path; no content change. 57 files:

  • Root/docs/resources: INSTALLATION.md → docs/INSTALLATION.md, default_prompt.md → agent/resources/default_prompt.md (pairs with the prompt.py loader hunk — see HAND-CARRY), agent/reviewer_findings.py → agent/review/findings.py.
  • Tests (46): the full tests/test_*.py → tests/<domain>/test_*.py set, e.g. tests/test_dispatch.py → tests/agent/, tests/test_workflow_push_guard.py → tests/agent/, tests/test_plan_mode.py, tests/test_plan_review.py → tests/agent/, tests/test_auth_sources.py, tests/test_authorship.py, tests/test_encryption*→ tests/auth/, tests/test_github_app.py, tests/test_open_pull_request.py, tests/test_pr_creation_guard.py → tests/github/, tests/test_gateway.py, tests/test_proxy_auth.py, tests/test_local_integration.py → tests/sandbox/, sanitize/middleware tests → tests/middleware/, model tests → tests/models/, Slack tool tests → tests/slack/, dashboard/team-settings tests → tests/dashboard/, tests/test_completion_webhook.py → tests/webhooks/, etc. (full list in docs/upstream-sync/domain-reorg/cross.json, key mech).
  • UI (10): ui/src/lib/agents/{api,queries,types,streamMessagesToUi,sidebarFilter,messageTimestamps}.ts → ui/src/features/agents/lib/, ui/src/components/agents/PrHeader.tsx → ui/src/features/reviews/components/, MessageTimestamp.tsx, diffUtils.ts.

Plus the 127 fork-untouched moved files (same treatment, zero divergence risk).

2b. HAND-CARRY

(i) R<100 moved files fork also touched — 34 files. Upstream's edits are exclusively path rewires; replicate the same rewires on the fork's content after git mv:

  • Python: agent/reviewer_publish.py → agent/review/publish.py (R099: from .reviewer_findings import → from .findings import, from .utils.X → from ..utils.X), same for reviewer_reconcile.py, reviewer_trace_context.py; reviewer test files R062–R099 retarget webapp → webhook_common / github_webhooks monkeypatch modules; tests/test_e2b_integration.py → tests/sandbox/ (fork re-implemented E2B sync — keep fork content, fix imports only).
  • UI: 20 .tsx/.ts files where the only edits are @/components/agents/... → @/features/agents/components/... and @/lib/agents/... → @/features/agents/lib/... (verified zero non-import edits on the worst-looking case, AgentThreadView.tsx R092). Fork-diverged files in this set that must keep fork content: PlanReview.tsx (plan-mode #130), WorkflowApprovalCard.tsx, AgentsSidebar.tsx, SidebarFilterMenu.tsx, AgentGitPanel.tsx, AutomationEditor.tsx.

(ii) M×M in-place overlap — 34 files, of which the substantive ten:

file upstream's reorg edit fork action
agent/webapp.py (fork: 2,590 lines) gutted to 4-line shim; content → webhooks/common.py + api/app.py + api/health.py + *_routes.py the big one — split the fork's monolith the same way, carrying fork-only Jira/Confluence/Connect routes (see §3)
agent/server.py extract 4 constants → runtime/constants.py, graph_loaded_for_execution → runtime/execution.py; adds public aliases get_cached_sandbox_backend etc. replicate extraction on fork content; fork's sync sandbox lifecycle stays in server.py/utils/sandbox_state.py untouched — runtime/sandbox.py shim delegates to agent.server (upstream's own shim does exactly this, so it wraps fork code cleanly)
agent/prompt.py DEFAULT_PROMPT_PATH env-only + importlib.resources load of agent/resources/default_prompt.md apply this one hunk by hand to fork's heavily customized prompt.py; verify the wheel packages the .md (upstream did not touch pyproject.toml; packages=["agent"] under hatchling should carry it — build and check)
agent/reviewer.py / agent/analyzer.py / agent/chat.py import rewires to agent.review.* / small sed-level on fork content
agent/webhooks/{github,linear,slack}.py from agent import webapp → from . import common; webapp.X → common.X (module-attribute style preserved so monkeypatching keeps working) caution: these exist on both sides with different content (fork built its own split earlier). No git mv applies — sed the fork's files; never adopt upstream's
tests/conftest.py from agent import webapp → from agent.webhooks import common as webhook_common (auto-review fixture) same sed on fork's conftest
tests/e2e/harness.py from agent.webapp import app, generate_thread_id_from_slack_thread → from agent.api.app import app + from agent.utils.thread_ids import ... fork already has agent/utils/thread_ids.py — same retarget
ui/tsconfig.json / ui/eslint.config.js replace the 8-file ported/ exclude lists with src/features/agents/experiments/** adopt the glob (simplification, no behavior change); ui/vite.config.ts upstream edit is cosmetic churn on the fork-only mock-harness plugin — skip
langgraph.json entrypoints → agent.graphs.* retarget fork's file and add ci_monitor": "agent.graphs.ci_monitor:get_ci_monitor" shim for the fork-only graph (upstream deleted its ci-autofix cluster; fork keeps it)
AGENTS.md/CLAUDE.md/README.md doc-path updates fork-rewritten docs — update path references manually (CLAUDE.md architecture sections name agent/webapp.py, tool/test paths)

The remaining ~24 M×M files (agent/tools/add_finding.py and the other reviewer tools, agent/dashboard/{eval_jobs,review_api,review_chat_api}.py, evals/reviewer/*, ui/src/routes/*.tsx, agent/utils/github_org_membership.py, agent/middleware/settle_review_check.py) take 1–3-line import seds (..reviewer_findings → ..review.findings, @/components/agents → @/features/...).

Blast-radius counts (fork tree): 38 Python files import reviewer_*/review_style_* modules; 12 Python files import agent.webapp; 240 monkeypatch.setattr(webapp, ...) sites across 11 test files must retarget to webhook_common/handler modules; 54 UI files import @/components/agents or @/lib/agents.

2c. FORK-ONLY placement proposals (64 true fork-only files)

Upstream's domain logic: tools stay flat in agent/tools/; util clients in agent/utils/; webhook handlers in agent/webhooks/<source>.py with HTTP routes in agent/webhooks/<source>_routes.py; tests in tests/<domain>/.

Source — stays put (no move needed):

  • Atlassian tools agent/tools/{jira_*,confluence_*}.py (11 files) → stay (agent/tools/ unmoved by reorg).
  • agent/utils/{jira,confluence,adf,atlassian_connect,jira_project_repo_map,confluence_space_repo_map}.py → stay.
  • Fork middleware, agent/integrations/e2b.py, dashboard additions, .githooks/, .github/, scripts/, docs/upstream-sync/, docs/repo-conventions/, deploy/ → stay.

Source — new homes created by the split (part of the webapp-split commit):

  • agent/webhooks/jira.py, agent/webhooks/confluence.py → stay as handler modules; add fork-only agent/webhooks/jira_routes.py and agent/webhooks/confluence_routes.py mirroring upstream's github_routes.py pattern (routes access helpers via common.X).
  • Atlassian Connect lifecycle/descriptor routes (/connect/*, GET /connect/atlassian-connect.json) → propose agent/webhooks/confluence_routes.py (they are Confluence-trigger plumbing) or a dedicated agent/webhooks/connect_routes.py if cleaner; register in fork's agent/api/app.py.

Tests — 13 fork-only/uncovered files:

file proposed home
tests/test_jira_webhook_{author,corroboration,replay}.py, tests/test_confluence_webhook.py, tests/test_linear_webhook_replay.py tests/webhooks/
tests/test_atlassian_connect.py tests/auth/ (JWT/qsh verification surface; alternative tests/webhooks/)
tests/test_jira_utils.py, tests/test_confluence_utils.py tests/tools/ (client layer backing the Jira/Confluence tools)
tests/test_repo_binding_isolation.py (TID-COLLIDE-01 guard) tests/sandbox/ (it exercises the sandbox/GitHub-token per-thread caches; alternative tests/webhooks/)
tests/test_auth_error_leak.py tests/auth/
tests/test_app_bot_identity.py tests/github/ (bot authorship/PR attribution)
tests/test_ci_autofix.py, tests/test_autofix_webhook.py tests/github/ (upstream deleted the impl cluster; fork keeps agent/ci_autofix.py, agent/ci_monitor.py in place)

UI fork-only: ui/src/components/PwaUpdateToast.tsx → ui/src/components/ stays (not an agents-feature file); ui/src/lib/auth-redirect*.{ts,tsx} → stay in ui/src/lib/ (upstream kept non-agents lib files there); ui/src/routes/$owner.$repo.pull.$number.tsx → stays (routes/ not moved). tests/e2e/** (fork harness + specs) → stays; upstream kept tests/e2e/ in place.


3. Consolidations / decision list for Adam

  1. FastAPI layer split (the only true consolidation) — FLAG-HUMAN, auth surface. Fork's 2,590-line agent/webapp.py carries fork-only webhook auth: verify_jira_secret (+ optional HMAC/timestamp), Atlassian Connect JWT/qsh handling, the GitHub/Slack/Linear signature paths, token-attribution gating, TID-COLLIDE-01 thread-binding. Splitting it into webhooks/common.py + api/ + *_routes.py moves signature-verification and dispatch code across files. Recommendation: adopt the split (it is behavior-preserving by construction — upstream kept module-attribute access precisely so monkeypatched tests stay valid, and the fork's webhook tests are the safety net), but treat the split commit as a sensitive change: /sh-security-review required (auth/webhook surface), and per the fork playbook surface the resolution choice to you before merge. No GPT-4.1 cross-family review strictly triggered (no IAM/Lambda-signature change), but the split of webhook verification code is a reasonable opt-in candidate.
  2. langgraph.json entrypoint retarget — deployment-manifest change (graphs → agent.graphs.* shims; http.app unchanged). Verify the LangGraph deployment redeploys cleanly; decide whether fork-only ci_monitor gets a agent/graphs/ci_monitor.py shim (recommended, for symmetry) or keeps its old path (inconsistent).
  3. Reviewer consolidation → agent/review/ package: adopt. It is 9 pure moves + import rewires; fork's reviewer customizations (#1713 reconciliation, findings model, publish flow) ride along untouched. No content decision needed.
  4. CI-autofix cluster (upstream deleted agent/ci_autofix.py; fork keeps it): confirm keep-baseline, and approve its post-split wiring point (its webhook triggers live in the split github.py/common.py) + test placement (tests/github/).
  5. UI experiments/ adoption: upstream renames ported/ → features/agents/experiments/ + chat/ and swaps the 8-file tsconfig/eslint exclude lists for one glob. Adopt (pure simplification). Fork-diverged DiffView.tsx, CloudPromptBar.tsx, etc. keep fork content.
  6. Atlassian placements (§2c): approve jira_routes.py/confluence_routes.py naming and where Connect lifecycle routes land — this is fork-only auth surface; your call on connect_routes.py vs folding into confluence_routes.py.
  7. Sequencing vs #1736 (bind cached GitHub tokens to users — priority security pick): recommend adopting the reorg first, because #1736 is written against agent/webhooks/common.py, which only exists after the split; it then lands near-clean (still gated by cross-review + /sh-security-review per its ledger row).

4. Hazards check (CLAUDE.md playbook)

  • (a) Thin-router re-import rebinds: post-reorg upstream is clean — git grep 'from \.webhooks\.' 8356eb34 -- agent/webapp.py agent/api agent/webhooks/common.py returns nothing; routes access handlers via from . import github as service + service.X/common.X module-attribute style, so the rebind hazard the playbook documents is designed out. Residual risk only if a hand-carry file drifts toward upstream content: agent/webhooks/{github,linear,slack}.py exist on both sides with different content (fork versions carry the fixes) — taking upstream's file here silently drops fork behavior. Same for tests/conftest.py and tests/e2e/harness.py. Rule for the exercise: upstream content may only be taken for the 21 "A" structural shims, everything else moves fork content.
  • (b) Paired tests move with impls: the map's test moves are 1:1 with unmoved impls (impl modules stay importable at old or shimmed paths), so no vertical splits arise; the 13 uncovered fork tests get the placements in §2c. Keep tests/test_e2b_integration.py (fork's sync E2B) as fork content when moving to tests/sandbox/. tests/middleware/test_sandbox_recovery.py → tests/sandbox/ (fork has tests/middleware/? — it exists only as the upstream pre-path; fork's copy sits wherever it was cherry-picked; include in the mechanical sweep).
  • (c) Tooling path references: verified —
    • Makefile: only generic tests/ (TEST_FILE ?= tests/) — unaffected; make triage-render/-check scripts don't reference moved paths.
    • pyproject.toml: packages = ["agent"], testpaths = ["tests"] — both recursive, unaffected; verify wheel includes agent/resources/default_prompt.md (only real packaging check).
    • .github/workflows/ci.yml: references tests/e2e/** (unmoved) and ui/ bun checks — unaffected; reviewer-eval.yml → evals/ (unmoved).
    • langgraph.json — changes (see decision 2); fork-only tests/e2e/langgraph.e2e.json uses its own agent_entrypoint.py (imports agent.server:traced_agent, which remains valid) — no change needed.
    • UI: @ alias root (src/) unchanged; vite.config.ts/tsconfig.json/eslint.config.js need only the exclude-glob swap; bun stays — do not import upstream's pnpm bits (fork oddly carries a ui/pnpm-workspace.yaml; consider deleting it in this exercise as lockfile hygiene, per playbook rule 7).
    • scripts/, .githooks/: no moved-path references found.
  • (d) Public entrypoints: upstream's claim "without changing its public entrypoints" is true behaviorally, false textually — langgraph.json graph strings all change to agent.graphs.* (verified by diff). HTTP surface (agent.webapp:app) and all webhook URLs unchanged. Fork must mirror the graph-string change plus its ci_monitor extra.

5. Payoff — the 20 deferred ledger rows (docs/upstream-sync/triage.md)

Of the 19 deferred rows besides 8356eb34 itself:

  • Clean picks after adoption (path remap was the blocker): 5 — #1732 dashboard label rendering, #1761 capitalize labels, #1744 collapse git panel, #1748 Linear issue search (test-path remap), #1742 defensive copy.
  • Near-clean after adoption (small content deltas or review gates remain): 3 — #1736 GitHub token binding (biggest beneficiary: written against webhooks/common.py; auth gates still apply), #1758 sandbox-create JSON env, #1760 separate LangSmith key/endpoint (both additive to diverged integrations/langsmith.py — small reconciles).
  • Still content reconciliation for fork-divergence reasons: 9 — #1724 (Slack NL plan approvals), #1731 (OpenAI Responses history vs #155 middleware), #1734 (Slack DMs), #1737 (reviewer diff bounds), #1741 (SSRF image fetches), #1733 (todos default-off vs fork prompt), #1735 (untagged two-party Slack), #1747 (Slack run links), #1719 (web-tool offload vs sync sandbox lifecycle). Adoption still removes their test/UI path-remap tax.
  • Not path-related (product/deps decisions): 2 — #1727 (GPT-5.5 default; rides the no-OpenAI-models decision), #1745 (deepagents 0.7.0a7 alpha validation).

Net: 8 of 19 rows become clean/near-clean, and every future upstream commit stops needing path remaps — the standing tax on the cherry-pick pipeline disappears (recent won't-merge rows #1759/#1769 cited the layout mismatch explicitly).

6. Execution plan sketch (one commit per cascade class; validate ruff → pytest --co → unit → e2e per layer)

Work on a throwaway branch off dev; promote as one PR with granular commits.

  1. C1 — docs/resources/assets (~1h): git mv INSTALLATION.md/CUSTOMIZATION.md → docs/, static/ → assets/ (check README badge/image refs), default_prompt.md → agent/resources/ + apply the prompt.py importlib.resources hunk + agent/resources/__init__.py; build wheel, confirm the .md ships. Gate: ruff + unit.
  2. C2 — agent/review/ package (~half day): mv 9 reviewer/style modules, rewrite the 38 importer files, add agent/review/__init__.py (fork's export surface). Gate: ruff + pytest --co + reviewer unit tests.
  3. C3 — graphs/runtime/providers shims + langgraph.json (~2h): take upstream's 21 structural A-files nearly verbatim (they delegate to agent.server etc., which is fork code), add fork-only agent/graphs/ci_monitor.py, retarget langgraph.json. Gate: pytest --co + make dev boots all graphs. Adam sign-off: decision 2.
  4. C4 — FastAPI split (~1–1.5 days, the critical commit): split fork's webapp.py → webhooks/common.py + api/{app,health}.py + {github,linear,slack,jira,confluence}_routes.py (+ Connect routes per decision 6); sed handlers webapp. → common.; retarget 240 test monkeypatch sites + tests/conftest.py + tests/e2e/harness.py; keep webapp.py shim. Gate: full unit + E2E (Playwright + real LangGraph dev server) — the only layer that catches wiring drift. Adam sign-offs: decisions 1, 4, 6; /sh-security-review on this commit.
  5. C5 — tests// moves (~half day): mechanical map moves (§2a) + R<100 monkeypatch retargets not already done in C4 + the 13 fork-only placements (§2c). Gate: pytest --co + full unit (paths only, no logic).
  6. C6 — UI features/ moves (~half day–1 day): git mv per map incl. ported/ → experiments|chat, rewrite @/ imports in the 54 importer files + moved files, tsconfig/eslint exclude glob, AgentPromptBar shim retarget, delete ui/pnpm-workspace.yaml (lockfile hygiene). Gate: bunx tsc --noEmit + bun build + Playwright e2e. Adam sign-off: decision 5.
  7. C7 — docs + ledger + memory (~2h): update fork CLAUDE.md/README/AGENTS.md path references, flip 8356eb34 to Landed in triage.jsonl (+ re-triage notes on the 8 unblocked rows), update the open-swe project memory, Confluence architecture page if module map is documented there.

Effort estimate: 3–5 focused days end-to-end, dominated by C4. Rollback story is clean: each commit is independently revertable until C4; after C4, C5/C6 are path-only.

Spot-checks validating the classification method

  1. Mechanical — tests/test_dispatch.py → tests/agent/test_dispatch.py listed R100 in the diff-tree output (byte-identical move); fork's diverged copy moves via git mv untouched.
  2. Hand-carry (R<100) — agent/reviewer_publish.py → agent/review/publish.py (R099): upstream's entire edit is the import block (from .reviewer_findings import → from .findings import, from .utils.github_http import → from ..utils.github_http import); no logic lines. Worst-scoring test tests/test_pr_ready_auto_review.py (R062): every hunk is monkeypatch.setattr(webapp, ...) → monkeypatch.setattr(webhook_common, ...) / webapp.process_github_pr_ready → github_webhooks.process_github_pr_ready. Worst-scoring fork-diverged UI file AgentThreadView.tsx (R092): grep of the rename diff shows 0 non-import changed lines.
  3. Consolidation — post-reorg agent/webapp.py is exactly from .api.app import app (4 lines, verified via git show 8356eb34:agent/webapp.py); agent/webhooks/github.py's reorg diff swaps from agent import webapp for from . import common and rewrites webapp.X → common.X with the docstring explicitly noting module-attribute access is preserved for monkeypatching.
  4. Test pairing — tests/conftest.py reorg diff retargets the autouse auto-review fixture from agent.webapp to agent.webhooks.common, confirming test fixtures follow the split, not the moves.

Working artifacts: docs/upstream-sync/domain-reorg/{movemap-m50.txt, fork-divergence-status.txt, cross.json, forkonly.json, merge-tree-now.txt, head-tree.txt, upstream-pre-tree.txt, upstream-main-tree.txt}.