208 lines
7.6 KiB
Python
208 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
import os
|
|
import sys
|
|
import json
|
|
import subprocess
|
|
from datetime import datetime
|
|
|
|
# Path Configurations
|
|
AGENCY_DIR = ".ai_agency"
|
|
STATE_FILE = os.path.join(AGENCY_DIR, "memory/state.json")
|
|
BACKLOG_FILE = os.path.join(AGENCY_DIR, "memory/backlog.json")
|
|
SCRATCHPAD_FILE = os.path.join(AGENCY_DIR, "memory/scratchpad.md")
|
|
AGENTS_DIR = os.path.join(AGENCY_DIR, "agents")
|
|
|
|
def load_json(filepath):
|
|
if not os.path.exists(filepath):
|
|
return {}
|
|
with open(filepath, "r", encoding="utf-8") as f:
|
|
return json.load(f)
|
|
|
|
def save_json(filepath, data):
|
|
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
|
with open(filepath, "w", encoding="utf-8") as f:
|
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
|
|
def run_cmd(cmd, check=True):
|
|
print(f"[EXEC] {cmd}")
|
|
res = subprocess.run(cmd, shell=True, text=True, capture_output=True)
|
|
if check and res.returncode != 0:
|
|
print(f"[ERROR] Command failed: {res.stderr}")
|
|
return False, res.stdout, res.stderr
|
|
return True, res.stdout, res.stderr
|
|
|
|
def is_brownfield():
|
|
markers = ["package.json", "requirements.txt", "pyproject.toml", "Cargo.toml", "go.mod", "composer.json"]
|
|
return any(os.path.exists(marker) for marker in markers)
|
|
|
|
def call_ai_agent(agent_name, prompt_context):
|
|
agent_file = os.path.join(AGENTS_DIR, f"{agent_name}.md")
|
|
if not os.path.exists(agent_file):
|
|
raise FileNotFoundError(f"Agent system prompt file missing: {agent_file}")
|
|
|
|
with open(agent_file, "r", encoding="utf-8") as f:
|
|
system_prompt = f.read()
|
|
|
|
full_prompt = f"{system_prompt}\n\n--- CURRENT EXECUTION CONTEXT ---\n{prompt_context}"
|
|
|
|
print(f"[AI RUNNING] Invoking agent: {agent_name}...")
|
|
|
|
with open(SCRATCHPAD_FILE, "a", encoding="utf-8") as f:
|
|
f.write(f"\n\n### Agent Call: {agent_name} @ {datetime.now().isoformat()}\n")
|
|
f.write(prompt_context)
|
|
|
|
return True
|
|
|
|
def handle_git_commit(ticket_id, message, is_wip=False):
|
|
prefix = "wip:" if is_wip else "feat:"
|
|
commit_msg = f"{prefix} {ticket_id} - {message}"
|
|
run_cmd("git add .", check=False)
|
|
run_cmd(f'git commit -m "{commit_msg}"', check=False)
|
|
|
|
def get_next_pending_task():
|
|
backlog = load_json(BACKLOG_FILE)
|
|
tasks = backlog.get("tasks", [])
|
|
for task in tasks:
|
|
if task.get("status") in ["PENDING", "pending"]:
|
|
deps = task.get("dependency_task_ids", [])
|
|
completed_ids = [t["id"] for t in tasks if t.get("status") in ["DONE", "completed"]]
|
|
if all(dep in completed_ids for dep in deps):
|
|
return task
|
|
return None
|
|
|
|
def mark_task_complete(ticket_id):
|
|
backlog = load_json(BACKLOG_FILE)
|
|
for task in backlog.get("tasks", []):
|
|
if task["id"] == ticket_id:
|
|
task["status"] = "DONE"
|
|
break
|
|
backlog["completed_tasks"] = sum(1 for t in backlog.get("tasks", []) if t.get("status") == "DONE")
|
|
save_json(BACKLOG_FILE, backlog)
|
|
|
|
def main():
|
|
print("=== AI Software House Orchestrator Running ===")
|
|
|
|
state = load_json(STATE_FILE)
|
|
if not state or state.get("project_name") == "UNINITIALIZED":
|
|
state = {
|
|
"project_name": "VibeForge Project",
|
|
"project_mode": "UNKNOWN",
|
|
"status": "RUNNING",
|
|
"checkpoint": {
|
|
"stage": "INITIALIZATION",
|
|
"active_agent": "00_auditor" if is_brownfield() else "01_ceo",
|
|
"current_ticket_id": None,
|
|
"sub_step_index": 0,
|
|
"total_sub_steps": 0
|
|
},
|
|
"execution_guards": {
|
|
"retry_count": 0,
|
|
"max_retry_attempts": 3,
|
|
"max_tokens_per_ticket": 50000,
|
|
"token_usage_total": 0
|
|
},
|
|
"git_state": {
|
|
"active_branch": "main",
|
|
"last_healthy_commit": None
|
|
},
|
|
"context_buffer": {
|
|
"last_agent_summary": "System initialized."
|
|
},
|
|
"last_updated": datetime.now().isoformat()
|
|
}
|
|
|
|
state["project_mode"] = "BROWNFIELD" if is_brownfield() else "GREENFIELD"
|
|
save_json(STATE_FILE, state)
|
|
|
|
if state.get("status") == "BLOCKED_NEEDS_HUMAN":
|
|
print("[BLOCKED] Orchestrator halted. Retry limits reached or manual intervention required.")
|
|
sys.exit(1)
|
|
|
|
active_agent = state["checkpoint"]["active_agent"]
|
|
current_ticket = state["checkpoint"]["current_ticket_id"]
|
|
print(f"State Hydrated: Agent={active_agent} | Mode={state['project_mode']} | Ticket={current_ticket}")
|
|
|
|
# Branch Management per Ticket
|
|
if current_ticket:
|
|
branch_name = f"feature/{current_ticket}"
|
|
run_cmd(f"git checkout -b {branch_name}", check=False)
|
|
run_cmd(f"git checkout {branch_name}", check=False)
|
|
|
|
# Prepare Context Prompt for current Agent Execution
|
|
context_data = {
|
|
"state": state,
|
|
"active_ticket_info": current_ticket,
|
|
"scratchpad": SCRATCHPAD_FILE
|
|
}
|
|
|
|
# Call AI Agent Logic
|
|
success = call_ai_agent(active_agent, json.dumps(context_data, indent=2))
|
|
|
|
if not success:
|
|
state["execution_guards"]["retry_count"] += 1
|
|
if state["execution_guards"]["retry_count"] >= state["execution_guards"]["max_retry_attempts"]:
|
|
state["status"] = "BLOCKED_NEEDS_HUMAN"
|
|
print(f"[FATAL] Agent {active_agent} failed {state['execution_guards']['max_retry_attempts']} times. Halting.")
|
|
save_json(STATE_FILE, state)
|
|
sys.exit(1)
|
|
|
|
# Dynamic Transition Logic
|
|
next_agent = None
|
|
|
|
if active_agent == "00_auditor":
|
|
next_agent = "01_ceo"
|
|
elif active_agent == "01_ceo":
|
|
next_agent = "02_product_manager"
|
|
elif active_agent == "02_product_manager":
|
|
next_agent = "03_architect"
|
|
elif active_agent == "03_architect":
|
|
next_task = get_next_pending_task()
|
|
if next_task:
|
|
state["checkpoint"]["current_ticket_id"] = next_task["id"]
|
|
next_agent = next_task.get("assigned_role", "04_dev_backend")
|
|
else:
|
|
next_agent = "09_tech_writer"
|
|
|
|
elif active_agent in ["04_dev_backend", "05_dev_frontend"]:
|
|
handle_git_commit(current_ticket, f"Work in progress by {active_agent}", is_wip=True)
|
|
next_agent = "06_qa_engineer"
|
|
|
|
elif active_agent == "06_qa_engineer":
|
|
next_agent = "07_visual_qa"
|
|
|
|
elif active_agent == "07_visual_qa":
|
|
next_agent = "08_devops_security"
|
|
|
|
elif active_agent == "08_devops_security":
|
|
next_agent = "09_tech_writer"
|
|
|
|
elif active_agent == "09_tech_writer":
|
|
handle_git_commit(current_ticket, "Completed task and updated documentation", is_wip=False)
|
|
mark_task_complete(current_ticket)
|
|
|
|
next_task = get_next_pending_task()
|
|
if next_task:
|
|
state["checkpoint"]["current_ticket_id"] = next_task["id"]
|
|
next_agent = next_task.get("assigned_role", "04_dev_backend")
|
|
print(f"[BACKLOG] Moving to next task: {next_task['id']}")
|
|
else:
|
|
state["checkpoint"]["current_ticket_id"] = None
|
|
next_agent = "COMPLETE"
|
|
|
|
# Update State & Save Checkpoint
|
|
if next_agent == "COMPLETE":
|
|
state["status"] = "SUCCESS"
|
|
state["checkpoint"]["active_agent"] = None
|
|
print("ALL BACKLOG TASKS COMPLETED SUCCESSFULLY!")
|
|
else:
|
|
state["checkpoint"]["active_agent"] = next_agent
|
|
state["execution_guards"]["retry_count"] = 0
|
|
print(f"[TRANSITION] Next Active Agent: {next_agent}")
|
|
|
|
state["last_updated"] = datetime.now().isoformat()
|
|
save_json(STATE_FILE, state)
|
|
print("Orchestrator loop completed safely. State persisted to disk.")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|