diff --git a/.pylint_cache_run/src_1.stats b/.pylint_cache_run/src_1.stats index 9255e58..c211c30 100644 Binary files a/.pylint_cache_run/src_1.stats and b/.pylint_cache_run/src_1.stats differ diff --git a/src/autometabuilder/app_runner.py b/src/autometabuilder/app_runner.py index 45d4725..ca2abcc 100644 --- a/src/autometabuilder/app_runner.py +++ b/src/autometabuilder/app_runner.py @@ -21,6 +21,7 @@ from .workflow_engine_builder import build_workflow_engine def run_app() -> None: + """Run the AutoMetabuilder CLI.""" load_env() configure_logging() logger = logging.getLogger("autometabuilder") @@ -43,23 +44,21 @@ def run_app() -> None: metadata = load_metadata() tools = load_tools(metadata) - registry_entries = load_tool_registry() - tool_map = build_tool_map(gh, registry_entries) + tool_map = build_tool_map(gh, load_tool_registry()) load_plugins(tool_map, tools) - tool_policies = load_tool_policies() - workflow_config = load_workflow_config(metadata) - workflow_context = build_workflow_context( - args, - gh, - msgs, - client, - tools, - tool_map, - prompt, - tool_policies - ) + context_parts = { + "args": args, + "gh": gh, + "msgs": msgs, + "client": client, + "tools": tools, + "tool_map": tool_map, + "prompt": prompt, + "tool_policies": load_tool_policies() + } + workflow_context = build_workflow_context(context_parts) logger.debug("Workflow context ready with %s tools", len(tool_map)) - engine = build_workflow_engine(workflow_config, workflow_context, logger) + engine = build_workflow_engine(load_workflow_config(metadata), workflow_context, logger) engine.execute() diff --git a/src/autometabuilder/callable_loader.py b/src/autometabuilder/callable_loader.py index 683a960..757f244 100644 --- a/src/autometabuilder/callable_loader.py +++ b/src/autometabuilder/callable_loader.py @@ -3,6 +3,7 @@ import importlib def load_callable(path: str): + """Import and return a callable.""" module_path, attr = path.rsplit(".", 1) module = importlib.import_module(module_path) return getattr(module, attr) diff --git a/src/autometabuilder/cli_args.py b/src/autometabuilder/cli_args.py index ed8acce..88e0a42 100644 --- a/src/autometabuilder/cli_args.py +++ b/src/autometabuilder/cli_args.py @@ -3,9 +3,22 @@ import argparse def parse_args(): + """Parse CLI arguments.""" parser = argparse.ArgumentParser(description="AutoMetabuilder: AI-driven SDLC assistant.") - parser.add_argument("--dry-run", action="store_true", help="Do not execute state-modifying tools.") - parser.add_argument("--yolo", action="store_true", help="Execute tools without confirmation.") - parser.add_argument("--once", action="store_true", help="Run a single full iteration (AI -> Tool -> AI).") + parser.add_argument( + "--dry-run", + action="store_true", + help="Do not execute state-modifying tools." + ) + parser.add_argument( + "--yolo", + action="store_true", + help="Execute tools without confirmation." + ) + parser.add_argument( + "--once", + action="store_true", + help="Run a single full iteration (AI -> Tool -> AI)." + ) parser.add_argument("--web", action="store_true", help="Start the Web UI.") return parser.parse_args() diff --git a/src/autometabuilder/context_loader.py b/src/autometabuilder/context_loader.py index e5d3fa5..60a2404 100644 --- a/src/autometabuilder/context_loader.py +++ b/src/autometabuilder/context_loader.py @@ -7,6 +7,7 @@ logger = logging.getLogger("autometabuilder") def get_sdlc_context(gh: GitHubIntegration, msgs: dict) -> str: + """Return SDLC context text from roadmap, issues, and PRs.""" sdlc_context = "" if os.path.exists("ROADMAP.md"): with open("ROADMAP.md", "r", encoding="utf-8") as f: diff --git a/src/autometabuilder/env_loader.py b/src/autometabuilder/env_loader.py index a3cc36b..1427323 100644 --- a/src/autometabuilder/env_loader.py +++ b/src/autometabuilder/env_loader.py @@ -3,4 +3,5 @@ from dotenv import load_dotenv def load_env() -> None: + """Load environment variables.""" load_dotenv() diff --git a/src/autometabuilder/github_service.py b/src/autometabuilder/github_service.py index 5595a6e..e3e5687 100644 --- a/src/autometabuilder/github_service.py +++ b/src/autometabuilder/github_service.py @@ -6,6 +6,7 @@ logger = logging.getLogger("autometabuilder") def create_github_integration(token: str, msgs: dict): + """Create GitHub integration if possible.""" if not token: return None try: diff --git a/src/autometabuilder/metadata.json b/src/autometabuilder/metadata.json index a54a0b3..caf1c6b 100644 --- a/src/autometabuilder/metadata.json +++ b/src/autometabuilder/metadata.json @@ -40,10 +40,18 @@ "description": "meta.settings.log_level.description", "type": "select", "options": [ + "TRACE", "DEBUG", "INFO", "WARNING", "ERROR" + ], + "option_labels": [ + "meta.settings.log_level.options.trace", + "meta.settings.log_level.options.debug", + "meta.settings.log_level.options.info", + "meta.settings.log_level.options.warning", + "meta.settings.log_level.options.error" ] }, "APP_LANG": { @@ -56,6 +64,13 @@ "fr", "nl", "pirate" + ], + "option_labels": [ + "meta.settings.app_lang.options.en", + "meta.settings.app_lang.options.es", + "meta.settings.app_lang.options.fr", + "meta.settings.app_lang.options.nl", + "meta.settings.app_lang.options.pirate" ] }, "PROMPT_PATH": { @@ -99,6 +114,7 @@ "WEB_PASSWORD" ], "env_values": [ + "TRACE", "INFO", "DEBUG", "WARNING", diff --git a/src/autometabuilder/metadata_loader.py b/src/autometabuilder/metadata_loader.py index 7ad8056..4766f35 100644 --- a/src/autometabuilder/metadata_loader.py +++ b/src/autometabuilder/metadata_loader.py @@ -4,6 +4,7 @@ import os def load_metadata() -> dict: + """Load metadata.json.""" metadata_path = os.path.join(os.path.dirname(__file__), "metadata.json") with open(metadata_path, "r", encoding="utf-8") as f: return json.load(f) diff --git a/src/autometabuilder/model_resolver.py b/src/autometabuilder/model_resolver.py index 0d10cf6..6839bc1 100644 --- a/src/autometabuilder/model_resolver.py +++ b/src/autometabuilder/model_resolver.py @@ -5,4 +5,5 @@ DEFAULT_MODEL = "openai/gpt-4o" def resolve_model_name(prompt: dict) -> str: + """Resolve model name from env or prompt.""" return os.environ.get("LLM_MODEL", prompt.get("model", DEFAULT_MODEL)) diff --git a/src/autometabuilder/openai_client.py b/src/autometabuilder/openai_client.py index 3a6037f..4a6c3ed 100644 --- a/src/autometabuilder/openai_client.py +++ b/src/autometabuilder/openai_client.py @@ -4,6 +4,7 @@ from tenacity import retry, stop_after_attempt, wait_exponential @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10)) def get_completion(client, model, messages, tools): + """Request a chat completion with retries.""" return client.chat.completions.create( model=model, messages=messages, diff --git a/src/autometabuilder/openai_factory.py b/src/autometabuilder/openai_factory.py index c0c9e22..22160ed 100644 --- a/src/autometabuilder/openai_factory.py +++ b/src/autometabuilder/openai_factory.py @@ -6,6 +6,7 @@ DEFAULT_ENDPOINT = "https://models.github.ai/inference" def create_openai_client(token: str): + """Create an OpenAI client.""" return OpenAI( base_url=os.environ.get("GITHUB_MODELS_ENDPOINT", DEFAULT_ENDPOINT), api_key=token, diff --git a/src/autometabuilder/plugin_loader.py b/src/autometabuilder/plugin_loader.py index 40cb95a..1b924d6 100644 --- a/src/autometabuilder/plugin_loader.py +++ b/src/autometabuilder/plugin_loader.py @@ -8,6 +8,7 @@ logger = logging.getLogger("autometabuilder") def load_plugins(tool_map: dict, tools: list) -> None: + """Load plugin tools and append metadata.""" plugins_dir = os.path.join(os.path.dirname(__file__), "plugins") if not os.path.exists(plugins_dir): return diff --git a/src/autometabuilder/tool_map_builder.py b/src/autometabuilder/tool_map_builder.py index 2418862..4247fb5 100644 --- a/src/autometabuilder/tool_map_builder.py +++ b/src/autometabuilder/tool_map_builder.py @@ -3,6 +3,7 @@ from .callable_loader import load_callable def build_tool_map(gh, registry_entries: list) -> dict: + """Build tool name to callable map.""" tool_map = {} for entry in registry_entries: name = entry.get("name") diff --git a/src/autometabuilder/tool_policy_loader.py b/src/autometabuilder/tool_policy_loader.py index 6963167..0204348 100644 --- a/src/autometabuilder/tool_policy_loader.py +++ b/src/autometabuilder/tool_policy_loader.py @@ -4,6 +4,7 @@ import os def load_tool_policies() -> dict: + """Load tool policies JSON.""" path = os.path.join(os.path.dirname(__file__), "tool_policies.json") if not os.path.exists(path): return {"modifying_tools": []} diff --git a/src/autometabuilder/tool_registry_loader.py b/src/autometabuilder/tool_registry_loader.py index cd1a356..f9481ea 100644 --- a/src/autometabuilder/tool_registry_loader.py +++ b/src/autometabuilder/tool_registry_loader.py @@ -4,6 +4,7 @@ import os def load_tool_registry() -> list: + """Load tool registry entries.""" path = os.path.join(os.path.dirname(__file__), "tool_registry.json") if not os.path.exists(path): return [] diff --git a/src/autometabuilder/tools/edit_file.py b/src/autometabuilder/tools/edit_file.py index be26088..16bd5dd 100644 --- a/src/autometabuilder/tools/edit_file.py +++ b/src/autometabuilder/tools/edit_file.py @@ -1,6 +1,7 @@ """Edit file content with search/replace.""" def edit_file(path: str, search: str, replace: str) -> str: + """Replace text in a file.""" try: with open(path, "r", encoding="utf-8") as f: content = f.read() diff --git a/src/autometabuilder/tools/list_files.py b/src/autometabuilder/tools/list_files.py index 80b4a59..84caaea 100644 --- a/src/autometabuilder/tools/list_files.py +++ b/src/autometabuilder/tools/list_files.py @@ -6,6 +6,7 @@ logger = logging.getLogger("autometabuilder") def list_files(directory: str = ".") -> str: + """Return newline-separated files under a directory.""" files_list = [] for root, _, files in os.walk(directory): if ".git" in root or "__pycache__" in root or ".venv" in root: diff --git a/src/autometabuilder/tools/read_file.py b/src/autometabuilder/tools/read_file.py index ae6f117..145d58e 100644 --- a/src/autometabuilder/tools/read_file.py +++ b/src/autometabuilder/tools/read_file.py @@ -1,6 +1,7 @@ """Read file content.""" def read_file(path: str) -> str: + """Read file content.""" try: with open(path, "r", encoding="utf-8") as f: return f.read() diff --git a/src/autometabuilder/tools/run_docker_task.py b/src/autometabuilder/tools/run_docker_task.py index 8123b28..82092bc 100644 --- a/src/autometabuilder/tools/run_docker_task.py +++ b/src/autometabuilder/tools/run_docker_task.py @@ -4,5 +4,6 @@ from ..docker_utils import run_command_in_docker def run_docker_task(image: str, command: str, workdir: str = "/workspace") -> str: + """Run a command inside Docker.""" volumes = {os.getcwd(): "/workspace"} return run_command_in_docker(image, command, volumes=volumes, workdir=workdir) diff --git a/src/autometabuilder/tools/run_lint.py b/src/autometabuilder/tools/run_lint.py index 7987a9b..f9885c2 100644 --- a/src/autometabuilder/tools/run_lint.py +++ b/src/autometabuilder/tools/run_lint.py @@ -6,6 +6,7 @@ logger = logging.getLogger("autometabuilder") def run_lint(path: str = "src") -> str: + """Run pylint on a path.""" logger.info("Running linting in %s...", path) result = subprocess.run(["pylint", path], capture_output=True, text=True, check=False) logger.info(result.stdout) diff --git a/src/autometabuilder/tools/run_tests.py b/src/autometabuilder/tools/run_tests.py index 8788fd8..b56631c 100644 --- a/src/autometabuilder/tools/run_tests.py +++ b/src/autometabuilder/tools/run_tests.py @@ -6,6 +6,7 @@ logger = logging.getLogger("autometabuilder") def run_tests(path: str = "tests") -> str: + """Run pytest on a path.""" logger.info("Running tests in %s...", path) result = subprocess.run(["pytest", path], capture_output=True, text=True, check=False) logger.info(result.stdout) diff --git a/src/autometabuilder/tools/write_file.py b/src/autometabuilder/tools/write_file.py index 780d40f..8f0bc94 100644 --- a/src/autometabuilder/tools/write_file.py +++ b/src/autometabuilder/tools/write_file.py @@ -1,6 +1,7 @@ """Write file content.""" def write_file(path: str, content: str) -> str: + """Write content to a file.""" try: with open(path, "w", encoding="utf-8") as f: f.write(content) diff --git a/src/autometabuilder/tools_loader.py b/src/autometabuilder/tools_loader.py index 6ccee3a..333fc61 100644 --- a/src/autometabuilder/tools_loader.py +++ b/src/autometabuilder/tools_loader.py @@ -4,6 +4,7 @@ import os def load_tools(metadata: dict) -> list: + """Load tool specs from metadata reference.""" tools_path = os.path.join(os.path.dirname(__file__), metadata.get("tools_path", "tools.json")) with open(tools_path, "r", encoding="utf-8") as f: return json.load(f) diff --git a/src/autometabuilder/web/static/js/plugins/workflow_palette.js b/src/autometabuilder/web/static/js/plugins/workflow_palette.js new file mode 100644 index 0000000..fbf8bf6 --- /dev/null +++ b/src/autometabuilder/web/static/js/plugins/workflow_palette.js @@ -0,0 +1,10 @@ +/** + * AutoMetabuilder - Workflow Palette Plugin + */ +(() => { + const init = async () => { + window.AMBWorkflowPalette?.init?.(); + }; + + window.AMBPlugins?.register('workflow_palette', init); +})(); diff --git a/src/autometabuilder/web/static/js/workflow/workflow_mutations.js b/src/autometabuilder/web/static/js/workflow/workflow_mutations.js index bc5ff7f..c129768 100644 --- a/src/autometabuilder/web/static/js/workflow/workflow_mutations.js +++ b/src/autometabuilder/web/static/js/workflow/workflow_mutations.js @@ -36,14 +36,15 @@ return candidate; }; - const addNode = (targetArray, pluginDefinitions) => { + const addNode = (targetArray, pluginDefinitions, typeOverride = '') => { const types = Object.keys(pluginDefinitions); const defaultType = types[0] || ''; + const nextType = pluginDefinitions[typeOverride] ? typeOverride : defaultType; const node = { - id: generateNodeId(defaultType, targetArray), - type: defaultType, - inputs: buildDefaultFields(pluginDefinitions[defaultType]?.inputs), - outputs: buildDefaultFields(pluginDefinitions[defaultType]?.outputs) + id: generateNodeId(nextType, targetArray), + type: nextType, + inputs: buildDefaultFields(pluginDefinitions[nextType]?.inputs), + outputs: buildDefaultFields(pluginDefinitions[nextType]?.outputs) }; targetArray.push(node); }; diff --git a/src/autometabuilder/web/static/js/workflow/workflow_palette.js b/src/autometabuilder/web/static/js/workflow/workflow_palette.js new file mode 100644 index 0000000..2f16207 --- /dev/null +++ b/src/autometabuilder/web/static/js/workflow/workflow_palette.js @@ -0,0 +1,99 @@ +/** + * AutoMetabuilder - Workflow Palette + */ +(() => { + const state = { + container: null, + searchInput: null, + list: null, + filter: '' + }; + + const groupOrder = ['core', 'tools', 'utils', 'control', 'other']; + const groupLabelKeys = { + core: 'ui.workflow.palette.group.core', + tools: 'ui.workflow.palette.group.tools', + utils: 'ui.workflow.palette.group.utils', + control: 'ui.workflow.palette.group.control', + other: 'ui.workflow.palette.group.other' + }; + + const getGroupKey = (type) => (type || '').split('.')[0] || 'other'; + + const buildEntries = (definitions, t) => Object.entries(definitions || {}).map(([key, def]) => { + const label = t?.(def.label || '', def.label || key) || key; + return { + key, + label, + group: getGroupKey(key), + search: `${key} ${label}`.toLowerCase() + }; + }); + + const render = () => { + const { t, escapeHtml } = window.AMBWorkflowUtils || {}; + const definitions = window.AMBWorkflowState?.state?.pluginDefinitions || {}; + if (!state.list) return; + const term = state.filter.toLowerCase(); + const entries = buildEntries(definitions, t).filter(entry => !term || entry.search.includes(term)); + if (!entries.length) { + state.list.innerHTML = `

${escapeHtml ? escapeHtml(t?.('ui.workflow.palette.empty', 'No matching nodes.')) : t?.('ui.workflow.palette.empty', 'No matching nodes.')}

`; + return; + } + + const grouped = entries.reduce((acc, entry) => { + const group = groupOrder.includes(entry.group) ? entry.group : 'other'; + acc[group] = acc[group] || []; + acc[group].push(entry); + return acc; + }, {}); + + const groupsHtml = groupOrder.filter(group => grouped[group]?.length).map(group => { + const groupLabel = t?.(groupLabelKeys[group], group) || group; + const items = grouped[group].map(entry => { + const safeLabel = escapeHtml ? escapeHtml(entry.label) : entry.label; + const safeKey = escapeHtml ? escapeHtml(entry.key) : entry.key; + return ` + + `; + }).join(''); + return ` +
+
${escapeHtml ? escapeHtml(groupLabel) : groupLabel}
+
${items}
+
+ `; + }).join(''); + + state.list.innerHTML = groupsHtml; + }; + + const init = () => { + state.container = document.getElementById('workflow-palette'); + state.searchInput = document.getElementById('workflow-palette-search'); + state.list = document.getElementById('workflow-palette-list'); + if (!state.container || !state.searchInput || !state.list) return; + + state.searchInput.addEventListener('input', (event) => { + state.filter = event.target.value || ''; + render(); + }); + + state.list.addEventListener('click', (event) => { + const target = event.target.closest('.amb-workflow-palette-item'); + if (!target) return; + const nodeType = target.dataset.nodeType; + const workflowState = window.AMBWorkflowState?.state; + if (!workflowState || !nodeType) return; + window.AMBWorkflowMutations?.addNode(workflowState.workflow.nodes, workflowState.pluginDefinitions, nodeType); + window.AMBWorkflowCanvasRenderer?.render?.(); + }); + + render(); + }; + + window.AMBWorkflowPalette = { init, render }; +})(); diff --git a/src/autometabuilder/web/templates/base.html b/src/autometabuilder/web/templates/base.html index 8d81c90..4eac1b3 100644 --- a/src/autometabuilder/web/templates/base.html +++ b/src/autometabuilder/web/templates/base.html @@ -15,7 +15,10 @@ - + {% set core_styles = ui_assets.get('core_styles', ['/static/css/main.css']) if ui_assets else ['/static/css/main.css'] %} + {% for style in core_styles %} + + {% endfor %} {% block head %}{% endblock %} diff --git a/src/autometabuilder/web/templates/components/organisms/settings_configuration.html b/src/autometabuilder/web/templates/components/organisms/settings_configuration.html index e0df7f1..e1db53f 100644 --- a/src/autometabuilder/web/templates/components/organisms/settings_configuration.html +++ b/src/autometabuilder/web/templates/components/organisms/settings_configuration.html @@ -11,6 +11,7 @@ {% set desc_label = desc.get('label', key) %} {% set desc_text = desc.get('description', '') %} {% set desc_placeholder = desc.get('placeholder', '') %} + {% set option_labels = desc.get('option_labels', []) %}
{% if desc_text %} @@ -20,7 +21,8 @@ {% else %} diff --git a/src/autometabuilder/web/templates/components/organisms/workflow_palette.html b/src/autometabuilder/web/templates/components/organisms/workflow_palette.html new file mode 100644 index 0000000..4bf5572 --- /dev/null +++ b/src/autometabuilder/web/templates/components/organisms/workflow_palette.html @@ -0,0 +1,16 @@ +{# Workflow palette card macro. #} +{% macro workflow_palette() -%} +
+
+
{{ t('ui.workflow.palette.title', 'Node Palette') }}
+
+
+ + +
+

{{ t('ui.workflow.palette.loading', 'Loading nodes...') }}

+
+
+
+{%- endmacro %} diff --git a/src/autometabuilder/web/templates/components/sections/workflow_section.html b/src/autometabuilder/web/templates/components/sections/workflow_section.html index b47014c..5bb0a8f 100644 --- a/src/autometabuilder/web/templates/components/sections/workflow_section.html +++ b/src/autometabuilder/web/templates/components/sections/workflow_section.html @@ -1,6 +1,7 @@ {# Workflow section macro. #} {% from "components/atoms/section_header.html" import section_header %} {% from "components/organisms/workflow_templates.html" import workflow_templates with context %} +{% from "components/organisms/workflow_palette.html" import workflow_palette with context %} {% macro workflow_section() -%}
@@ -8,29 +9,36 @@ {{ workflow_templates() }} -
-
-
{{ t('ui.workflow.card.title', 'Tasks & Steps') }}
-
- -
+
+
+ {{ workflow_palette() }}
-
-
- -
-
- -
- +
+
+
+
{{ t('ui.workflow.card.title', 'Tasks & Steps') }}
+
+ +
- +
+
+ +
+
+ +
+ +
+
+
+
diff --git a/src/autometabuilder/web/ui_assets.json b/src/autometabuilder/web/ui_assets.json index 73541e1..fd7869f 100644 --- a/src/autometabuilder/web/ui_assets.json +++ b/src/autometabuilder/web/ui_assets.json @@ -1,4 +1,8 @@ { + "core_styles": [ + "/static/css/main.css", + "/static/css/workflow_palette.css" + ], "core_scripts": [ "/static/js/app_context.js", "/static/js/plugin_registry.js", @@ -14,6 +18,7 @@ "/static/js/workflow/workflow_utils.js", "/static/js/workflow/workflow_state.js", "/static/js/workflow/workflow_mutations.js", + "/static/js/workflow/workflow_palette.js", "/static/js/workflow/workflow_plugin_options.js", "/static/js/workflow/workflow_field_renderer.js", "/static/js/workflow/workflow_node_template.js", @@ -26,6 +31,7 @@ "page_scripts": [ "/static/js/plugins/navigation_loader.js", "/static/js/plugins/workflow_builder.js", + "/static/js/plugins/workflow_palette.js", "/static/js/plugins/workflow_templates.js", "/static/js/plugins/run_mode_toggle.js", "/static/js/plugins/prompt_builder.js", diff --git a/src/autometabuilder/workflow/engine.py b/src/autometabuilder/workflow/engine.py index f8ab479..7fb0a20 100644 --- a/src/autometabuilder/workflow/engine.py +++ b/src/autometabuilder/workflow/engine.py @@ -2,12 +2,14 @@ class WorkflowEngine: + """Run workflow configs through a node executor.""" def __init__(self, workflow_config, node_executor, logger): self.workflow_config = workflow_config or {} self.node_executor = node_executor self.logger = logger def execute(self): + """Execute the workflow config.""" nodes = self.workflow_config.get("nodes") if not isinstance(nodes, list): self.logger.error("Workflow config missing nodes list.") diff --git a/src/autometabuilder/workflow/input_resolver.py b/src/autometabuilder/workflow/input_resolver.py index c1cdfd8..c2b8c31 100644 --- a/src/autometabuilder/workflow/input_resolver.py +++ b/src/autometabuilder/workflow/input_resolver.py @@ -3,16 +3,20 @@ from .value_helpers import ValueHelpers class InputResolver: + """Resolve bindings in workflow inputs.""" def __init__(self, store: dict): self.store = store def resolve_inputs(self, inputs: dict) -> dict: + """Resolve bindings for every input.""" return {key: self.resolve_binding(value) for key, value in (inputs or {}).items()} def resolve_binding(self, value): + """Resolve a single binding value.""" if isinstance(value, str) and value.startswith("$"): return self.store.get(value[1:]) return value def coerce_bool(self, value) -> bool: + """Coerce values into booleans.""" return ValueHelpers.coerce_bool(value) diff --git a/src/autometabuilder/workflow/loop_executor.py b/src/autometabuilder/workflow/loop_executor.py index 341ca3a..594a186 100644 --- a/src/autometabuilder/workflow/loop_executor.py +++ b/src/autometabuilder/workflow/loop_executor.py @@ -2,15 +2,18 @@ class LoopExecutor: + """Execute loop nodes.""" def __init__(self, runtime, input_resolver): self.runtime = runtime self.input_resolver = input_resolver self.node_executor = None def set_node_executor(self, node_executor) -> None: + """Inject node executor dependency.""" self.node_executor = node_executor def execute(self, node): + """Run loop body until stop condition.""" inputs = node.get("inputs", {}) max_iterations = self.input_resolver.resolve_binding(inputs.get("max_iterations", 1)) stop_when_raw = inputs.get("stop_when") diff --git a/src/autometabuilder/workflow/node_executor.py b/src/autometabuilder/workflow/node_executor.py index 8ace4db..b2d2ba0 100644 --- a/src/autometabuilder/workflow/node_executor.py +++ b/src/autometabuilder/workflow/node_executor.py @@ -2,6 +2,7 @@ class NodeExecutor: + """Execute workflow nodes with plugins.""" def __init__(self, runtime, plugin_registry, input_resolver, loop_executor): self.runtime = runtime self.plugin_registry = plugin_registry @@ -9,10 +10,12 @@ class NodeExecutor: self.loop_executor = loop_executor def execute_nodes(self, nodes): + """Execute a list of nodes.""" for node in nodes: self.execute_node(node) def execute_node(self, node): + """Execute a single node.""" node_type = node.get("type") if not node_type: self.runtime.logger.error("Workflow node missing type.") diff --git a/src/autometabuilder/workflow/plugin_loader.py b/src/autometabuilder/workflow/plugin_loader.py index 2afe57b..8d3ce52 100644 --- a/src/autometabuilder/workflow/plugin_loader.py +++ b/src/autometabuilder/workflow/plugin_loader.py @@ -3,4 +3,5 @@ from ..callable_loader import load_callable def load_plugin_callable(path: str): + """Load a workflow plugin callable.""" return load_callable(path) diff --git a/src/autometabuilder/workflow/plugin_registry.py b/src/autometabuilder/workflow/plugin_registry.py index 4f6d1b3..2eedc37 100644 --- a/src/autometabuilder/workflow/plugin_registry.py +++ b/src/autometabuilder/workflow/plugin_registry.py @@ -8,6 +8,7 @@ logger = logging.getLogger("autometabuilder") def load_plugin_map() -> dict: + """Load workflow plugin map JSON.""" map_path = os.path.join(os.path.dirname(__file__), "plugin_map.json") if not os.path.exists(map_path): return {} @@ -21,6 +22,7 @@ def load_plugin_map() -> dict: class PluginRegistry: + """Resolve workflow plugin handlers.""" def __init__(self, plugin_map: dict): self._plugins = {} for node_type, path in plugin_map.items(): @@ -31,4 +33,5 @@ class PluginRegistry: logger.error("Failed to register plugin %s: %s", node_type, error) def get(self, node_type: str): + """Return plugin handler for node type.""" return self._plugins.get(node_type) diff --git a/src/autometabuilder/workflow/plugins/core_ai_request.py b/src/autometabuilder/workflow/plugins/core_ai_request.py index 5f33a21..a627316 100644 --- a/src/autometabuilder/workflow/plugins/core_ai_request.py +++ b/src/autometabuilder/workflow/plugins/core_ai_request.py @@ -3,6 +3,7 @@ from ...openai_client import get_completion def run(runtime, inputs): + """Invoke the model with current messages.""" messages = list(inputs.get("messages") or []) response = get_completion( runtime.context["client"], diff --git a/src/autometabuilder/workflow/plugins/core_append_context_message.py b/src/autometabuilder/workflow/plugins/core_append_context_message.py index 119df22..cf5314d 100644 --- a/src/autometabuilder/workflow/plugins/core_append_context_message.py +++ b/src/autometabuilder/workflow/plugins/core_append_context_message.py @@ -2,6 +2,7 @@ def run(runtime, inputs): + """Append context to the message list.""" messages = list(inputs.get("messages") or []) context_val = inputs.get("context") if context_val: diff --git a/src/autometabuilder/workflow/plugins/core_append_tool_results.py b/src/autometabuilder/workflow/plugins/core_append_tool_results.py index 75eac83..4e8f3fd 100644 --- a/src/autometabuilder/workflow/plugins/core_append_tool_results.py +++ b/src/autometabuilder/workflow/plugins/core_append_tool_results.py @@ -4,6 +4,7 @@ from ...roadmap_utils import is_mvp_reached def run(runtime, inputs): + """Append tool results to the message list.""" messages = list(inputs.get("messages") or []) tool_results = inputs.get("tool_results") or [] if tool_results: diff --git a/src/autometabuilder/workflow/plugins/core_append_user_instruction.py b/src/autometabuilder/workflow/plugins/core_append_user_instruction.py index 0b25b1c..556249c 100644 --- a/src/autometabuilder/workflow/plugins/core_append_user_instruction.py +++ b/src/autometabuilder/workflow/plugins/core_append_user_instruction.py @@ -2,6 +2,7 @@ def run(runtime, inputs): + """Append the next user instruction.""" messages = list(inputs.get("messages") or []) messages.append({"role": "user", "content": runtime.context["msgs"]["user_next_step"]}) return {"messages": messages} diff --git a/src/autometabuilder/workflow/plugins/core_load_context.py b/src/autometabuilder/workflow/plugins/core_load_context.py index c0d85f0..5610671 100644 --- a/src/autometabuilder/workflow/plugins/core_load_context.py +++ b/src/autometabuilder/workflow/plugins/core_load_context.py @@ -2,5 +2,6 @@ from ...context_loader import get_sdlc_context -def run(runtime, inputs): +def run(runtime, _inputs): + """Load SDLC context into the workflow store.""" return {"context": get_sdlc_context(runtime.context["gh"], runtime.context["msgs"])} diff --git a/src/autometabuilder/workflow/plugins/core_run_tool_calls.py b/src/autometabuilder/workflow/plugins/core_run_tool_calls.py index 8ea56d1..d95e821 100644 --- a/src/autometabuilder/workflow/plugins/core_run_tool_calls.py +++ b/src/autometabuilder/workflow/plugins/core_run_tool_calls.py @@ -4,6 +4,7 @@ from ..tool_calls_handler import handle_tool_calls def run(runtime, inputs): + """Execute tool calls from an AI response.""" resp_msg = inputs.get("response") tool_calls = getattr(resp_msg, "tool_calls", None) or [] if not resp_msg: diff --git a/src/autometabuilder/workflow/plugins/core_seed_messages.py b/src/autometabuilder/workflow/plugins/core_seed_messages.py index f7971dd..405010e 100644 --- a/src/autometabuilder/workflow/plugins/core_seed_messages.py +++ b/src/autometabuilder/workflow/plugins/core_seed_messages.py @@ -1,6 +1,7 @@ """Workflow plugin: seed messages.""" -def run(runtime, inputs): +def run(runtime, _inputs): + """Seed messages from the prompt.""" prompt = runtime.context["prompt"] return {"messages": list(prompt["messages"])} diff --git a/src/autometabuilder/workflow/plugins/tools_create_branch.py b/src/autometabuilder/workflow/plugins/tools_create_branch.py index 53b6fe4..e11142d 100644 --- a/src/autometabuilder/workflow/plugins/tools_create_branch.py +++ b/src/autometabuilder/workflow/plugins/tools_create_branch.py @@ -2,6 +2,7 @@ def run(runtime, inputs): + """Create a branch via tool runner.""" result = runtime.tool_runner.call( "create_branch", branch_name=inputs.get("branch_name"), diff --git a/src/autometabuilder/workflow/plugins/tools_create_pull_request.py b/src/autometabuilder/workflow/plugins/tools_create_pull_request.py index 4ed1257..1128f6f 100644 --- a/src/autometabuilder/workflow/plugins/tools_create_pull_request.py +++ b/src/autometabuilder/workflow/plugins/tools_create_pull_request.py @@ -2,6 +2,7 @@ def run(runtime, inputs): + """Create a pull request via tool runner.""" result = runtime.tool_runner.call( "create_pull_request", title=inputs.get("title"), diff --git a/src/autometabuilder/workflow/plugins/tools_list_files.py b/src/autometabuilder/workflow/plugins/tools_list_files.py index 395df13..90a8fac 100644 --- a/src/autometabuilder/workflow/plugins/tools_list_files.py +++ b/src/autometabuilder/workflow/plugins/tools_list_files.py @@ -2,5 +2,6 @@ def run(runtime, inputs): + """List files via tool runner.""" result = runtime.tool_runner.call("list_files", directory=inputs.get("path", ".")) return {"files": result} diff --git a/src/autometabuilder/workflow/plugins/tools_read_file.py b/src/autometabuilder/workflow/plugins/tools_read_file.py index dd4e8f2..b419623 100644 --- a/src/autometabuilder/workflow/plugins/tools_read_file.py +++ b/src/autometabuilder/workflow/plugins/tools_read_file.py @@ -2,5 +2,6 @@ def run(runtime, inputs): + """Read a file via tool runner.""" result = runtime.tool_runner.call("read_file", path=inputs.get("path")) return {"content": result} diff --git a/src/autometabuilder/workflow/plugins/tools_run_lint.py b/src/autometabuilder/workflow/plugins/tools_run_lint.py index 8b31618..085c363 100644 --- a/src/autometabuilder/workflow/plugins/tools_run_lint.py +++ b/src/autometabuilder/workflow/plugins/tools_run_lint.py @@ -2,5 +2,6 @@ def run(runtime, inputs): + """Run lint via tool runner.""" result = runtime.tool_runner.call("run_lint", path=inputs.get("path", "src")) return {"results": result} diff --git a/src/autometabuilder/workflow/plugins/tools_run_tests.py b/src/autometabuilder/workflow/plugins/tools_run_tests.py index 3d9a11b..2061f3a 100644 --- a/src/autometabuilder/workflow/plugins/tools_run_tests.py +++ b/src/autometabuilder/workflow/plugins/tools_run_tests.py @@ -2,5 +2,6 @@ def run(runtime, inputs): + """Run tests via tool runner.""" result = runtime.tool_runner.call("run_tests", path=inputs.get("path", "tests")) return {"results": result} diff --git a/src/autometabuilder/workflow/plugins/utils_branch_condition.py b/src/autometabuilder/workflow/plugins/utils_branch_condition.py index d92827b..39778b3 100644 --- a/src/autometabuilder/workflow/plugins/utils_branch_condition.py +++ b/src/autometabuilder/workflow/plugins/utils_branch_condition.py @@ -3,7 +3,8 @@ import re from ..value_helpers import ValueHelpers -def run(runtime, inputs): +def run(_runtime, inputs): + """Evaluate a branch condition.""" value = inputs.get("value") mode = inputs.get("mode", "is_truthy") compare = inputs.get("compare", "") diff --git a/src/autometabuilder/workflow/plugins/utils_filter_list.py b/src/autometabuilder/workflow/plugins/utils_filter_list.py index 43865d5..0c7292e 100644 --- a/src/autometabuilder/workflow/plugins/utils_filter_list.py +++ b/src/autometabuilder/workflow/plugins/utils_filter_list.py @@ -3,7 +3,8 @@ import re from ..value_helpers import ValueHelpers -def run(runtime, inputs): +def run(_runtime, inputs): + """Filter items using a match mode.""" items = ValueHelpers.ensure_list(inputs.get("items")) mode = inputs.get("mode", "contains") pattern = inputs.get("pattern", "") diff --git a/src/autometabuilder/workflow/plugins/utils_map_list.py b/src/autometabuilder/workflow/plugins/utils_map_list.py index 8d4d64b..ce522a1 100644 --- a/src/autometabuilder/workflow/plugins/utils_map_list.py +++ b/src/autometabuilder/workflow/plugins/utils_map_list.py @@ -2,7 +2,8 @@ from ..value_helpers import ValueHelpers -def run(runtime, inputs): +def run(_runtime, inputs): + """Map items to formatted strings.""" items = ValueHelpers.ensure_list(inputs.get("items")) template = inputs.get("template", "{item}") mapped = [] diff --git a/src/autometabuilder/workflow/plugins/utils_not.py b/src/autometabuilder/workflow/plugins/utils_not.py index a7a283b..cc3ebe2 100644 --- a/src/autometabuilder/workflow/plugins/utils_not.py +++ b/src/autometabuilder/workflow/plugins/utils_not.py @@ -2,5 +2,6 @@ from ..value_helpers import ValueHelpers -def run(runtime, inputs): +def run(_runtime, inputs): + """Negate a boolean value.""" return {"result": not ValueHelpers.coerce_bool(inputs.get("value"))} diff --git a/src/autometabuilder/workflow/plugins/utils_reduce_list.py b/src/autometabuilder/workflow/plugins/utils_reduce_list.py index 020ac9d..41e053e 100644 --- a/src/autometabuilder/workflow/plugins/utils_reduce_list.py +++ b/src/autometabuilder/workflow/plugins/utils_reduce_list.py @@ -2,7 +2,8 @@ from ..value_helpers import ValueHelpers -def run(runtime, inputs): +def run(_runtime, inputs): + """Reduce a list into a string.""" items = ValueHelpers.ensure_list(inputs.get("items")) separator = ValueHelpers.normalize_separator(inputs.get("separator", "")) reduced = separator.join([str(item) for item in items]) diff --git a/src/autometabuilder/workflow/runtime.py b/src/autometabuilder/workflow/runtime.py index 7b053a0..53d92ec 100644 --- a/src/autometabuilder/workflow/runtime.py +++ b/src/autometabuilder/workflow/runtime.py @@ -2,6 +2,7 @@ class WorkflowRuntime: + """Runtime state for workflow execution.""" def __init__(self, context: dict, store: dict, tool_runner, logger): self.context = context self.store = store diff --git a/src/autometabuilder/workflow/tool_calls_handler.py b/src/autometabuilder/workflow/tool_calls_handler.py index 2350e2d..10fe996 100644 --- a/src/autometabuilder/workflow/tool_calls_handler.py +++ b/src/autometabuilder/workflow/tool_calls_handler.py @@ -3,6 +3,7 @@ import json def handle_tool_calls(resp_msg, tool_map: dict, msgs: dict, args, policies: dict, logger) -> list: + """Execute tool calls and return tool result messages.""" if not resp_msg.tool_calls: return [] @@ -17,9 +18,11 @@ def handle_tool_calls(resp_msg, tool_map: dict, msgs: dict, args, policies: dict handler = tool_map.get(function_name) if not handler: - msg = msgs.get("error_tool_not_found", "Tool {name} not found or unavailable.").format( - name=function_name + msg_template = msgs.get( + "error_tool_not_found", + "Tool {name} not found or unavailable." ) + msg = msg_template.format(name=function_name) logger.error(msg) tool_results.append({ "tool_call_id": call_id, @@ -37,7 +40,8 @@ def handle_tool_calls(resp_msg, tool_map: dict, msgs: dict, args, policies: dict ).format(name=function_name, args=payload) ) if confirm.lower() != "y": - logger.info(msgs.get("info_tool_skipped", "Skipping tool: {name}").format(name=function_name)) + skipped_template = msgs.get("info_tool_skipped", "Skipping tool: {name}") + logger.info(skipped_template.format(name=function_name)) tool_results.append({ "tool_call_id": call_id, "role": "tool", @@ -61,7 +65,8 @@ def handle_tool_calls(resp_msg, tool_map: dict, msgs: dict, args, policies: dict }) continue - logger.info(msgs.get("info_executing_tool", "Executing tool: {name}").format(name=function_name)) + exec_template = msgs.get("info_executing_tool", "Executing tool: {name}") + logger.info(exec_template.format(name=function_name)) try: result = handler(**payload) content = str(result) if result is not None else "Success" diff --git a/src/autometabuilder/workflow/tool_runner.py b/src/autometabuilder/workflow/tool_runner.py index 9b8e9ca..d4b81df 100644 --- a/src/autometabuilder/workflow/tool_runner.py +++ b/src/autometabuilder/workflow/tool_runner.py @@ -2,12 +2,14 @@ class ToolRunner: + """Run tool callables with shared logging.""" def __init__(self, tool_map: dict, msgs: dict, logger): self.tool_map = tool_map self.msgs = msgs self.logger = logger def call(self, tool_name: str, **kwargs): + """Call a named tool with filtered kwargs.""" tool = self.tool_map.get(tool_name) if not tool: msg = self.msgs.get( diff --git a/src/autometabuilder/workflow/value_helpers.py b/src/autometabuilder/workflow/value_helpers.py index cb7c453..269fa38 100644 --- a/src/autometabuilder/workflow/value_helpers.py +++ b/src/autometabuilder/workflow/value_helpers.py @@ -2,8 +2,10 @@ class ValueHelpers: + """Normalize values for workflow helpers.""" @staticmethod def ensure_list(value): + """Return a list for any incoming value.""" if value is None: return [] if isinstance(value, list): @@ -16,6 +18,7 @@ class ValueHelpers: @staticmethod def coerce_bool(value) -> bool: + """Coerce values into booleans.""" if isinstance(value, bool): return value if isinstance(value, str): @@ -28,6 +31,7 @@ class ValueHelpers: @staticmethod def normalize_separator(text): + """Normalize escaped separators.""" if text is None: return "" if isinstance(text, str): diff --git a/src/autometabuilder/workflow_config_loader.py b/src/autometabuilder/workflow_config_loader.py index 0a6946a..366168c 100644 --- a/src/autometabuilder/workflow_config_loader.py +++ b/src/autometabuilder/workflow_config_loader.py @@ -4,6 +4,7 @@ import os def load_workflow_config(metadata: dict) -> dict: + """Load workflow config referenced by metadata.""" workflow_file = metadata.get("workflow_path", "workflow.json") workflow_path = os.path.join(os.path.dirname(__file__), workflow_file) with open(workflow_path, "r", encoding="utf-8") as f: diff --git a/src/autometabuilder/workflow_context_builder.py b/src/autometabuilder/workflow_context_builder.py index bb96131..266407c 100644 --- a/src/autometabuilder/workflow_context_builder.py +++ b/src/autometabuilder/workflow_context_builder.py @@ -2,15 +2,9 @@ from .model_resolver import resolve_model_name -def build_workflow_context(args, gh, msgs, client, tools, tool_map, prompt, tool_policies) -> dict: - return { - "args": args, - "gh": gh, - "msgs": msgs, - "client": client, - "model_name": resolve_model_name(prompt), - "tools": tools, - "tool_map": tool_map, - "prompt": prompt, - "tool_policies": tool_policies, - } +def build_workflow_context(parts: dict) -> dict: + """Build the workflow context dict.""" + prompt = parts["prompt"] + context = dict(parts) + context["model_name"] = resolve_model_name(prompt) + return context diff --git a/src/autometabuilder/workflow_engine_builder.py b/src/autometabuilder/workflow_engine_builder.py index cbe24ed..efcd917 100644 --- a/src/autometabuilder/workflow_engine_builder.py +++ b/src/autometabuilder/workflow_engine_builder.py @@ -9,6 +9,7 @@ from .workflow.tool_runner import ToolRunner def build_workflow_engine(workflow_config: dict, context: dict, logger): + """Assemble workflow engine dependencies.""" runtime = WorkflowRuntime(context=context, store={}, tool_runner=None, logger=logger) tool_runner = ToolRunner(context["tool_map"], context["msgs"], logger) runtime.tool_runner = tool_runner