release(v4.4.0): bump framework and eight skills to 4.4.0 in lockstep
Issue #5 (Python core migration, CAS state transitions, shadow watcher, auto-completion gate, eager-import immutable runtime, B-13 freeze removal) is complete and merged to main. This stamps the release. SemVer: MINOR, not MAJOR. The change is large but breaks nothing. Across the whole Issue #5 range there are zero file deletions and zero renames, the event wire schema stays at SCHEMA_VERSION 1, all four literal script paths survive as compatibility shims, the 968-line legacy shell loop is still in run_loop.sh, and every default change reverts through a single environment variable (MAM_DELEGATION_PY / MAM_ENGINE_CAS / MAM_LOOP_PY / MAM_LOOP_FREEZE). The riskiest new feature, auto-completion, ships default-off. No operator has anything to migrate, which is the decisive reason not to call it 5.0.0. The reasoning is recorded in the VERSIONS.md release notes, not just here. Close a gap in the lockstep guard. lib.sh exports MAM_VERSION and eager_load() refuses to start the loop when it disagrees with FRAMEWORK_VERSION, so eager.py is a load-bearing fourth leg — but test_version_consistency.py only checked three, and the sole check on eager.py was a hardcoded literal in the phase 4 test that a release bump has to edit anyway. The lockstep test now covers eager.py, a new test pins the fail-closed gate itself, and the phase 4 test no longer carries a version literal. Fix a latent test-isolation defect the bump exposed. The first full run failed four tests with "MAM_VERSION mismatch: expected 4.4.0, got 4.3.0 in environment" — 4.3.0 was not in the tree, it was inherited from the shell that launched pytest and had sourced the pre-bump lib.sh. Those tests were measuring the runner's shell rather than the tree under test, and only passed until now because the two values always happened to agree. A session-scoped fixture drops the ambient value. Production is unaffected: run_loop.sh:17 sources lib.sh before anything else and lib.sh re-exports MAM_VERSION unconditionally, so a stale inherited value is always overwritten before Python starts. Full suite: 750 passed in 912.50s, 0 failed, 0 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UrDNJkHmm18uu7n7JwV5Qd
This commit is contained in:
1 parent
b347efd760
commit
387479732e
21 files changed
+160
-40
No files matched your search
@@ -1065,6 +1065,31 @@ print(str(uuid.uuid4()))
|
||||
# lib_py, which is why test_tier1_unit.py's inserts stay legal.)
|
||||
# ==============================================================================
|
||||
|
||||
# ==============================================================================
|
||||
# Ambient MAM_VERSION isolation.
|
||||
#
|
||||
# lib.sh exports MAM_VERSION, so any shell that has sourced it — an agent
|
||||
# session, a developer's terminal — hands that value down to pytest and on to
|
||||
# every subprocess the suite spawns. lib_py.loop.eager.eager_load() compares it
|
||||
# against FRAMEWORK_VERSION and refuses to start on a mismatch, so an inherited
|
||||
# value from a *pre-upgrade* shell makes the loop tests fail against a tree that
|
||||
# is perfectly consistent with itself.
|
||||
#
|
||||
# The suite must measure the tree under test, not the version its launcher
|
||||
# happened to source, so the ambient value is dropped for the whole session.
|
||||
# Tests that exercise the gate set MAM_VERSION explicitly and are unaffected.
|
||||
# ==============================================================================
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def _drop_ambient_mam_version():
|
||||
previous = os.environ.pop("MAM_VERSION", None)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if previous is not None:
|
||||
os.environ["MAM_VERSION"] = previous
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def delegation_tree_provenance_guard(tmp_path_factory):
|
||||
yield
|
||||
|
||||
@@ -36,9 +36,11 @@ def test_p4_01_eager_import_completeness_and_bytecode_inhibition(monkeypatch):
|
||||
assert sys.dont_write_bytecode is True
|
||||
assert os.environ.get("PYTHONDONTWRITEBYTECODE") == "1"
|
||||
|
||||
# Verify framework version constant sync
|
||||
assert eager.FRAMEWORK_VERSION == "4.3.0"
|
||||
eager.verify_framework_version("4.3.0")
|
||||
# Verify framework version constant sync. The literal value is not pinned
|
||||
# here: tests/test_version_consistency.py owns the lockstep between
|
||||
# eager.FRAMEWORK_VERSION, lib.sh's MAM_VERSION, VERSIONS.md and the eight
|
||||
# SKILL.md files, so a release bump must not have to edit this file too.
|
||||
eager.verify_framework_version(eager.FRAMEWORK_VERSION)
|
||||
|
||||
# Verify mismatch raises RuntimeError
|
||||
monkeypatch.setenv("MAM_VERSION", "9.9.9")
|
||||
|
||||
@@ -11,6 +11,7 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
LIB_SH = REPO_ROOT / ".agents" / "skills" / "lib.sh"
|
||||
VERSIONS_MD = REPO_ROOT / "VERSIONS.md"
|
||||
SKILLS_DIR = REPO_ROOT / ".agents" / "skills"
|
||||
EAGER_PY = SKILLS_DIR / "lib_py" / "loop" / "eager.py"
|
||||
|
||||
SKILL_NAMES = [
|
||||
"multi-agent-mux-create",
|
||||
@@ -46,6 +47,15 @@ def _get_versions_md_versions():
|
||||
return current_framework_ver, matrix_dict
|
||||
|
||||
|
||||
def _get_eager_py_framework_version():
|
||||
"""Read FRAMEWORK_VERSION statically — importing lib_py.loop.eager would
|
||||
pull the whole framework onto sys.path just to read one constant."""
|
||||
text = EAGER_PY.read_text(encoding="utf-8")
|
||||
m = re.search(r'^FRAMEWORK_VERSION\s*=\s*"(\d+\.\d+\.\d+)"', text, re.M)
|
||||
assert m, f"Failed to parse FRAMEWORK_VERSION in {EAGER_PY}"
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def _get_skill_md_frontmatter_version(skill_name: str):
|
||||
skill_file = SKILLS_DIR / skill_name / "SKILL.md"
|
||||
assert skill_file.is_file(), f"Skill file not found: {skill_file}"
|
||||
@@ -78,6 +88,45 @@ def test_three_way_version_lockstep():
|
||||
fm_ver = _get_skill_md_frontmatter_version(skill)
|
||||
assert fm_ver == lib_ver, f"{skill}/SKILL.md version ({fm_ver}) != lib.sh ({lib_ver})"
|
||||
|
||||
# 4. Runtime constant in lib_py/loop/eager.py.
|
||||
eager_ver = _get_eager_py_framework_version()
|
||||
assert eager_ver == lib_ver, (
|
||||
f"eager.py FRAMEWORK_VERSION ({eager_ver}) != lib.sh MAM_VERSION ({lib_ver})"
|
||||
)
|
||||
|
||||
|
||||
def test_runtime_version_gate_rejects_a_partially_bumped_tree():
|
||||
"""A tree where only one of the two runtime constants was bumped must not run.
|
||||
|
||||
lib.sh exports MAM_VERSION into the environment and eager_load() compares it
|
||||
against FRAMEWORK_VERSION, so a half-updated deployment fails closed at loop
|
||||
startup rather than running two versions of the framework against each other.
|
||||
This guards the mechanism; the values themselves are pinned by the lockstep
|
||||
test above.
|
||||
"""
|
||||
import importlib.util
|
||||
|
||||
spec = importlib.util.spec_from_file_location("_eager_under_test", EAGER_PY)
|
||||
eager = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(eager)
|
||||
|
||||
assert eager.FRAMEWORK_VERSION == _get_lib_sh_mam_version()
|
||||
|
||||
env_before = os.environ.get("MAM_VERSION")
|
||||
os.environ["MAM_VERSION"] = "0.0.0-stale"
|
||||
try:
|
||||
try:
|
||||
eager.verify_framework_version()
|
||||
except RuntimeError as exc:
|
||||
assert "MAM_VERSION mismatch" in str(exc)
|
||||
else:
|
||||
raise AssertionError("a stale MAM_VERSION was accepted by the runtime gate")
|
||||
finally:
|
||||
if env_before is None:
|
||||
os.environ.pop("MAM_VERSION", None)
|
||||
else:
|
||||
os.environ["MAM_VERSION"] = env_before
|
||||
|
||||
|
||||
def test_mam_version_is_not_env_overridable():
|
||||
"""T-0 (Constraint 1): MAM_VERSION cannot be spoofed by caller environment."""
|
||||
|
||||
Reference in new issue
Block a user