Implement MVP milestone tracking with multi-iteration task loop and roadmap utilities. Update Web UI to display MVP status.

This commit is contained in:
2026-01-09 14:22:13 +00:00
parent c992d4f2c1
commit 580d89b281
5 changed files with 106 additions and 32 deletions

View File

@@ -12,7 +12,7 @@
- [x] **Declarative Task Processing**: Move more logic into JSON/YAML specifications.
- [x] **Feedback Loop**: Support for the AI to read comments on PRs it created.
## Phase 3: Advanced Automation
## Phase 3: Advanced Automation (MVP)
- [x] **Automated Testing**: Integration with test runners to verify changes before PR.
- [x] **Linting Integration**: Automatically run and fix linting issues.
- [x] **Multi-Model Support**: Easily switch between different LLM providers.

View File

@@ -17,6 +17,7 @@ from .github_integration import GitHubIntegration, get_repo_name_from_env
from .docker_utils import run_command_in_docker
from .web.server import start_web_ui
from .integrations.notifications import notify_all
from .roadmap_utils import update_roadmap, is_mvp_reached
load_dotenv()
@@ -77,11 +78,6 @@ def get_sdlc_context(gh: GitHubIntegration, msgs: dict) -> str:
return sdlc_context
def update_roadmap(content: str):
"""Update ROADMAP.md with new content."""
with open("ROADMAP.md", "w", encoding="utf-8") as f:
f.write(content)
logger.info("ROADMAP.md updated successfully.")
def list_files(directory: str = "."):
@@ -349,34 +345,55 @@ def main():
messages.append({"role": "user", "content": msgs["user_next_step"]})
model_name = os.environ.get("LLM_MODEL", prompt.get("model", DEFAULT_MODEL))
response = get_completion(client, model_name, messages, tools)
resp_msg = response.choices[0].message
logger.info(
resp_msg.content
if resp_msg.content
else msgs["info_tool_call_requested"]
)
# Handle tool calls
tool_results = handle_tool_calls(resp_msg, tool_map, gh, msgs, dry_run=args.dry_run, yolo=args.yolo)
if args.once and tool_results:
logger.info(msgs.get("info_second_pass", "Performing second pass with tool results..."))
# Multi-iteration loop
iteration = 0
max_iterations = 10
while iteration < max_iterations:
iteration += 1
logger.info(f"--- Iteration {iteration} ---")
response = get_completion(client, model_name, messages, tools)
resp_msg = response.choices[0].message
logger.info(
resp_msg.content
if resp_msg.content
else msgs["info_tool_call_requested"]
)
messages.append(resp_msg)
if not resp_msg.tool_calls:
# If no more tools requested, we are done
notify_all(f"AutoMetabuilder task complete: {resp_msg.content[:100]}...")
break
# Handle tool calls
tool_results = handle_tool_calls(resp_msg, tool_map, gh, msgs, dry_run=args.dry_run, yolo=args.yolo)
messages.extend(tool_results)
response = get_completion(client, model_name, messages, tools)
final_msg = response.choices[0].message
logger.info(final_msg.content if final_msg.content else msgs["info_tool_call_requested"])
# Notify about task completion
notify_all(f"AutoMetabuilder task complete: {final_msg.content[:100]}...")
# In a multi-iteration loop, we would call handle_tool_calls again here.
# For --once, we just do one more pass.
if final_msg.tool_calls:
handle_tool_calls(final_msg, tool_map, gh, msgs, dry_run=args.dry_run, yolo=args.yolo)
if args.yolo and is_mvp_reached():
logger.info("MVP reached. Stopping YOLO loop.")
notify_all("AutoMetabuilder YOLO loop stopped: MVP reached.")
break
if args.once:
# If --once is set, we do one more pass to show the final result
logger.info(msgs.get("info_second_pass", "Performing second pass with tool results..."))
response = get_completion(client, model_name, messages, tools)
final_msg = response.choices[0].message
logger.info(final_msg.content if final_msg.content else msgs["info_tool_call_requested"])
notify_all(f"AutoMetabuilder task complete: {final_msg.content[:100]}...")
# For --once, we still handle tool calls if any in the second pass, but then stop.
if final_msg.tool_calls:
handle_tool_calls(final_msg, tool_map, gh, msgs, dry_run=args.dry_run, yolo=args.yolo)
break
else:
logger.warning(f"Reached maximum iterations ({max_iterations}). Stopping.")
notify_all(f"AutoMetabuilder stopped: Reached {max_iterations} iterations.")
if __name__ == "__main__":

