- Add AGENCY.md: master orchestration protocol (universal AI editor entry point)
- Add 00_intake.md: user requirements & intent detection agent
- Add 10_deploy.md: production deployment agent
- Add 11_seo_content.md: SEO specialist & content writer agent (dual mode)
- Add specs/reviews/: directory for specialist review reports
REVIEW PIPELINE (new):
- Each specialist reviews ONLY their own domain:
- 04_dev_backend: API, services, DB, auth, DTOs
- 05_dev_frontend: components, state, UX code, performance
- 07_visual_qa: UX patterns, a11y, responsive, design system
- 08_devops_security: secrets, Docker, CVEs, CI/CD
- 11_seo_content: meta tags, content quality, structured data
- 02_product_manager: synthesis mode reads all findings, deduplicates,
creates unified prioritized backlog
All agents now support dual modes (REVIEW + IMPLEMENT/ENFORCE/CREATE/INSPECT)
state.json v3: adds project_intent, review_phase tracking, resume_context
backlog.json: fixed structure {tasks: [...]}, added sub_steps per task
orchestrate.py: simplified to state management utility (no fake AI calls)
198 lines
6.6 KiB
Python
198 lines
6.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
AI Agency State Management Utility
|
|
====================================
|
|
This script is a HELPER TOOL only — it does NOT run the AI.
|
|
The AI orchestration logic lives in .ai_agency/AGENCY.md
|
|
|
|
Use these utilities to:
|
|
- View current project state
|
|
- Manually reset state
|
|
- Fix stuck/blocked states
|
|
- Inspect backlog progress
|
|
|
|
Usage:
|
|
python orchestrate.py status # Show current state summary
|
|
python orchestrate.py reset # Reset state to IDLE (start over)
|
|
python orchestrate.py unblock # Clear BLOCKED_NEEDS_HUMAN status
|
|
python orchestrate.py next-task # Show next pending task
|
|
python orchestrate.py progress # Show backlog completion progress
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
from datetime import datetime
|
|
|
|
AGENCY_DIR = ".ai_agency"
|
|
STATE_FILE = os.path.join(AGENCY_DIR, "memory/state.json")
|
|
BACKLOG_FILE = os.path.join(AGENCY_DIR, "memory/backlog.json")
|
|
|
|
def load_json(path):
|
|
if not os.path.exists(path):
|
|
return None
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
return json.load(f)
|
|
|
|
def save_json(path, data):
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
|
|
def cmd_status():
|
|
state = load_json(STATE_FILE)
|
|
if not state:
|
|
print("❌ No state.json found. Agency has not been initialized yet.")
|
|
return
|
|
|
|
print(f"\n{'='*50}")
|
|
print(f"🏢 AI Agency Status")
|
|
print(f"{'='*50}")
|
|
print(f"Project : {state.get('project_name', 'Unknown')}")
|
|
print(f"Mode : {state.get('project_mode', 'Unknown')}")
|
|
print(f"Status : {state.get('status', 'Unknown')}")
|
|
|
|
cp = state.get("checkpoint", {})
|
|
print(f"\n📍 Checkpoint:")
|
|
print(f" Active Agent : {cp.get('active_agent', 'None')}")
|
|
print(f" Active Ticket : {cp.get('current_ticket_id', 'None')}")
|
|
|
|
sub = cp.get("sub_step", {})
|
|
if sub:
|
|
print(f" Sub-step : {sub.get('index')}/{sub.get('total')} — {sub.get('description', '')}")
|
|
|
|
rc = state.get("resume_context", {})
|
|
if rc.get("last_completed_action"):
|
|
print(f"\n💾 Resume Context:")
|
|
print(f" Last done : {rc.get('last_completed_action', '')}")
|
|
print(f" Next todo : {rc.get('next_action', '')}")
|
|
|
|
guards = state.get("execution_guards", {})
|
|
if guards.get("blocking_reason"):
|
|
print(f"\n🚫 BLOCKED: {guards['blocking_reason']}")
|
|
|
|
print(f"\n🕐 Last Updated: {state.get('last_updated', 'Unknown')}")
|
|
|
|
def cmd_progress():
|
|
backlog = load_json(BACKLOG_FILE)
|
|
if not backlog:
|
|
print("❌ No backlog.json found.")
|
|
return
|
|
|
|
tasks = backlog.get("tasks", [])
|
|
if not tasks:
|
|
print("❌ No tasks in backlog.")
|
|
return
|
|
|
|
total = len(tasks)
|
|
completed = sum(1 for t in tasks if t.get("status") == "completed")
|
|
pending = sum(1 for t in tasks if t.get("status") == "pending")
|
|
blocked = sum(1 for t in tasks if t.get("status") in ["BLOCKED", "SECURITY_FAILED"])
|
|
|
|
print(f"\n{'='*50}")
|
|
print(f"📋 Backlog Progress")
|
|
print(f"{'='*50}")
|
|
print(f"Total : {total}")
|
|
print(f"✅ Done : {completed}")
|
|
print(f"⏳ Pending: {pending}")
|
|
print(f"❌ Blocked: {blocked}")
|
|
print(f"\nProgress : [{'█' * completed}{'░' * pending}] {int(completed/total*100)}%")
|
|
|
|
print(f"\n📝 Task List:")
|
|
for task in tasks:
|
|
icon = {"completed": "✅", "pending": "⏳", "COMPLETED_PENDING_QA": "🔍"}.get(task.get("status", ""), "❓")
|
|
print(f" {icon} [{task['priority']}] {task['id']}: {task['title'][:60]}")
|
|
|
|
def cmd_next_task():
|
|
backlog = load_json(BACKLOG_FILE)
|
|
if not backlog:
|
|
print("❌ No backlog.json found.")
|
|
return
|
|
|
|
tasks = backlog.get("tasks", [])
|
|
completed_ids = [t["id"] for t in tasks if t.get("status") == "completed"]
|
|
|
|
for task in tasks:
|
|
if task.get("status") == "pending":
|
|
deps = task.get("dependency_task_ids", [])
|
|
if all(dep in completed_ids for dep in deps):
|
|
print(f"\n🎯 Next Task Ready:")
|
|
print(f" ID : {task['id']}")
|
|
print(f" Title : {task['title']}")
|
|
print(f" Role : {task['assigned_role']}")
|
|
print(f" Priority : {task['priority']}")
|
|
print(f" Est. : {task['estimated_minutes']} minutes")
|
|
return
|
|
|
|
print("✅ No pending tasks with satisfied dependencies found.")
|
|
|
|
def cmd_unblock():
|
|
state = load_json(STATE_FILE)
|
|
if not state:
|
|
print("❌ No state.json found.")
|
|
return
|
|
|
|
state["status"] = "IN_PROGRESS"
|
|
state["execution_guards"]["retry_count"] = 0
|
|
state["execution_guards"]["blocking_reason"] = None
|
|
state["last_updated"] = datetime.now().isoformat()
|
|
save_json(STATE_FILE, state)
|
|
print(f"✅ Status reset to IN_PROGRESS. Blocking reason cleared.")
|
|
print(f" Active agent: {state['checkpoint'].get('active_agent')}")
|
|
print(f" Resume the AI and say: 'continue agency work'")
|
|
|
|
def cmd_reset():
|
|
confirm = input("⚠️ This will reset state to IDLE. All checkpoint progress will be lost. Type 'yes' to confirm: ")
|
|
if confirm.strip().lower() != "yes":
|
|
print("Aborted.")
|
|
return
|
|
|
|
state = {
|
|
"schema_version": "2.0",
|
|
"project_name": "UNINITIALIZED",
|
|
"project_root": ".",
|
|
"project_mode": "UNKNOWN",
|
|
"status": "IDLE",
|
|
"checkpoint": {
|
|
"active_agent": "00_intake",
|
|
"current_ticket_id": None,
|
|
"sub_step": { "index": 0, "total": 0, "name": "", "description": "" }
|
|
},
|
|
"resume_context": {
|
|
"last_completed_action": None,
|
|
"next_action": "Start from 00_intake — gather requirements",
|
|
"files_modified_this_session": [],
|
|
"files_pending": [],
|
|
"notes": "Fresh start. Tell the AI: 'use the agency' to begin."
|
|
},
|
|
"tech_stack": {},
|
|
"execution_guards": {
|
|
"retry_count": 0,
|
|
"max_retry_attempts": 3,
|
|
"blocking_reason": None,
|
|
"agent_visit_counts": {}
|
|
},
|
|
"agent_call_log": [],
|
|
"last_updated": datetime.now().isoformat()
|
|
}
|
|
save_json(STATE_FILE, state)
|
|
print("✅ State reset to IDLE. Start fresh by telling the AI: 'use the agency'")
|
|
|
|
def main():
|
|
commands = {
|
|
"status": cmd_status,
|
|
"progress": cmd_progress,
|
|
"next-task": cmd_next_task,
|
|
"unblock": cmd_unblock,
|
|
"reset": cmd_reset,
|
|
}
|
|
|
|
if len(sys.argv) < 2 or sys.argv[1] not in commands:
|
|
print(__doc__)
|
|
print("Available commands:", ", ".join(commands.keys()))
|
|
sys.exit(1)
|
|
|
|
commands[sys.argv[1]]()
|
|
|
|
if __name__ == "__main__":
|
|
main()
|