This commit is contained in:
AndyMik90
2026-01-21 14:44:39 +01:00
parent 422f2117aa
commit 7b172e1b76
7 changed files with 907 additions and 472 deletions
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -117,7 +117,8 @@ class MethodologyLoader:
if not entry.enabled:
raise PluginLoadError(f"Methodology '{name}' is disabled")
plugin_path = Path(entry.path)
# Resolve the plugin path (handles both relative and absolute paths)
plugin_path = self._registry.resolve_plugin_path(entry)
if not plugin_path.exists():
raise PluginLoadError(
f"Plugin directory not found for methodology '{name}': {plugin_path}"
@@ -409,7 +410,8 @@ class MethodologyLoader:
if entry is None:
raise PluginLoadError(f"Methodology '{name}' is not registered")
plugin_path = Path(entry.path)
# Resolve the plugin path (handles both relative and absolute paths)
plugin_path = self._registry.resolve_plugin_path(entry)
return self._load_manifest(name, plugin_path)
def is_loaded(self, name: str) -> bool:
+45 -2
View File
@@ -205,9 +205,11 @@ class MethodologyRegistryImpl:
if not manifest_data:
continue
# Always store absolute paths to avoid CWD-dependent resolution
absolute_path = plugin_dir.resolve()
entry = RegistryEntry(
name=manifest_data.get("name", plugin_dir.name),
path=str(plugin_dir),
path=str(absolute_path),
version=manifest_data.get("version", "0.0.0"),
verified=True,
enabled=True,
@@ -246,9 +248,11 @@ class MethodologyRegistryImpl:
if not manifest_data:
continue
# Always store absolute paths to avoid CWD-dependent resolution
absolute_path = plugin_dir.resolve()
entry = RegistryEntry(
name=manifest_data.get("name", plugin_dir.name),
path=str(plugin_dir),
path=str(absolute_path),
version=manifest_data.get("version", "0.0.0"),
verified=False,
enabled=True,
@@ -345,6 +349,45 @@ class MethodologyRegistryImpl:
return entry
return None
def resolve_plugin_path(self, entry: RegistryEntry) -> Path:
"""Resolve a plugin path to an absolute path.
Handles both absolute and relative paths stored in the registry.
For verified plugins with relative paths, resolves against verified_plugins_dir.
Args:
entry: Registry entry containing the path
Returns:
Resolved absolute Path to the plugin directory
"""
plugin_path = Path(entry.path)
# If absolute and exists, use it directly
if plugin_path.is_absolute():
return plugin_path
# For relative paths, try to resolve against verified_plugins_dir
if entry.verified and self._verified_plugins_dir is not None:
# Check if path is relative to verified_plugins_dir
resolved = self._verified_plugins_dir / plugin_path.name
if resolved.exists():
return resolved
# Also try joining the full relative path in case it's structured
resolved = self._verified_plugins_dir.parent.parent.parent / plugin_path
if resolved.exists():
return resolved
# For community plugins, try community_plugins_dir
if not entry.verified and self._community_plugins_dir is not None:
resolved = self._community_plugins_dir / plugin_path.name
if resolved.exists():
return resolved
# Fallback: return as-is (will likely fail, but preserves original behavior)
return plugin_path
def get_methodology(self, name: str) -> MethodologyRunner:
"""Get a methodology runner by name.
+5 -4
View File
@@ -79,8 +79,10 @@ if sys.platform == "win32":
if "_new_stream" in dir():
del _new_stream
# Add auto-claude to path (parent of runners/)
# Add auto-claude to path (parent of runners/) for relative imports
sys.path.insert(0, str(Path(__file__).parent.parent))
# Add project root (parent of apps/) for absolute imports like 'from apps.backend...'
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
# Validate platform-specific dependencies BEFORE any imports that might
# trigger graphiti_core -> real_ladybug -> pywintypes import chain (ACS-253)
@@ -246,9 +248,8 @@ def _run_methodology_pipeline(
task_config=task_config,
)
# Initialize the runner
debug("spec_runner", "Initializing methodology runner")
runner.initialize(context)
# Note: Runner initialization is handled by FullAutoExecutor.execute()
# to avoid double-initialization issues
# Create and run the executor
debug("spec_runner", "Creating FullAutoExecutor")
+1 -1
View File
@@ -1 +1 @@
../../../../../../apps/frontend/node_modules
../../node_modules
@@ -56,11 +56,6 @@ export const ComplexitySelector = memo(function ComplexitySelector({
const { t } = useTranslation(['tasks']);
const prefix = idPrefix ? `${idPrefix}-` : '';
// Find the current option to get its icon and color
const currentOption = COMPLEXITY_OPTIONS.find(opt => opt.value === value);
const CurrentIcon = currentOption?.icon || Sparkles;
const currentColorClass = currentOption?.colorClass || 'text-info';
return (
<div className="space-y-2">
<Label
@@ -79,10 +74,7 @@ export const ComplexitySelector = memo(function ComplexitySelector({
aria-describedby={`${prefix}execution-complexity-help`}
className="h-10"
>
<div className="flex items-center gap-2">
<CurrentIcon className={cn('h-4 w-4', currentColorClass)} />
<SelectValue placeholder={t('tasks:executionComplexity.auto.label')} />
</div>
<SelectValue placeholder={t('tasks:executionComplexity.auto.label')} />
</SelectTrigger>
<SelectContent>
{COMPLEXITY_OPTIONS.map(({ value: optValue, icon: Icon, colorClass }) => (
+8 -4
View File
@@ -349,15 +349,19 @@ class TestBMADRunnerInitialization:
assert runner._initialized is True
assert runner._context is mock_context
def test_initialize_raises_if_already_initialized(self, mock_context: Any) -> None:
"""Test that initializing twice raises RuntimeError."""
def test_initialize_allows_reinitialization(self, mock_context: Any) -> None:
"""Test that initializing twice resets state for runner reuse."""
from apps.backend.methodologies.bmad import BMADRunner
runner = BMADRunner()
runner.initialize(mock_context)
with pytest.raises(RuntimeError, match="already initialized"):
runner.initialize(mock_context)
# Should not raise - runners support reuse/reinitialization
runner.initialize(mock_context)
# Runner should still be functional after reinitialization
assert runner._initialized is True
assert runner._context is mock_context
def test_get_phases_raises_before_initialization(self) -> None:
"""Test that get_phases() raises if not initialized."""