View File

@@ -0,0 +1,40 @@
import os
import re
import logging
logger = logging.getLogger("autometabuilder")
def update_roadmap(content: str):
"""Update ROADMAP.md with new content."""
with open("ROADMAP.md", "w", encoding="utf-8") as f:
f.write(content)
logger.info("ROADMAP.md updated successfully.")
def is_mvp_reached() -> bool:
"""Check if the MVP section in ROADMAP.md is completed."""
if not os.path.exists("ROADMAP.md"):
return False
with open("ROADMAP.md", "r", encoding="utf-8") as f:
content = f.read()
# Find the MVP section
mvp_match = re.search(r"## .*?\(MVP\)(.*?)##", content, re.DOTALL | re.IGNORECASE)
if not mvp_match:
# Try finding it if it's the last section
mvp_match = re.search(r"## .*?\(MVP\)(.*)", content, re.DOTALL | re.IGNORECASE)
if not mvp_match:
return False
mvp_section = mvp_match.group(1)
# Check if there are any unchecked items [ ]
if "[ ]" in mvp_section:
return False
# If there are checked items [x], and no unchecked items, we consider it reached
if "[x]" in mvp_section:
return True
return False

View File

@@ -8,6 +8,7 @@ from fastapi.security import HTTPBasic, HTTPBasicCredentials
from dotenv import load_dotenv, set_key
import subprocess
import sys
from ..roadmap_utils import is_mvp_reached
app = FastAPI()
security = HTTPBasic()
@@ -92,6 +93,7 @@ async def read_item(request: Request, username: str = Depends(get_current_user))
translations = get_translations()
prompt_content = get_prompt_content()
is_running = bot_process is not None
mvp_status = is_mvp_reached()
return templates.TemplateResponse("index.html", {
"request": request,
"logs": logs,
@@ -99,6 +101,7 @@ async def read_item(request: Request, username: str = Depends(get_current_user))
"translations": translations,
"prompt_content": prompt_content,
"is_running": is_running,
"mvp_reached": mvp_status,
"username": username
})
@@ -135,7 +138,10 @@ async def update_settings(request: Request, username: str = Depends(get_current_
@app.get("/api/status")
async def get_status(username: str = Depends(get_current_user)):
return {"is_running": bot_process is not None}
return {
"is_running": bot_process is not None,
"mvp_reached": is_mvp_reached()
}
@app.get("/api/logs")
async def get_logs(username: str = Depends(get_current_user)):

View File

@@ -25,6 +25,13 @@
<span class="badge bg-success">Idle</span>
{% endif %}
</p>
<p>MVP Milestone:
{% if mvp_reached %}
<span class="badge bg-primary">Reached ✓</span>
{% else %}
<span class="badge bg-secondary">In Progress</span>
{% endif %}
</p>
</div>
<form action="/run" method="post" class="mt-2">
@@ -111,13 +118,17 @@
if (data.is_running) {
statusContainer.innerHTML = `
<p>Status: <span class="badge bg-warning text-dark">Bot Running...</span></p>
<p>MVP Milestone: ${data.mvp_reached ? '<span class="badge bg-primary">Reached ✓</span>' : '<span class="badge bg-secondary">In Progress</span>'}</p>
<div class="progress mt-2">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%"></div>
</div>
`;
runBtn.disabled = true;
} else {
statusContainer.innerHTML = `<p>Status: <span class="badge bg-success">Idle</span></p>`;
statusContainer.innerHTML = `
<p>Status: <span class="badge bg-success">Idle</span></p>
<p>MVP Milestone: ${data.mvp_reached ? '<span class="badge bg-primary">Reached ✓</span>' : '<span class="badge bg-secondary">In Progress</span>'}</p>
`;
runBtn.disabled = false;
}
} catch (error) {