From 648cf3fc340b14b4666a731bca28a8ffcc569467 Mon Sep 17 00:00:00 2001 From: StillKnotKnown <192589389+StillKnotKnown@users.noreply.github.com> Date: Fri, 16 Jan 2026 19:30:17 +0200 Subject: [PATCH] fix(spec): resolve circular import between spec.pipeline and core.client (ACS-302) (#1192) * fix(spec): resolve circular import between spec.pipeline and core.client Implement lazy imports via __getattr__ to break the circular import chain where spec.pipeline.agent_runner imports core.client, which imports agents.tools_pkg, which imports from spec.validate_pkg. The import chain creates a cycle when spec/__init__.py imports SpecOrchestrator at module level. By deferring these imports via __getattr__, the import chain only executes when these symbols are actually accessed, breaking the cycle. This follows the established pattern used in core/__init__.py, agents/__init__.py, and integrations/graphiti/__init__.py. Refs: ACS-302 * refactor(spec): cache lazy imports in globals() for efficiency Update __getattr__ to cache imported objects in globals() after first access. This ensures subsequent accesses bypass __getattr__ entirely, avoiding redundant import overhead. Also add return type annotation (-> Any) for Python 3.12+ compatibility. Suggested-by: gemini-code-assist Refs: ACS-302 --------- Co-authored-by: StillKnotKnown --- apps/backend/spec/__init__.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/apps/backend/spec/__init__.py b/apps/backend/spec/__init__.py index 9b80c5dc..7100ca09 100644 --- a/apps/backend/spec/__init__.py +++ b/apps/backend/spec/__init__.py @@ -23,8 +23,17 @@ Usage: ) success = await orchestrator.run() + +Note: + SpecOrchestrator and get_specs_dir are lazy-imported to avoid circular + dependencies between spec.pipeline and core.client. The import chain: + spec.pipeline.agent_runner imports core.client, which imports + agents.tools_pkg, which imports from spec.validate_pkg, causing a cycle + when spec/__init__.py imports SpecOrchestrator at module level. """ +from typing import Any + from .complexity import ( Complexity, ComplexityAnalyzer, @@ -33,7 +42,6 @@ from .complexity import ( save_assessment, ) from .phases import PhaseExecutor, PhaseResult -from .pipeline import SpecOrchestrator, get_specs_dir __all__ = [ # Main orchestrator @@ -49,3 +57,25 @@ __all__ = [ "PhaseExecutor", "PhaseResult", ] + + +def __getattr__(name: str) -> Any: + """Lazy imports to avoid circular dependencies with core.client. + + The spec.pipeline module imports from core.client (via agent_runner.py), + which imports from agents.tools_pkg, which imports from spec.validate_pkg. + This creates a circular dependency when spec/__init__.py imports + SpecOrchestrator at module level. + + By deferring these imports via __getattr__, the import chain only + executes when these symbols are actually accessed, breaking the cycle. + + Imported objects are cached in globals() to avoid repeated imports. + """ + if name in ("SpecOrchestrator", "get_specs_dir"): + from .pipeline import SpecOrchestrator, get_specs_dir + + # Cache in globals so subsequent accesses bypass __getattr__ + globals().update(SpecOrchestrator=SpecOrchestrator, get_specs_dir=get_specs_dir) + return globals()[name] + raise AttributeError(f"module {__name__!r} has no attribute {name!r}")