Introduce AutoMetabuilder core components and workflow packages:

- Implement core components: CLI argument parsing, environment loading, GitHub service creation, and logging configuration.
- Add support for OpenAI client setup and model resolution.
- Develop SDLC context loader from GitHub and repository files.
- Implement workflow context and engine builders.
- Introduce major workflow packages: `game_tick_loop` and `contextual_iterative_loop`.
- Update localization files with new package descriptions and labels.
- Streamline web navigation by loading items from a dedicated JSON file.
This commit is contained in:
2026-01-09 22:01:55 +00:00
parent ac8479581f
commit 0470cc196c
63 changed files with 304 additions and 69 deletions

Binary file not shown.

View File

@@ -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()

View File

@@ -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)

View File

@@ -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()

View File

@@ -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:

View File

@@ -3,4 +3,5 @@ from dotenv import load_dotenv
def load_env() -> None:
"""Load environment variables."""
load_dotenv()

View File

@@ -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:

View File

@@ -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",

View File

@@ -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)

View File

@@ -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))

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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")

View File

@@ -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": []}

View File

@@ -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 []

View File

@@ -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()

View File

@@ -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:

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -0,0 +1,10 @@
/**
* AutoMetabuilder - Workflow Palette Plugin
*/
(() => {
const init = async () => {
window.AMBWorkflowPalette?.init?.();
};
window.AMBPlugins?.register('workflow_palette', init);
})();

View File

@@ -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);
};

View File

@@ -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 = `<p class="text-muted small mb-0">${escapeHtml ? escapeHtml(t?.('ui.workflow.palette.empty', 'No matching nodes.')) : t?.('ui.workflow.palette.empty', 'No matching nodes.')}</p>`;
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 `
<button type="button" class="amb-workflow-palette-item" data-node-type="${safeKey}">
<span class="amb-workflow-palette-title">${safeLabel}</span>
<span class="amb-workflow-palette-subtitle">${safeKey}</span>
</button>
`;
}).join('');
return `
<div class="amb-workflow-palette-group">
<div class="amb-workflow-palette-heading">${escapeHtml ? escapeHtml(groupLabel) : groupLabel}</div>
<div class="amb-workflow-palette-items">${items}</div>
</div>
`;
}).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 };
})();

View File

@@ -15,7 +15,10 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/choices.js@10.2.0/public/assets/styles/choices.min.css">
<!-- Custom Styles -->
<link rel="stylesheet" href="/static/css/main.css">
{% 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 %}
<link rel="stylesheet" href="{{ style }}">
{% endfor %}
{% block head %}{% endblock %}
</head>

View File

@@ -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', []) %}
<div class="amb-form-group">
<label class="amb-form-label">{{ t(desc_label, desc_label) }}</label>
{% if desc_text %}
@@ -20,7 +21,8 @@
<select name="env_{{ key }}" class="form-select" data-choices>
<option value="">{{ t('ui.common.select_placeholder', 'Select...') }}</option>
{% for opt in desc.get('options', []) %}
<option value="{{ opt }}" {% if env_vars.get(key) == opt %}selected{% endif %}>{{ opt }}</option>
{% set opt_label = option_labels[loop.index0] if option_labels|length > loop.index0 else opt %}
<option value="{{ opt }}" {% if env_vars.get(key) == opt %}selected{% endif %}>{{ t(opt_label, opt_label) }}</option>
{% endfor %}
</select>
{% else %}

View File

@@ -0,0 +1,16 @@
{# Workflow palette card macro. #}
{% macro workflow_palette() -%}
<div class="amb-card amb-workflow-palette" id="workflow-palette">
<div class="amb-card-header">
<h5><i class="bi bi-grid"></i> {{ t('ui.workflow.palette.title', 'Node Palette') }}</h5>
</div>
<div class="amb-card-body">
<label class="form-label small text-muted" for="workflow-palette-search">{{ t('ui.workflow.palette.search_label', 'Find nodes') }}</label>
<input type="search" id="workflow-palette-search" class="form-control form-control-sm"
placeholder="{{ t('ui.workflow.palette.search_placeholder', 'Search nodes or keywords...') }}">
<div id="workflow-palette-list" class="amb-workflow-palette-list">
<p class="text-muted small mb-0">{{ t('ui.workflow.palette.loading', 'Loading nodes...') }}</p>
</div>
</div>
</div>
{%- endmacro %}

View File

@@ -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() -%}
<section id="workflow" class="amb-section">
@@ -8,29 +9,36 @@
{{ workflow_templates() }}
<div class="amb-card">
<div class="amb-card-header">
<h5><i class="bi bi-diagram-3"></i> {{ t('ui.workflow.card.title', 'Tasks & Steps') }}</h5>
<div>
<button type="button" class="btn btn-sm btn-outline-secondary" data-workflow-toggle>
<i class="bi bi-code-slash"></i> {{ t('ui.workflow.toggle_json', 'Toggle JSON') }}
</button>
</div>
<div class="row g-3">
<div class="col-lg-4">
{{ workflow_palette() }}
</div>
<div class="amb-card-body">
<div id="workflow-builder" class="mb-3">
<button type="button" class="btn btn-primary" data-workflow-fallback>
<i class="bi bi-plus-lg"></i> {{ t('ui.workflow.add_node', 'Add Node') }}
</button>
</div>
<form action="/workflow" method="post" id="workflow-form">
<textarea id="workflow-content" name="content" class="form-control amb-code-editor d-none" rows="15">{{ workflow_content }}</textarea>
<div class="d-flex gap-2 mt-3">
<button type="submit" class="btn btn-success">
<i class="bi bi-save"></i> {{ t('ui.workflow.save', 'Save Workflow') }}
</button>
<div class="col-lg-8">
<div class="amb-card">
<div class="amb-card-header">
<h5><i class="bi bi-diagram-3"></i> {{ t('ui.workflow.card.title', 'Tasks & Steps') }}</h5>
<div>
<button type="button" class="btn btn-sm btn-outline-secondary" data-workflow-toggle>
<i class="bi bi-code-slash"></i> {{ t('ui.workflow.toggle_json', 'Toggle JSON') }}
</button>
</div>
</div>
</form>
<div class="amb-card-body">
<div id="workflow-builder" class="mb-3">
<button type="button" class="btn btn-primary" data-workflow-fallback>
<i class="bi bi-plus-lg"></i> {{ t('ui.workflow.add_node', 'Add Node') }}
</button>
</div>
<form action="/workflow" method="post" id="workflow-form">
<textarea id="workflow-content" name="content" class="form-control amb-code-editor d-none" rows="15">{{ workflow_content }}</textarea>
<div class="d-flex gap-2 mt-3">
<button type="submit" class="btn btn-success">
<i class="bi bi-save"></i> {{ t('ui.workflow.save', 'Save Workflow') }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</section>

View File

@@ -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",

View File

@@ -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.")

View File

@@ -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)

View File

@@ -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")

View File

@@ -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.")

View File

@@ -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)

View File

@@ -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)

View File

@@ -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"],

View File

@@ -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:

View File

@@ -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:

View File

@@ -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}

View File

@@ -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"])}

View File

@@ -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:

View File

@@ -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"])}

View File

@@ -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"),

View File

@@ -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"),

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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", "")

View File

@@ -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", "")

View File

@@ -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 = []

View File

@@ -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"))}

View File

@@ -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])

View File

@@ -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

View File

@@ -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"

View File

@@ -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(

View File

@@ -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):

View File

@@ -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:

View File

@@ -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

View File

@@ -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