From c68dc1877f4ee00540b3e73619cea0aac0a6e77c Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Mon, 27 Apr 2026 03:19:17 -0700 Subject: [PATCH] fix(release): drift-gate TOP_LEVEL_MODULES + smoke-import main in publish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two compounding bugs surfaced when 0.1.16 hit production today: 1. scripts/build_runtime_package.py had a hand-curated TOP_LEVEL_MODULES set listing every workspace/*.py that should get its bare imports rewritten to `molecule_runtime.X`. The set silently went stale: - Missing: transcript_auth (added since #87 phase 1c), runtime_wedge, watcher → unrewritten imports shipped, every workspace startup died with ModuleNotFoundError. - Stale: claude_sdk_executor, cli_executor (both removed in #87), hermes_executor (never existed) → harmless but misleading. 2. publish-runtime.yml's wheel-smoke step asserted on stable invariants (BaseAdapter, AdapterConfig, a2a_client error sentinel) but never imported main. So even though main.py held the broken bare `from transcript_auth import ...`, the smoke check passed. Fixes: - Build script now derives the on-disk module set from workspace/*.py and asserts it matches TOP_LEVEL_MODULES exactly. Drift in either direction fails the build with a specific diff message instead of shipping a broken wheel. Closed-list typo guard preserved (we still edit the set explicitly when a module is added/removed) — the gate just makes drift impossible to ignore. - TOP_LEVEL_MODULES updated to current reality: drop the 3 stale, add the 3 missing. - publish-runtime.yml wheel-smoke now `import molecule_runtime.main` before the invariant asserts. main is the entry point and transitively imports every module — any bare-import bug surfaces as ModuleNotFoundError before PyPI accepts the upload. Tested locally: `python3 scripts/build_runtime_package.py --version 0.1.99 --out /tmp/build-test` succeeds, and /tmp/build-test/molecule_runtime/main.py contains the rewritten `from molecule_runtime.transcript_auth import ...`. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/publish-runtime.yml | 8 ++++++ scripts/build_runtime_package.py | 35 +++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish-runtime.yml b/.github/workflows/publish-runtime.yml index 80862e07..c22fbcc7 100644 --- a/.github/workflows/publish-runtime.yml +++ b/.github/workflows/publish-runtime.yml @@ -135,6 +135,14 @@ jobs: WORKSPACE_ID=00000000-0000-0000-0000-000000000000 \ PLATFORM_URL=http://localhost:8080 \ /tmp/smoke/bin/python -c " + # Importing main is the strongest smoke test we can do here: + # main.py is the entry point and pulls every other module + # transitively. If the build script missed an import rewrite + # (e.g. left a bare \`from transcript_auth import ...\` instead + # of \`from molecule_runtime.transcript_auth import ...\` — the + # 0.1.16 incident), this fails with ModuleNotFoundError instead + # of shipping to PyPI and breaking every workspace startup. + import molecule_runtime.main # noqa: F401 from molecule_runtime import a2a_client, a2a_tools from molecule_runtime.builtin_tools import memory from molecule_runtime.adapters import get_adapter, BaseAdapter, AdapterConfig diff --git a/scripts/build_runtime_package.py b/scripts/build_runtime_package.py index 91e121b2..66342bd7 100755 --- a/scripts/build_runtime_package.py +++ b/scripts/build_runtime_package.py @@ -42,8 +42,13 @@ from pathlib import Path # matches one of these) gets rewritten to use the package prefix. # # Closed list (not "every .py we copy") because a typo in workspace/ would -# otherwise leak into a wrong rewrite. Update this when adding a new -# top-level module to workspace/. +# otherwise leak into a wrong rewrite. The set is asserted against +# `workspace/*.py` at build time — if the disk contents drift from this +# list (new module added, old one removed), the build fails loud instead +# of silently shipping unrewritten imports. That gap caused 0.1.16 to +# ship `from transcript_auth import ...` (unrewritten — module added +# without updating this set), which broke every workspace startup with +# `ModuleNotFoundError: No module named 'transcript_auth'`. TOP_LEVEL_MODULES = { "a2a_cli", "a2a_client", @@ -53,15 +58,12 @@ TOP_LEVEL_MODULES = { "adapter_base", "agent", "agents_md", - "claude_sdk_executor", - "cli_executor", "config", "consolidation", "coordinator", "events", "executor_helpers", "heartbeat", - "hermes_executor", "initial_prompt", "main", "molecule_ai_status", @@ -69,7 +71,10 @@ TOP_LEVEL_MODULES = { "plugins", "preflight", "prompt", + "runtime_wedge", "shared_runtime", + "transcript_auth", + "watcher", } # Subdirectory packages — these are already real packages (they have or will @@ -250,6 +255,26 @@ def main() -> int: print(f"error: source not a directory: {src}", file=sys.stderr) return 2 + # Drift gate: assert TOP_LEVEL_MODULES matches workspace/*.py. + # Without this, a new top-level module added to workspace/ ships + # with unrewritten `from import` statements that explode at + # runtime with ModuleNotFoundError. (See 0.1.16 transcript_auth + # incident — closed list silently went stale.) + on_disk_modules = { + f.stem for f in src.glob("*.py") + if f.stem not in {"__init__", "conftest"} + } + missing = on_disk_modules - TOP_LEVEL_MODULES + stale = TOP_LEVEL_MODULES - on_disk_modules + if missing or stale: + print("error: TOP_LEVEL_MODULES drifted from workspace/*.py contents:", file=sys.stderr) + if missing: + print(f" in workspace/ but NOT in TOP_LEVEL_MODULES (will ship un-rewritten): {sorted(missing)}", file=sys.stderr) + if stale: + print(f" in TOP_LEVEL_MODULES but NOT in workspace/ (no-op, but misleading): {sorted(stale)}", file=sys.stderr) + print(" Edit scripts/build_runtime_package.py:TOP_LEVEL_MODULES to match.", file=sys.stderr) + return 3 + pkg_dir = out / "molecule_runtime" print(f"[build] source: {src}") print(f"[build] output: {out}")