From 45b038fe7ed17c2d11d502e3649437489f8f59f5 Mon Sep 17 00:00:00 2001 From: Manoj Prabhakar Paidiparthy Date: Tue, 9 Jun 2026 21:22:41 -0700 Subject: [PATCH 1/2] proposal: decouple hook module from parent bundle runtime dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hook module currently declares amplifier-bundle-context-intelligence as a runtime dependency in pyproject.toml, with [tool.uv.sources] mapping it to path = "../../../". This works for in-workspace installs but makes the module uninstallable in any environment that strips uv sources at install time, notably amplifier-agent's foundation activator (uv pip install --no-sources, documented at amplifier_foundation/modules/activator.py:471). This commit proposes decoupling the hook module from its parent bundle so it can be installed standalone: - Removes "amplifier-bundle-context-intelligence" from dependencies - Removes the [tool.uv.sources] path mapping - Inverts the three TestHookDependencies tests to assert the bundle is NOT a runtime dependency (making the new contract explicit) The hook's actual Python imports (__init__.py, config_resolver.py, handlers/logging_handler.py) do not reference amplifier_bundle_context_intelligence, so this change is mechanically safe. This is a design proposal opened for maintainer review. If the maintainer prefers to keep the hook bundle-coupled, please close — the consuming project (amplifier-agent) will adapt by deferring the integration until a standalone-installable version is available. Related diagnostic: https://github.com/microsoft-amplifier/amplifier-support/issues/269 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com> --- .../hook-context-intelligence/pyproject.toml | 2 - .../tests/test_hook_dependencies.py | 50 +++++++++++-------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/modules/hook-context-intelligence/pyproject.toml b/modules/hook-context-intelligence/pyproject.toml index 375e53be..970d78b1 100644 --- a/modules/hook-context-intelligence/pyproject.toml +++ b/modules/hook-context-intelligence/pyproject.toml @@ -8,7 +8,6 @@ license = "MIT" dependencies = [ "httpx>=0.28.1", "idna>=3.15", - "amplifier-bundle-context-intelligence", ] [project.entry-points."amplifier.modules"] @@ -36,7 +35,6 @@ dev = [ [tool.uv.sources] amplifier-core = { git = "https://github.com/microsoft/amplifier-core", rev = "v1.4.1" } -amplifier-bundle-context-intelligence = { path = "../.." } [tool.pytest.ini_options] asyncio_mode = "auto" diff --git a/modules/hook-context-intelligence/tests/test_hook_dependencies.py b/modules/hook-context-intelligence/tests/test_hook_dependencies.py index 3a945690..b7586e84 100644 --- a/modules/hook-context-intelligence/tests/test_hook_dependencies.py +++ b/modules/hook-context-intelligence/tests/test_hook_dependencies.py @@ -1,6 +1,9 @@ -"""Test that the hook module pyproject.toml declares the context_intelligence dependency. +"""Test that the hook module pyproject.toml does NOT depend on its parent bundle. -TDD: This test is written FIRST and will FAIL until pyproject.toml is updated. +Design: The hook module should be installable independently of its parent bundle +so it can be used in environments that compose modules individually. + +TDD: These tests are written to enforce decoupling from the parent bundle. """ from __future__ import annotations @@ -17,36 +20,41 @@ def _load_pyproject() -> dict: class TestHookDependencies: - """Verify hook module's pyproject.toml declares amplifier-bundle-context-intelligence.""" - - def test_amplifier_bundle_context_intelligence_in_dependencies(self) -> None: - """amplifier-bundle-context-intelligence must appear in the dependencies list.""" + """Verify hook module's pyproject.toml does NOT depend on its parent bundle. + + Per the design proposal: the hook module should be installable independently + of its parent bundle so it can be used in environments that compose modules + individually (e.g. amplifier-agent's --no-sources install policy). + """ + + def test_amplifier_bundle_context_intelligence_not_in_dependencies(self) -> None: + """amplifier-bundle-context-intelligence must NOT appear in dependencies.""" data = _load_pyproject() deps: list[str] = data["project"]["dependencies"] dep_names = [d.split(">=")[0].split("==")[0].strip() for d in deps] - assert "amplifier-bundle-context-intelligence" in dep_names, ( - f"Expected 'amplifier-bundle-context-intelligence' in dependencies, got: {deps}" + assert "amplifier-bundle-context-intelligence" not in dep_names, ( + "The hook module must not declare its parent bundle as a runtime " + "dependency. Doing so makes the module uninstallable in environments " + "that strip [tool.uv.sources] at install time." ) - def test_uv_sources_has_path_entry_for_bundle(self) -> None: - """[tool.uv.sources] must have path = '../..' for amplifier-bundle-context-intelligence.""" + def test_uv_sources_does_not_reference_bundle(self) -> None: + """[tool.uv.sources] must NOT have a path entry for the parent bundle.""" data = _load_pyproject() sources: dict = data.get("tool", {}).get("uv", {}).get("sources", {}) - assert "amplifier-bundle-context-intelligence" in sources, ( - f"Expected 'amplifier-bundle-context-intelligence' in [tool.uv.sources], got: {sources}" + assert "amplifier-bundle-context-intelligence" not in sources, ( + "The hook module must not pin its parent bundle via [tool.uv.sources]. " + "This entry is invisible under --no-sources install and produces an " + "unresolvable dependency." ) - entry = sources["amplifier-bundle-context-intelligence"] - assert entry.get("path") == "../..", f"Expected path = '../..', got: {entry}" - def test_dependencies_list_has_httpx_and_bundle(self) -> None: - """The production dependencies must include httpx and amplifier-bundle-context-intelligence. - amplifier-core is NOT a production dep — it is runtime-provided by the Amplifier CLI. - """ + def test_dependencies_list_has_httpx_only(self) -> None: + """Production dependencies must be httpx (and nothing else from this project).""" data = _load_pyproject() deps: list[str] = data["project"]["dependencies"] - assert any("httpx" in d for d in deps), f"httpx not found in {deps}" - assert any("amplifier-bundle-context-intelligence" in d for d in deps), ( - f"amplifier-bundle-context-intelligence not found in {deps}" + assert any("httpx" in d for d in deps), "httpx is required for HTTP dispatch" + assert not any("amplifier-bundle-context-intelligence" in d for d in deps), ( + "Parent bundle must not be a runtime dependency of the hook." ) assert not any("amplifier-core" in d for d in deps), ( f"amplifier-core must not be a production dep (runtime-provided): {deps}" From 3a94d0dd5b4050bcbbbb146c081130269d5e1016 Mon Sep 17 00:00:00 2001 From: Manoj Prabhakar Paidiparthy Date: Tue, 9 Jun 2026 22:01:37 -0700 Subject: [PATCH 2/2] fix(hook): vendor required context_intelligence symbols for standalone install The previous commit (45b038f) removed the runtime dependency on the parent bundle but left dangling imports in config_resolver.py: from context_intelligence.config import SETTINGS_PATH, _parse_settings_yaml from context_intelligence.reconstruct.discover import workspace_slug context_intelligence is a top-level Python package that ships inside amplifier-bundle-context-intelligence -- it is NOT vendored into the hook module's own package. So when only the hook is installed (e.g. amplifier-agent's --no-sources model), the imports fail at mount time: ModuleValidationError: No module named 'context_intelligence' This commit vendors the four symbols the hook actually uses into a new _vendored.py module: - AMPLIFIER_DIR, SETTINGS_PATH (constants, from config.py) - _parse_settings_yaml (function, from config.py) - workspace_slug (function, from reconstruct/discover.py) config_resolver.py now imports from amplifier_module_hook_context_intelligence._vendored instead of context_intelligence. Tests updated to import from the vendored location. Net effect: the hook installs AND mounts cleanly under --no-sources, with zero runtime dependency on the parent bundle. The bundle remains the authoritative source for context_intelligence/* code; the vendored copies in the hook are aligned by convention, documented in the file's docstring. End-to-end DTU verification of amplifier-agent integration pending after this lands on the PR branch. Refs: - https://github.com/microsoft/amplifier-bundle-context-intelligence/pull/35 - https://github.com/microsoft-amplifier/amplifier-support/issues/269 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com> --- .../_vendored.py | 119 ++++++++++++++++++ .../config_resolver.py | 5 +- .../tests/test_config_resolver.py | 2 +- 3 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 modules/hook-context-intelligence/amplifier_module_hook_context_intelligence/_vendored.py diff --git a/modules/hook-context-intelligence/amplifier_module_hook_context_intelligence/_vendored.py b/modules/hook-context-intelligence/amplifier_module_hook_context_intelligence/_vendored.py new file mode 100644 index 00000000..653bd79d --- /dev/null +++ b/modules/hook-context-intelligence/amplifier_module_hook_context_intelligence/_vendored.py @@ -0,0 +1,119 @@ +"""Vendored symbols from ``context_intelligence`` for standalone installability. + +The hook module's runtime needs four symbols from the parent bundle's +``context_intelligence`` package. Copying them here lets the hook install +and mount standalone — without requiring the parent bundle on PYTHONPATH +or in the install tree. + +Provenance (last synced from upstream): + - AMPLIFIER_DIR, SETTINGS_PATH, _parse_settings_yaml + → context_intelligence/config.py + - workspace_slug + → context_intelligence/reconstruct/discover.py + +If you change behaviour in either of those upstream files, mirror the change +here. The two copies are aligned by convention, not by shared import — the +hook MUST NOT import from ``context_intelligence`` at runtime, or it stops +being standalone-installable in environments that strip ``[tool.uv.sources]`` +(notably amplifier-agent's ``--no-sources`` activator policy). + +This module is internal to the hook package. External callers should not +import from here; if you need the same logic in your own code, copy it. +""" + +from __future__ import annotations + +import logging +from pathlib import Path + +log = logging.getLogger("amplifier_module_hook_context_intelligence._vendored") + +# --------------------------------------------------------------------------- +# From context_intelligence/config.py +# --------------------------------------------------------------------------- + +AMPLIFIER_DIR = Path.home() / ".amplifier" +SETTINGS_PATH = AMPLIFIER_DIR / "settings.yaml" + + +def _parse_settings_yaml(path: Path) -> dict: + """Minimal YAML parser for settings.yaml -- good enough for the flat keys + we need without requiring PyYAML. + + Returns a dict with CI server config keys (``server_url``, ``api_key``) if found. + """ + result: dict[str, str] = {} + if not path.is_file(): + return result + + try: + # Try PyYAML first + import yaml + + with open(path) as f: + data = yaml.safe_load(f) + if isinstance(data, dict): + ci_cfg = ( + data.get("overrides", {}).get("hook-context-intelligence", {}).get("config", {}) + ) + if isinstance(ci_cfg, dict): + if "context_intelligence_server_url" in ci_cfg: + result["server_url"] = ci_cfg["context_intelligence_server_url"] + if "context_intelligence_api_key" in ci_cfg: + result["api_key"] = ci_cfg["context_intelligence_api_key"] + except ImportError: + # Fallback: crude line-based extraction + try: + text = path.read_text() + in_ci_section = False + for line in text.splitlines(): + stripped = line.strip() + if "hook-context-intelligence" in stripped: + in_ci_section = True + continue + if in_ci_section: + if stripped.startswith("context_intelligence_server_url:"): + val = stripped.split(":", 1)[1].strip().strip("'\"") + result["server_url"] = val + elif stripped.startswith("context_intelligence_api_key:"): + val = stripped.split(":", 1)[1].strip().strip("'\"") + result["api_key"] = val + # If we hit a non-indented line, we've left the section + if not line.startswith(" ") and not line.startswith("\t") and stripped: + if "context_intelligence" not in stripped: + in_ci_section = False + except OSError: + pass + except Exception as exc: + log.debug("Could not parse %s: %s", path, exc) + return result + + +# --------------------------------------------------------------------------- +# From context_intelligence/reconstruct/discover.py +# --------------------------------------------------------------------------- + + +def workspace_slug(project_dir: str) -> str: + """Derive the workspace slug from an absolute project directory path. + + Converts the absolute path to a slug by replacing every ``/`` with ``-``. + + Examples:: + + workspace_slug("/home/bkrabach/dev/attractor-dev-machine") + # -> "-home-bkrabach-dev-attractor-dev-machine" + + Parameters + ---------- + project_dir: + Absolute path to the project directory. + + Returns + ------- + str + Slug derived from the absolute path. + """ + import os + + return os.path.abspath(project_dir).replace("/", "-") diff --git a/modules/hook-context-intelligence/amplifier_module_hook_context_intelligence/config_resolver.py b/modules/hook-context-intelligence/amplifier_module_hook_context_intelligence/config_resolver.py index aa2b6f2a..3ff76488 100644 --- a/modules/hook-context-intelligence/amplifier_module_hook_context_intelligence/config_resolver.py +++ b/modules/hook-context-intelligence/amplifier_module_hook_context_intelligence/config_resolver.py @@ -6,8 +6,9 @@ from pathlib import Path from typing import Any -from context_intelligence.config import SETTINGS_PATH, _parse_settings_yaml -from context_intelligence.reconstruct.discover import workspace_slug +from ._vendored import SETTINGS_PATH +from ._vendored import _parse_settings_yaml +from ._vendored import workspace_slug _DEFAULT_BASE_PATH = "~/.amplifier/projects" _DEFAULT_PROJECT_SLUG = "default" diff --git a/modules/hook-context-intelligence/tests/test_config_resolver.py b/modules/hook-context-intelligence/tests/test_config_resolver.py index ee9c47cf..90645fdd 100644 --- a/modules/hook-context-intelligence/tests/test_config_resolver.py +++ b/modules/hook-context-intelligence/tests/test_config_resolver.py @@ -750,7 +750,7 @@ def test_unix_path_produces_expected_slug(self) -> None: assert _slugify_path("/home/user/project") == "-home-user-project" def test_unix_path_matches_workspace_slug(self) -> None: - from context_intelligence.reconstruct.discover import workspace_slug + from amplifier_module_hook_context_intelligence._vendored import workspace_slug path = "/home/user/project" assert _slugify_path(path) == workspace_slug(path)