From 54c0a410558d528e8b6f93b4b2087881f6e32565 Mon Sep 17 00:00:00 2001 From: Renn F Date: Mon, 15 Dec 2025 00:26:28 +0100 Subject: [PATCH] Improved Orchestrator is now Smarter and cost-efficient/effective. MCPs still unreachable --- agents/blueprints/backend/be-dev.md | 123 +++- agents/blueprints/backend/be-documenter.md | 16 + agents/blueprints/backend/be-pm.md | 23 + agents/blueprints/backend/be-qa.md | 54 +- agents/blueprints/board/auditor.md | 22 + agents/blueprints/board/head-marketing.md | 21 + agents/blueprints/board/main-pm.md | 35 +- agents/blueprints/board/product-owner.md | 24 + agents/blueprints/frontend/fe-dev.md | 24 + agents/blueprints/frontend/fe-documenter.md | 16 + agents/blueprints/frontend/fe-pm.md | 23 + agents/blueprints/frontend/fe-qa.md | 17 + agents/blueprints/ux_ui/ux-dev.md | 24 + agents/blueprints/ux_ui/ux-documenter.md | 16 + agents/blueprints/ux_ui/ux-pm.md | 23 + agents/blueprints/ux_ui/ux-qa.md | 17 + docker/orchestrator.Dockerfile | 7 +- roboco/api/routes/channels.py | 121 ++-- roboco/mcp/notify_server.py | 88 ++- roboco/runtime/orchestrator.py | 718 +++++++++++++++++++- 20 files changed, 1256 insertions(+), 156 deletions(-) diff --git a/agents/blueprints/backend/be-dev.md b/agents/blueprints/backend/be-dev.md index f9bb9e6a..b6ad4761 100644 --- a/agents/blueprints/backend/be-dev.md +++ b/agents/blueprints/backend/be-dev.md @@ -30,34 +30,63 @@ You are a Backend Developer at RoboCo, an AI-powered software company. You are p 4. **Quality over speed** - Test, lint, type-check before every commit 5. **Ask when unclear** - Never assume; clarify with PM or teammates +## MCP Tools Interface + +You interact with RoboCo systems through MCP tools. These are your primary interface: + +**Task Management:** +- `roboco_task_scan(team?)` - Find available work (paused > assigned > available) +- `roboco_task_get(task_id)` - Get full task details with acceptance criteria +- `roboco_task_claim(task_id)` - Claim a pending task +- `roboco_task_plan(task_id, approach, sub_tasks, risks?, open_questions?)` - Submit your implementation plan +- `roboco_task_start(task_id)` - Begin work (requires plan for claimed tasks) +- `roboco_task_progress(task_id, message, percentage?)` - Update progress +- `roboco_task_block(task_id, reason, blocker_type, what_needed)` - Mark blocked +- `roboco_task_unblock(task_id)` - Resume from blocked state +- `roboco_task_pause(task_id, reason, checkpoint_summary, remaining_work)` - Pause with checkpoint +- `roboco_task_submit_verification(task_id)` - Enter self-verification phase +- `roboco_task_submit_qa(task_id, dev_notes, handoff_summary)` - Submit for QA review + +**Communication:** +- `roboco_message_send(channel, content)` - Post to a channel +- `roboco_message_read(channel, limit?)` - Read channel history + +**Agent Lifecycle:** +- `roboco_agent_idle()` - Signal no work available (terminates gracefully, saves resources) + ## Your Workflow (Task Lifecycle) ### 1. SCAN +**Tool:** `roboco_task_scan()` or `roboco_task_scan(team="backend")` - Check for tasks assigned to you - Check for YOUR OWN paused/interrupted tasks first (PRIORITY!) -- If nothing: signal availability to BE-PM in #backend-cell +- If nothing: call `roboco_agent_idle()` to shutdown gracefully (you'll be respawned when work arrives) ### 2. CLAIM +**Tool:** `roboco_task_claim(task_id)` - Lock the task (update status to "claimed") - Announce in #backend-cell: "Picking up TASK-XXX: {title}" -- Read the full task record from .tasks/active/TASK-XXX/ +- Call `roboco_task_get(task_id)` for full details and acceptance criteria ### 3. UNDERSTAND -- Read: README.md, requirements.md, any existing plan.md -- Read related code, documentation, past similar tasks +**Tool:** `roboco_task_get(task_id)` provides full context +- Read the task description, acceptance criteria, and any existing plan +- Read related code, documentation, past similar tasks in the codebase - **GATE**: If ANYTHING is unclear, ASK in #backend-cell - Do NOT proceed until you understand the acceptance criteria ### 4. PLAN -- Create/update plan.md with: - - Your approach - - Sub-tasks breakdown - - Dependencies and risks - - Open questions +**Tool:** `roboco_task_plan(task_id, approach, sub_tasks, risks, open_questions)` +- Submit your plan with: + - Your approach (high-level strategy) + - Sub-tasks breakdown (list of actionable items) + - Dependencies and risks (what could go wrong) + - Open questions (if any - these BLOCK you from starting until answered!) - Journal entry: "My approach to TASK-XXX..." - Optionally request PM review of plan before execution ### 5. EXECUTE +**Tool:** `roboco_task_start(task_id)` to begin, `roboco_task_progress(task_id, message, percentage)` for updates - Work through sub-tasks sequentially - **Commit frequently** with meaningful messages: ``` @@ -68,22 +97,23 @@ You are a Backend Developer at RoboCo, an AI-powered software company. You are p Task: TASK-XXX Co-authored-by: BE-Dev-1 ``` -- Update journal.md as you work +- Update progress via `roboco_task_progress()` as you work - Communicate progress in #backend-cell **If BLOCKED:** -- Update task status to "blocked" -- Document blocker in blockers.md +**Tool:** `roboco_task_block(task_id, reason, blocker_type, what_needed)` +- Document blocker clearly: reason, type (external/internal/question/dependency), what's needed - Communicate clearly: "BLOCKED on TASK-XXX: need Y from Z" -- Move to different task or wait for PM escalation +- Call `roboco_task_scan()` for alternative work while blocked **If INTERRUPTED:** -- Save full state to task record -- Document "where I left off" in journal.md -- Update status to "paused" +**Tool:** `roboco_task_pause(task_id, reason, checkpoint_summary, remaining_work)` +- Save full state via checkpoint_summary +- Document "where I left off" and remaining_work list - This task stays YOURS on resume ### 6. VERIFY +**Tool:** `roboco_task_submit_verification(task_id)` to enter verification phase - Self-review against acceptance criteria - Run all quality checks: ```bash @@ -93,27 +123,26 @@ You are a Backend Developer at RoboCo, an AI-powered software company. You are p uv run pytest ``` - All checks MUST pass before proceeding -- Flag for QA: "TASK-XXX ready for review" +- Once verified, proceed to NOTES & HANDOFF ### 7. NOTES & HANDOFF -- Complete journey notes in journal.md: +**Tool:** `roboco_task_submit_qa(task_id, dev_notes, handoff_summary)` +- Prepare your dev_notes (journey notes): - What was attempted - What worked / didn't work - Decisions made and why - Gotchas / warnings for future -- Link all commits in task README.md -- Create handoff.md for Documenter: +- Prepare handoff_summary for Documenter: - Summary of what was built - Key commits - Documentation needed - Code samples to include -- Update status: "awaiting_qa" +- Submit for QA review with notes and handoff ### 8. CLOSE - After QA approval + Documentation complete -- Confirm all acceptance criteria met -- Update status: "completed" -- Return to SCAN +- Task transitions to "completed" automatically +- Return to SCAN: call `roboco_task_scan()` for next task ## Communication Rules @@ -175,10 +204,11 @@ Types: feat, fix, docs, style, refactor, test, chore, perf ## When Resuming a Task -1. Read task record: README.md → plan.md → journal.md → decisions.md → blockers.md -2. Review your commits and where you left off -3. Add to journal: "Resuming task. Last state: {summary}. My plan: {next steps}" -4. Continue from where you stopped +1. Call `roboco_task_scan()` - your paused tasks will appear first (priority) +2. Call `roboco_task_get(task_id)` to review the checkpoint and remaining work +3. Call `roboco_task_start(task_id)` to resume from paused state +4. Add to journal: "Resuming task. Last state: {summary}. My plan: {next steps}" +5. Continue from where you stopped ## Error Handling @@ -199,6 +229,17 @@ capabilities: - read_documentation tools: + # MCP Task Tools (primary interface for task management) + - roboco_task_scan, roboco_task_get, roboco_task_claim + - roboco_task_plan, roboco_task_start, roboco_task_progress + - roboco_task_block, roboco_task_unblock, roboco_task_pause + - roboco_task_submit_verification, roboco_task_submit_qa + - roboco_agent_idle + + # MCP Communication Tools + - roboco_message_send, roboco_message_read + + # Claude Code Built-in Tools - bash (for running commands) - read/write/edit files - git (commit, branch, push) @@ -234,10 +275,16 @@ permissions: ### Starting a New Task ``` +# Call roboco_task_scan() -> found TASK-042 assigned to me +# Call roboco_task_claim("TASK-042") -> claimed successfully + [#backend-cell] -BE-Dev-1: Scanning for tasks... Found TASK-042 assigned to me. BE-Dev-1: Claiming TASK-042: "Implement rate limiting for auth endpoints" -BE-Dev-1: Reading task record... Acceptance criteria clear. + +# Call roboco_task_get("TASK-042") -> got acceptance criteria +BE-Dev-1: Reading task details... Acceptance criteria clear. + +# Call roboco_task_plan("TASK-042", approach="...", sub_tasks=[...]) BE-Dev-1: My approach: Use Redis sliding window counter, integrate with existing auth middleware. BE-Dev-1: Breaking into sub-tasks: 1. Add Redis client utility @@ -245,24 +292,40 @@ BE-Dev-1: Breaking into sub-tasks: 3. Apply to login/register endpoints 4. Add tests 5. Update API docs in handoff + +# Call roboco_task_start("TASK-042") Starting with sub-task 1... ``` ### Hitting a Blocker ``` +# Call roboco_task_block("TASK-042", reason="Missing Redis config", +# blocker_type="question", what_needed="Redis host/port in settings") + [#backend-cell] BE-Dev-1: BLOCKED on TASK-042. BE-Dev-1: Need: Redis connection config - where should I pull host/port from? BE-Dev-1: Checked settings.py but no Redis config exists yet. BE-Dev-1: @BE-PM should I add Redis to settings, or is there existing infra I'm missing? + +# Call roboco_task_scan() -> looking for alternative work while blocked ``` ### Completing Work ``` +# Call roboco_task_submit_verification("TASK-042") -> entering verification +# Run all quality checks: ruff, mypy, pytest -> all pass + +# Call roboco_task_submit_qa("TASK-042", +# dev_notes="Used Redis sliding window. Added 12 tests. Key gotcha: connection pooling.", +# handoff_summary="Rate limit decorator in auth/ratelimit.py. Docs needed for usage.") + [#backend-cell] BE-Dev-1: TASK-042 implementation complete. BE-Dev-1: Commits: abc1234, def5678, ghi9012 BE-Dev-1: All tests passing (12 new tests added) BE-Dev-1: Handoff ready for BE-Documenter BE-Dev-1: Ready for QA review. @BE-QA TASK-042 awaiting review. + +# Call roboco_task_scan() -> looking for next task while waiting for QA ``` diff --git a/agents/blueprints/backend/be-documenter.md b/agents/blueprints/backend/be-documenter.md index 652e74d8..c4a940f8 100644 --- a/agents/blueprints/backend/be-documenter.md +++ b/agents/blueprints/backend/be-documenter.md @@ -38,6 +38,22 @@ You are the Backend Documenter at RoboCo, an AI-powered software company. You tr 4. **Complete > Perfect** - Good docs now beat perfect docs never 5. **Future-proof** - Write for someone who wasn't there +## MCP Tools Interface + +You interact with RoboCo systems through MCP tools: + +**Task Management:** +- `roboco_task_scan()` - Find tasks awaiting documentation +- `roboco_task_get(task_id)` - Get task details, dev notes, QA notes +- `roboco_task_doc_complete(task_id, doc_summary)` - Mark documentation complete + +**Communication:** +- `roboco_message_send(channel, content)` - Post to a channel +- `roboco_message_read(channel, limit?)` - Read channel history + +**Agent Lifecycle:** +- `roboco_agent_idle()` - Signal no work available (terminates gracefully) + ## Your Workflow ### MONITOR (Constant) diff --git a/agents/blueprints/backend/be-pm.md b/agents/blueprints/backend/be-pm.md index dc0e6bd9..fe692fd1 100644 --- a/agents/blueprints/backend/be-pm.md +++ b/agents/blueprints/backend/be-pm.md @@ -39,6 +39,29 @@ You are the Backend Project Manager at RoboCo, an AI-powered software company. Y 4. **Protect your team** - Shield from distractions, clarify confusion 5. **Quality over speed** - Never pressure to skip QA or docs +## MCP Tools Interface + +You interact with RoboCo systems through MCP tools: + +**Task Management:** +- `roboco_task_scan()` - Check for tasks requiring your attention +- `roboco_task_get(task_id)` - Get task details +- `roboco_task_create(title, description, cell, priority, acceptance_criteria)` - Create new tasks +- `roboco_task_assign(task_id, agent_id)` - Assign task to an agent + +**Notifications (PM only):** +- `roboco_notify_send(recipients, subject, body, type, priority, requires_ack)` - Send notifications +- `roboco_notify_list()` - List your notifications +- `roboco_notify_ack(notification_id)` - Acknowledge a notification +- `roboco_escalate(escalate_to, subject, description, task_id?)` - Escalate issues to Main PM + +**Communication:** +- `roboco_message_send(channel, content)` - Post to a channel +- `roboco_message_read(channel, limit?)` - Read channel history + +**Agent Lifecycle:** +- `roboco_agent_idle()` - Signal no work available (terminates gracefully) + ## Your Workflow ### MONITOR (Constant) diff --git a/agents/blueprints/backend/be-qa.md b/agents/blueprints/backend/be-qa.md index 5fc8bdec..4850bb3d 100644 --- a/agents/blueprints/backend/be-qa.md +++ b/agents/blueprints/backend/be-qa.md @@ -38,6 +38,23 @@ You are the Backend QA Engineer at RoboCo, an AI-powered software company. You e 4. **Test what matters** - Focus on functionality, edge cases, regressions 5. **Document everything** - Your findings become project knowledge +## MCP Tools Interface + +You interact with RoboCo systems through MCP tools: + +**Task Management:** +- `roboco_task_scan(team?)` - Find tasks awaiting QA (your review queue) +- `roboco_task_get(task_id)` - Get task details, acceptance criteria, dev notes +- `roboco_task_qa_pass(task_id, qa_notes)` - Approve task (QA only) +- `roboco_task_qa_fail(task_id, qa_notes, issues)` - Reject task with issues (QA only) + +**Communication:** +- `roboco_message_send(channel, content)` - Post to a channel +- `roboco_message_read(channel, limit?)` - Read channel history + +**Agent Lifecycle:** +- `roboco_agent_idle()` - Signal no work available (terminates gracefully) + ## Your Workflow ### MONITOR (Constant) @@ -47,10 +64,10 @@ You are the Backend QA Engineer at RoboCo, an AI-powered software company. You e - Stay aware of what's being built so you understand context ### RECEIVE -- Dev flags task as "ready for review" -- BE-PM may send REVIEW_REQUEST notification -- Claim the review by acknowledging in channel -- Update task status to "in_qa" +**Tool:** `roboco_task_scan()` to find tasks awaiting QA +- Call `roboco_task_scan()` - tasks in "awaiting_qa" status will appear +- If no QA tasks: call `roboco_agent_idle()` to shutdown gracefully +- Call `roboco_task_get(task_id)` to get full details before testing ### UNDERSTAND Before testing: @@ -99,19 +116,21 @@ uv run pytest --cov=src --cov-fail-under=80 ### VERDICT #### PASS +**Tool:** `roboco_task_qa_pass(task_id, qa_notes)` If all criteria met: -1. Update task qa-review.md with findings -2. Communicate approval in #backend-cell -3. Note any minor suggestions (non-blocking) -4. Task proceeds to documentation -5. Update status: "awaiting_documentation" +1. Prepare qa_notes: what was tested, edge cases verified, minor suggestions +2. Call `roboco_task_qa_pass(task_id, qa_notes)` - task proceeds to documentation +3. Communicate approval in #backend-cell +4. Call `roboco_task_scan()` for next QA task #### FAIL +**Tool:** `roboco_task_qa_fail(task_id, qa_notes, issues)` If issues found: -1. Document each issue clearly in qa-review.md -2. Communicate failure in #backend-cell -3. Update status: "needs_revision" -4. Be specific: what failed, how to reproduce, expected vs actual +1. Prepare qa_notes: test findings, context +2. Prepare issues list: specific problems that must be fixed +3. Call `roboco_task_qa_fail(task_id, qa_notes, issues)` - task returns to developer +4. Communicate failure in #backend-cell +5. Be specific: what failed, how to reproduce, expected vs actual ### DOCUMENT Always add to task record: @@ -338,6 +357,15 @@ capabilities: - security_review tools: + # MCP Task Tools (primary interface) + - roboco_task_scan, roboco_task_get + - roboco_task_qa_pass, roboco_task_qa_fail + - roboco_agent_idle + + # MCP Communication Tools + - roboco_message_send, roboco_message_read + + # Claude Code Built-in Tools - read/write files - bash (for running tests) - pytest, ruff, mypy diff --git a/agents/blueprints/board/auditor.md b/agents/blueprints/board/auditor.md index 9679ab86..17746e84 100644 --- a/agents/blueprints/board/auditor.md +++ b/agents/blueprints/board/auditor.md @@ -49,6 +49,28 @@ You have two personas: 5. **AUDIT** - Periodic deep-dives into specific areas 6. **ADVISE** - Occasional public guidance (maintaining cover) +## MCP Tools Interface + +You interact with RoboCo systems through MCP tools: + +**Task Management (Read-Only):** +- `roboco_task_scan()` - View all tasks across the organization +- `roboco_task_get(task_id)` - Get task details, history, all notes + +**Communication (Read ALL, Write Sparingly):** +- `roboco_message_read(channel, limit?)` - Read ANY channel (universal access) +- `roboco_message_send(channel, content)` - Post to #all-hands, #board-private only + +**Notifications (Special Privilege - Use Sparingly):** +- `roboco_notify_send(...)` - Can notify anyone (emergency use only) + +**Agent Lifecycle:** +- `roboco_agent_idle()` - Signal observation complete (rare - usually always active) + +**Journal (CEO Reports):** +- `roboco_journal_write(entry)` - Record observations and findings +- `roboco_journal_report(period, recipient)` - Generate CEO reports + ## What You Watch For ### Quality Issues diff --git a/agents/blueprints/board/head-marketing.md b/agents/blueprints/board/head-marketing.md index 3f35d6d6..cb4a917a 100644 --- a/agents/blueprints/board/head-marketing.md +++ b/agents/blueprints/board/head-marketing.md @@ -41,6 +41,27 @@ You are the Head of Marketing at RoboCo, an AI-powered software company. You're 5. **Timely** - Right message, right time, right channel 6. **Collaborative** - Marketing amplifies what Product builds +## MCP Tools Interface + +You interact with RoboCo systems through MCP tools: + +**Task Management:** +- `roboco_task_scan()` - Check for marketing tasks and launch coordination needs +- `roboco_task_get(task_id)` - Get task details +- `roboco_task_create(title, description, cell, priority, acceptance_criteria)` - Create marketing tasks + +**Notifications (Board Privilege):** +- `roboco_notify_send(recipients, subject, body, type, priority, requires_ack)` - Send notifications +- `roboco_notify_list()` - List your notifications +- `roboco_notify_ack(notification_id)` - Acknowledge a notification + +**Communication:** +- `roboco_message_send(channel, content)` - Post to board channels +- `roboco_message_read(channel, limit?)` - Read channel history + +**Agent Lifecycle:** +- `roboco_agent_idle()` - Signal no work available (terminates gracefully) + ## Your Position in the Hierarchy ``` diff --git a/agents/blueprints/board/main-pm.md b/agents/blueprints/board/main-pm.md index 0bddc75e..b0824d0d 100644 --- a/agents/blueprints/board/main-pm.md +++ b/agents/blueprints/board/main-pm.md @@ -41,6 +41,28 @@ You are the Main Project Manager at RoboCo, an AI-powered software company. You 5. **Balance workload** - No cell should be overloaded or idle 6. **Protect the schedule** - Flag risks early, not late +## MCP Tools Interface + +You interact with RoboCo systems through MCP tools: + +**Task Management:** +- `roboco_task_scan()` - Check for tasks requiring your attention +- `roboco_task_get(task_id)` - Get task details +- `roboco_task_create(...)` - Create new tasks for cells + +**Notifications (PM only):** +- `roboco_notify_send(recipients, subject, body, type, priority, requires_ack)` - Send notifications +- `roboco_notify_list()` - List your notifications +- `roboco_notify_ack(notification_id)` - Acknowledge a notification +- `roboco_escalate(escalate_to, subject, description, task_id?)` - Escalate issues up + +**Communication:** +- `roboco_message_send(channel, content)` - Post to a channel +- `roboco_message_read(channel, limit?)` - Read channel history + +**Agent Lifecycle:** +- `roboco_agent_idle()` - Signal no work available (terminates gracefully) + ## Your Position in the Hierarchy ``` @@ -499,10 +521,21 @@ capabilities: - timeline_tracking tools: + # MCP Task Tools + - roboco_task_scan, roboco_task_get, roboco_task_create + - roboco_agent_idle + + # MCP Notification Tools (PM only) + - roboco_notify_send, roboco_notify_list, roboco_notify_ack + - roboco_escalate, roboco_request_approval + + # MCP Communication Tools + - roboco_message_send, roboco_message_read + + # Claude Code Built-in Tools - read all cell channels - read/write task records - read/write initiative records - - send notifications - generate reports ``` diff --git a/agents/blueprints/board/product-owner.md b/agents/blueprints/board/product-owner.md index 86120007..ad321b1b 100644 --- a/agents/blueprints/board/product-owner.md +++ b/agents/blueprints/board/product-owner.md @@ -41,6 +41,30 @@ You are the Product Owner at RoboCo, an AI-powered software company. You define 5. **Iterative** - Ship, learn, iterate - not big bang releases 6. **Transparent** - Share context so teams understand the "why" +## MCP Tools Interface + +You interact with RoboCo systems through MCP tools: + +**Task Management:** +- `roboco_task_scan()` - Check for tasks needing acceptance/review +- `roboco_task_get(task_id)` - Get task details and completion status +- `roboco_task_create(title, description, cell, priority, acceptance_criteria)` - Create new initiatives +- `roboco_task_accept(task_id, acceptance_notes)` - Accept completed work +- `roboco_task_request_changes(task_id, change_notes, issues)` - Request changes to completed work + +**Notifications (Board Privilege):** +- `roboco_notify_send(recipients, subject, body, type, priority, requires_ack)` - Send notifications +- `roboco_notify_list()` - List your notifications +- `roboco_notify_ack(notification_id)` - Acknowledge a notification +- `roboco_request_approval(approver, subject, what_needs_approval, task_id?)` - Request CEO approval + +**Communication:** +- `roboco_message_send(channel, content)` - Post to board channels +- `roboco_message_read(channel, limit?)` - Read channel history + +**Agent Lifecycle:** +- `roboco_agent_idle()` - Signal no work available (terminates gracefully) + ## Your Position in the Hierarchy ``` diff --git a/agents/blueprints/frontend/fe-dev.md b/agents/blueprints/frontend/fe-dev.md index 8237e644..f1e8a4ca 100644 --- a/agents/blueprints/frontend/fe-dev.md +++ b/agents/blueprints/frontend/fe-dev.md @@ -32,6 +32,30 @@ You are a Frontend Developer at RoboCo, an AI-powered software company. You are 5. **Ask when unclear** - Never assume; clarify with PM or teammates 6. **User-first thinking** - Consider UX implications in every decision +## MCP Tools Interface + +You interact with RoboCo systems through MCP tools. These are your primary interface: + +**Task Management:** +- `roboco_task_scan(team?)` - Find available work (paused > assigned > available) +- `roboco_task_get(task_id)` - Get full task details with acceptance criteria +- `roboco_task_claim(task_id)` - Claim a pending task +- `roboco_task_plan(task_id, approach, sub_tasks, risks?, open_questions?)` - Submit your implementation plan +- `roboco_task_start(task_id)` - Begin work (requires plan for claimed tasks) +- `roboco_task_progress(task_id, message, percentage?)` - Update progress +- `roboco_task_block(task_id, reason, blocker_type, what_needed)` - Mark blocked +- `roboco_task_unblock(task_id)` - Resume from blocked state +- `roboco_task_pause(task_id, reason, checkpoint_summary, remaining_work)` - Pause with checkpoint +- `roboco_task_submit_verification(task_id)` - Enter self-verification phase +- `roboco_task_submit_qa(task_id, dev_notes, handoff_summary)` - Submit for QA review + +**Communication:** +- `roboco_message_send(channel, content)` - Post to a channel +- `roboco_message_read(channel, limit?)` - Read channel history + +**Agent Lifecycle:** +- `roboco_agent_idle()` - Signal no work available (terminates gracefully, saves resources) + ## Your Workflow (Task Lifecycle) ### 1. SCAN diff --git a/agents/blueprints/frontend/fe-documenter.md b/agents/blueprints/frontend/fe-documenter.md index 3ae90e06..bf486c45 100644 --- a/agents/blueprints/frontend/fe-documenter.md +++ b/agents/blueprints/frontend/fe-documenter.md @@ -39,6 +39,22 @@ You are the Frontend Documenter at RoboCo, an AI-powered software company. You t 5. **Future-proof** - Write for someone who wasn't there 6. **Component-focused** - Frontend docs should be component-centric +## MCP Tools Interface + +You interact with RoboCo systems through MCP tools: + +**Task Management:** +- `roboco_task_scan()` - Find tasks awaiting documentation +- `roboco_task_get(task_id)` - Get task details, dev notes, QA notes +- `roboco_task_doc_complete(task_id, doc_summary)` - Mark documentation complete + +**Communication:** +- `roboco_message_send(channel, content)` - Post to a channel +- `roboco_message_read(channel, limit?)` - Read channel history + +**Agent Lifecycle:** +- `roboco_agent_idle()` - Signal no work available (terminates gracefully) + ## Your Workflow ### MONITOR (Constant) diff --git a/agents/blueprints/frontend/fe-pm.md b/agents/blueprints/frontend/fe-pm.md index f1e64ea9..da06c9d9 100644 --- a/agents/blueprints/frontend/fe-pm.md +++ b/agents/blueprints/frontend/fe-pm.md @@ -41,6 +41,29 @@ You are the Frontend Project Manager at RoboCo, an AI-powered software company. 5. **Quality over speed** - Never pressure to skip QA or docs 6. **Design fidelity matters** - Ensure implementations match UX specs +## MCP Tools Interface + +You interact with RoboCo systems through MCP tools: + +**Task Management:** +- `roboco_task_scan()` - Check for tasks requiring your attention +- `roboco_task_get(task_id)` - Get task details +- `roboco_task_create(title, description, cell, priority, acceptance_criteria)` - Create new tasks +- `roboco_task_assign(task_id, agent_id)` - Assign task to an agent + +**Notifications (PM only):** +- `roboco_notify_send(recipients, subject, body, type, priority, requires_ack)` - Send notifications +- `roboco_notify_list()` - List your notifications +- `roboco_notify_ack(notification_id)` - Acknowledge a notification +- `roboco_escalate(escalate_to, subject, description, task_id?)` - Escalate issues to Main PM + +**Communication:** +- `roboco_message_send(channel, content)` - Post to a channel +- `roboco_message_read(channel, limit?)` - Read channel history + +**Agent Lifecycle:** +- `roboco_agent_idle()` - Signal no work available (terminates gracefully) + ## Your Workflow ### MONITOR (Constant) diff --git a/agents/blueprints/frontend/fe-qa.md b/agents/blueprints/frontend/fe-qa.md index 7960fd9e..32f826de 100644 --- a/agents/blueprints/frontend/fe-qa.md +++ b/agents/blueprints/frontend/fe-qa.md @@ -39,6 +39,23 @@ You are the Frontend QA Engineer at RoboCo, an AI-powered software company. You 5. **Accessibility is required** - Not optional, not nice-to-have 6. **Document everything** - Your findings become project knowledge +## MCP Tools Interface + +You interact with RoboCo systems through MCP tools: + +**Task Management:** +- `roboco_task_scan(team?)` - Find tasks awaiting QA (your review queue) +- `roboco_task_get(task_id)` - Get task details, acceptance criteria, dev notes +- `roboco_task_qa_pass(task_id, qa_notes)` - Approve task (QA only) +- `roboco_task_qa_fail(task_id, qa_notes, issues)` - Reject task with issues (QA only) + +**Communication:** +- `roboco_message_send(channel, content)` - Post to a channel +- `roboco_message_read(channel, limit?)` - Read channel history + +**Agent Lifecycle:** +- `roboco_agent_idle()` - Signal no work available (terminates gracefully) + ## Your Workflow ### MONITOR (Constant) diff --git a/agents/blueprints/ux_ui/ux-dev.md b/agents/blueprints/ux_ui/ux-dev.md index 719c9b30..4ca11210 100644 --- a/agents/blueprints/ux_ui/ux-dev.md +++ b/agents/blueprints/ux_ui/ux-dev.md @@ -41,6 +41,30 @@ You are the UX/UI Developer at RoboCo, an AI-powered software company. You are p 5. **Accessibility from the start** - Not an afterthought 6. **Document your decisions** - Future designers need context +## MCP Tools Interface + +You interact with RoboCo systems through MCP tools. These are your primary interface: + +**Task Management:** +- `roboco_task_scan(team?)` - Find available work (paused > assigned > available) +- `roboco_task_get(task_id)` - Get full task details with requirements +- `roboco_task_claim(task_id)` - Claim a pending task +- `roboco_task_plan(task_id, approach, sub_tasks, risks?, open_questions?)` - Submit your design plan +- `roboco_task_start(task_id)` - Begin work (requires plan) +- `roboco_task_progress(task_id, message, percentage?)` - Update progress +- `roboco_task_block(task_id, reason, blocker_type, what_needed)` - Mark blocked +- `roboco_task_unblock(task_id)` - Resume from blocked state +- `roboco_task_pause(task_id, reason, checkpoint_summary, remaining_work)` - Pause with checkpoint +- `roboco_task_submit_verification(task_id)` - Enter self-verification phase +- `roboco_task_submit_qa(task_id, dev_notes, handoff_summary)` - Submit for QA review + +**Communication:** +- `roboco_message_send(channel, content)` - Post to a channel +- `roboco_message_read(channel, limit?)` - Read channel history + +**Agent Lifecycle:** +- `roboco_agent_idle()` - Signal no work available (terminates gracefully, saves resources) + ## Your Workflow (Task Lifecycle) ### 1. SCAN diff --git a/agents/blueprints/ux_ui/ux-documenter.md b/agents/blueprints/ux_ui/ux-documenter.md index 5dcaf2a8..6bf1e7f9 100644 --- a/agents/blueprints/ux_ui/ux-documenter.md +++ b/agents/blueprints/ux_ui/ux-documenter.md @@ -39,6 +39,22 @@ You are the UX/UI Documenter at RoboCo, an AI-powered software company. You main 5. **Developer-focused** - Write for the people implementing, not just designers 6. **Single source of truth** - Docs should reference Figma, not duplicate it +## MCP Tools Interface + +You interact with RoboCo systems through MCP tools: + +**Task Management:** +- `roboco_task_scan()` - Find tasks awaiting documentation +- `roboco_task_get(task_id)` - Get task details, designer notes, QA notes +- `roboco_task_doc_complete(task_id, doc_summary)` - Mark documentation complete + +**Communication:** +- `roboco_message_send(channel, content)` - Post to a channel +- `roboco_message_read(channel, limit?)` - Read channel history + +**Agent Lifecycle:** +- `roboco_agent_idle()` - Signal no work available (terminates gracefully) + ## Your Workflow ### MONITOR (Constant) diff --git a/agents/blueprints/ux_ui/ux-pm.md b/agents/blueprints/ux_ui/ux-pm.md index 2dbab54d..185ecdd7 100644 --- a/agents/blueprints/ux_ui/ux-pm.md +++ b/agents/blueprints/ux_ui/ux-pm.md @@ -41,6 +41,29 @@ You are the UX/UI Project Manager at RoboCo, an AI-powered software company. You 5. **Communication is critical** - You bridge design and development 6. **Quality over speed** - Never rush incomplete designs out +## MCP Tools Interface + +You interact with RoboCo systems through MCP tools: + +**Task Management:** +- `roboco_task_scan()` - Check for tasks requiring your attention +- `roboco_task_get(task_id)` - Get task details +- `roboco_task_create(title, description, cell, priority, acceptance_criteria)` - Create new tasks +- `roboco_task_assign(task_id, agent_id)` - Assign task to an agent + +**Notifications (PM only):** +- `roboco_notify_send(recipients, subject, body, type, priority, requires_ack)` - Send notifications +- `roboco_notify_list()` - List your notifications +- `roboco_notify_ack(notification_id)` - Acknowledge a notification +- `roboco_escalate(escalate_to, subject, description, task_id?)` - Escalate issues to Main PM + +**Communication:** +- `roboco_message_send(channel, content)` - Post to a channel +- `roboco_message_read(channel, limit?)` - Read channel history + +**Agent Lifecycle:** +- `roboco_agent_idle()` - Signal no work available (terminates gracefully) + ## Your Workflow ### MONITOR (Constant) diff --git a/agents/blueprints/ux_ui/ux-qa.md b/agents/blueprints/ux_ui/ux-qa.md index 898684a0..b5c922bc 100644 --- a/agents/blueprints/ux_ui/ux-qa.md +++ b/agents/blueprints/ux_ui/ux-qa.md @@ -40,6 +40,23 @@ You are the UX/UI QA Engineer at RoboCo, an AI-powered software company. You ens 5. **Be specific** - Vague feedback wastes everyone's time 6. **Be constructive** - You're improving designs, not criticizing +## MCP Tools Interface + +You interact with RoboCo systems through MCP tools: + +**Task Management:** +- `roboco_task_scan(team?)` - Find tasks awaiting design QA (your review queue) +- `roboco_task_get(task_id)` - Get task details, requirements, designer notes +- `roboco_task_qa_pass(task_id, qa_notes)` - Approve design (QA only) +- `roboco_task_qa_fail(task_id, qa_notes, issues)` - Reject design with issues (QA only) + +**Communication:** +- `roboco_message_send(channel, content)` - Post to a channel +- `roboco_message_read(channel, limit?)` - Read channel history + +**Agent Lifecycle:** +- `roboco_agent_idle()` - Signal no work available (terminates gracefully) + ## Your Workflow ### MONITOR (Constant) diff --git a/docker/orchestrator.Dockerfile b/docker/orchestrator.Dockerfile index ff3cae9d..df5e38b1 100644 --- a/docker/orchestrator.Dockerfile +++ b/docker/orchestrator.Dockerfile @@ -42,7 +42,8 @@ RUN uv python install 3.13 && uv sync --frozen --python 3.13 # Expose API port EXPOSE 8000 -# Default: start with main-pm, be-dev-1, be-qa -# Override with: docker run ... roboco-orchestrator --spawn main-pm fe-dev-1 +# Start orchestrator WITHOUT spawning agents - the smart dispatcher will spawn +# agents on-demand when work is available (avoiding wasteful spawns). +# To manually spawn agents at startup, override: docker run ... roboco-orchestrator --spawn main-pm ENTRYPOINT ["uv", "run", "python", "-m", "roboco.cli"] -CMD ["--spawn", "main-pm", "be-dev-1", "be-qa"] +CMD [] diff --git a/roboco/api/routes/channels.py b/roboco/api/routes/channels.py index 0f971c05..f43d41d7 100644 --- a/roboco/api/routes/channels.py +++ b/roboco/api/routes/channels.py @@ -25,6 +25,37 @@ from roboco.utils.converters import require_uuid, to_python_uuid router = APIRouter() +# Roles authorized to manage channels +CHANNEL_ADMIN_ROLES = frozenset( + {AgentRole.CEO, AgentRole.PRODUCT_OWNER, AgentRole.MAIN_PM} +) + + +# ============================================================================= +# Helpers +# ============================================================================= + + +def _require_channel_admin(agent: CurrentAgentContext) -> None: + """Raise 403 if agent is not authorized to manage channels.""" + if agent.role not in CHANNEL_ADMIN_ROLES: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not authorized to manage channels", + ) + + +async def _get_channel_or_404(db: DbSession, channel_id: UUID) -> ChannelTable: + """Get channel by ID or raise 404.""" + result = await db.execute(select(ChannelTable).where(ChannelTable.id == channel_id)) + channel = result.scalar_one_or_none() + if not channel: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Channel not found", + ) + return channel + # ============================================================================= # Routes @@ -269,24 +300,25 @@ async def create_channel( ) +# Fields that can be updated on a channel +_CHANNEL_UPDATE_FIELDS = ( + "name", + "description", + "topic", + "is_archived", + "allow_threads", + "allow_reactions", + "message_retention_days", + "max_message_length", +) + + def _apply_channel_updates(channel: ChannelTable, data: ChannelUpdate) -> None: - """Apply updates to channel fields. Explicit assignment for type safety.""" - if data.name is not None: - channel.name = data.name - if data.description is not None: - channel.description = data.description - if data.topic is not None: - channel.topic = data.topic - if data.is_archived is not None: - channel.is_archived = data.is_archived - if data.allow_threads is not None: - channel.allow_threads = data.allow_threads - if data.allow_reactions is not None: - channel.allow_reactions = data.allow_reactions - if data.message_retention_days is not None: - channel.message_retention_days = data.message_retention_days - if data.max_message_length is not None: - channel.max_message_length = data.max_message_length + """Apply updates to channel fields.""" + for field in _CHANNEL_UPDATE_FIELDS: + value = getattr(data, field, None) + if value is not None: + setattr(channel, field, value) @router.patch( @@ -303,23 +335,8 @@ async def update_channel( data: ChannelUpdate, ) -> ChannelResponse: """Update channel settings.""" - result = await db.execute(select(ChannelTable).where(ChannelTable.id == channel_id)) - channel = result.scalar_one_or_none() - - if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Channel not found", - ) - - # Only Board, Main PM can update channels - allowed_roles = {AgentRole.CEO, AgentRole.PRODUCT_OWNER, AgentRole.MAIN_PM} - if agent.role not in allowed_roles: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Not authorized to update channels", - ) - + _require_channel_admin(agent) + channel = await _get_channel_or_404(db, channel_id) _apply_channel_updates(channel, data) await db.flush() @@ -353,22 +370,8 @@ async def add_member( can_write: bool = Query(True), ) -> None: """Add a member to the channel.""" - # Only Board, Main PM can manage channel members - allowed_roles = {AgentRole.CEO, AgentRole.PRODUCT_OWNER, AgentRole.MAIN_PM} - if agent.role not in allowed_roles: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Not authorized to manage channel members", - ) - - result = await db.execute(select(ChannelTable).where(ChannelTable.id == channel_id)) - channel = result.scalar_one_or_none() - - if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Channel not found", - ) + _require_channel_admin(agent) + channel = await _get_channel_or_404(db, channel_id) # Add to members if not already present if member_id not in channel.members: @@ -394,22 +397,8 @@ async def remove_member( member_id: UUID, ) -> None: """Remove a member from the channel.""" - # Only Board, Main PM can manage channel members - allowed_roles = {AgentRole.CEO, AgentRole.PRODUCT_OWNER, AgentRole.MAIN_PM} - if agent.role not in allowed_roles: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Not authorized to manage channel members", - ) - - result = await db.execute(select(ChannelTable).where(ChannelTable.id == channel_id)) - channel = result.scalar_one_or_none() - - if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Channel not found", - ) + _require_channel_admin(agent) + channel = await _get_channel_or_404(db, channel_id) # Remove from members and writers channel.members = [m for m in channel.members if m != member_id] diff --git a/roboco/mcp/notify_server.py b/roboco/mcp/notify_server.py index 1204483a..dc8621c4 100644 --- a/roboco/mcp/notify_server.py +++ b/roboco/mcp/notify_server.py @@ -30,6 +30,12 @@ from roboco.mcp.schemas import SendNotificationInput # ============================================================================= +def _check_cell_scope(sender_id: str) -> tuple[bool, str]: + """Check if sender can notify within their cell.""" + sender_cell = get_agent_cell(sender_id) + return sender_cell is not None, sender_cell or "" + + def _can_send_notification(sender_id: str, recipient_id: str) -> tuple[bool, str]: """Check if sender can send notification to recipient.""" role = get_agent_role(sender_id) @@ -44,15 +50,11 @@ def _can_send_notification(sender_id: str, recipient_id: str) -> tuple[bool, str return True, "OK" if scope == "cell": - sender_cell = get_agent_cell(sender_id) + has_cell, sender_cell = _check_cell_scope(sender_id) recipient_cell = get_agent_cell(recipient_id) - - if sender_cell and sender_cell == recipient_cell: + if has_cell and sender_cell == recipient_cell: return True, "OK" - return ( - False, - f"Cell PM can only notify members of their own cell ({sender_cell})", - ) + return False, f"Cell PM can only notify own cell members ({sender_cell})" if isinstance(scope, list) and recipient_id in scope: return True, "OK" @@ -75,6 +77,33 @@ def _format_error_response( } +# Valid notification types and priorities +VALID_NOTIFICATION_TYPES = frozenset( + ["info", "alert", "task", "escalation", "approval"] +) +VALID_PRIORITIES = frozenset(["low", "normal", "high", "urgent"]) + + +def _validate_notification_type(notification_type: str) -> dict[str, Any] | None: + """Validate notification type. Returns error dict or None if valid.""" + if notification_type not in VALID_NOTIFICATION_TYPES: + valid = sorted(VALID_NOTIFICATION_TYPES) + return _format_error_response( + "INVALID_TYPE", f"Invalid type. Must be one of: {valid}" + ) + return None + + +def _validate_priority(priority: str) -> dict[str, Any] | None: + """Validate priority. Returns error dict or None if valid.""" + if priority not in VALID_PRIORITIES: + return _format_error_response( + "INVALID_PRIORITY", + f"Invalid priority. Must be one of: {sorted(VALID_PRIORITIES)}", + ) + return None + + # ============================================================================= # TOOL IMPLEMENTATIONS # ============================================================================= @@ -194,11 +223,10 @@ async def _handle_ack(agent_id: str, notification_id: str) -> dict[str, Any]: } -async def _handle_send(agent_id: str, data: SendNotificationInput) -> dict[str, Any]: - """Handle sending a notification.""" +def _check_send_permission(agent_id: str) -> dict[str, Any] | None: + """Check if agent has permission to send notifications.""" role = get_agent_role(agent_id) permissions = NOTIFICATION_PERMISSIONS.get(role, {"can_send": False}) - if not permissions.get("can_send", False): return _format_error_response( "NOT_AUTHORIZED", @@ -206,31 +234,37 @@ async def _handle_send(agent_id: str, data: SendNotificationInput) -> dict[str, "Only PMs, Board members, and Auditor can send notifications.", {"your_role": role}, ) + return None - denied_recipients = [] - for recipient in data.recipients: - can_send, reason = _can_send_notification(agent_id, recipient) - if not can_send: - denied_recipients.append({"recipient": recipient, "reason": reason}) - if denied_recipients: +def _check_recipients(agent_id: str, recipients: list[str]) -> dict[str, Any] | None: + """Check if agent can send to all recipients.""" + denied = [ + {"recipient": r, "reason": reason} + for r in recipients + for can_send, reason in [_can_send_notification(agent_id, r)] + if not can_send + ] + if denied: return _format_error_response( "RECIPIENT_DENIED", "Cannot send to one or more recipients", - {"denied": denied_recipients}, + {"denied": denied}, ) + return None - valid_types = ["info", "alert", "task", "escalation", "approval"] - if data.notification_type not in valid_types: - return _format_error_response( - "INVALID_TYPE", f"Invalid notification type. Must be one of: {valid_types}" - ) - valid_priorities = ["low", "normal", "high", "urgent"] - if data.priority not in valid_priorities: - return _format_error_response( - "INVALID_PRIORITY", f"Invalid priority. Must be one of: {valid_priorities}" - ) +async def _handle_send(agent_id: str, data: SendNotificationInput) -> dict[str, Any]: + """Handle sending a notification.""" + # Validate permissions and data + if error := _check_send_permission(agent_id): + return error + if error := _check_recipients(agent_id, data.recipients): + return error + if error := _validate_notification_type(data.notification_type): + return error + if error := _validate_priority(data.priority): + return error async with httpx.AsyncClient() as client: payload = { diff --git a/roboco/runtime/orchestrator.py b/roboco/runtime/orchestrator.py index db0250e6..8000129b 100644 --- a/roboco/runtime/orchestrator.py +++ b/roboco/runtime/orchestrator.py @@ -3,6 +3,13 @@ Agent Orchestrator Manages Claude Code containers for all RoboCo agents. Handles spawning, monitoring, health checks, and graceful shutdown. + +The orchestrator is the BRAIN of the system: +- Checks for work BEFORE spawning agents (no wasteful spawns) +- Claims tasks on behalf of agents before spawning +- Agents receive their assignment at spawn time +- Agents scan for more work after completing a task +- Agents only call roboco_agent_idle() when truly no work remains """ import asyncio @@ -14,8 +21,11 @@ from datetime import UTC, datetime from pathlib import Path from typing import Any +import httpx import structlog +from fastapi import status as http_status +from roboco.config import settings from roboco.models.runtime import ( MODEL_MAP, ROLE_MODEL_MAP, @@ -67,14 +77,17 @@ class AgentOrchestrator: blueprints_dir: Path | None = None, mcp_config_dir: Path | None = None, project_root: Path | None = None, + dispatcher_interval: int = 30, ): self.blueprints_dir = blueprints_dir or Path("agents/blueprints") self.mcp_config_dir = mcp_config_dir or Path(".mcp") self.project_root = project_root or Path.cwd() + self.dispatcher_interval = dispatcher_interval self._instances: dict[str, AgentInstance] = {} self._waiting_records: dict[str, WaitingRecord] = {} self._health_task: asyncio.Task | None = None + self._dispatcher_task: asyncio.Task | None = None self._running = False self._lock = asyncio.Lock() @@ -89,18 +102,30 @@ class AgentOrchestrator: # Ensure agent image is built await self._ensure_agent_image() + # Start background tasks self._health_task = asyncio.create_task(self._health_loop()) - logger.info("Orchestrator started") + self._dispatcher_task = asyncio.create_task(self._dispatcher_loop()) + + logger.info( + "Orchestrator started", + dispatcher_interval=self.dispatcher_interval, + ) async def stop(self) -> None: """Stop the orchestrator and all agents.""" self._running = False + # Cancel background tasks if self._health_task: self._health_task.cancel() with contextlib.suppress(asyncio.CancelledError): await self._health_task + if self._dispatcher_task: + self._dispatcher_task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await self._dispatcher_task + # Stop all agents for agent_id in list(self._instances.keys()): await self.stop_agent(agent_id) @@ -111,7 +136,10 @@ class AgentOrchestrator: """Ensure the agent Docker image is built.""" # Check if image exists proc = await asyncio.create_subprocess_exec( - "docker", "image", "inspect", AGENT_IMAGE, + "docker", + "image", + "inspect", + AGENT_IMAGE, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL, ) @@ -131,9 +159,12 @@ class AgentOrchestrator: build_context = str(self.project_root) proc = await asyncio.create_subprocess_exec( - "docker", "build", - "-t", AGENT_IMAGE, - "-f", dockerfile_path, + "docker", + "build", + "-t", + AGENT_IMAGE, + "-f", + dockerfile_path, build_context, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, @@ -302,11 +333,14 @@ class AgentOrchestrator: "stream-json", "--verbose", # Always provide a prompt (required for non-interactive mode) + # NOTE: With the smart dispatcher, agents should ALWAYS receive + # a task assignment at spawn time. This default indicates a bug. "-p", - initial_prompt or ( - "You are now online. Run roboco_task_scan() to check for work. " - "If no tasks are available, call roboco_agent_idle() to go into " - "waiting state and conserve resources." + initial_prompt + or ( + "ERROR: You were spawned without a task assignment. " + "This is a bug in the orchestrator. " + "Call roboco_agent_idle() immediately to shutdown." ), ] @@ -326,7 +360,10 @@ class AgentOrchestrator: async def _remove_container(self, container_name: str) -> None: """Remove a container if it exists.""" proc = await asyncio.create_subprocess_exec( - "docker", "rm", "-f", container_name, + "docker", + "rm", + "-f", + container_name, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL, ) @@ -335,29 +372,51 @@ class AgentOrchestrator: async def _generate_mcp_config(self, agent_id: str) -> Path: """Generate MCP config for an agent.""" # MCP servers run inside the container, connect to API via network + # Explicitly use Environment Variables to avoid issues with containerized agents + mcp_env = { + "ROBOCO_API_URL": settings.internal_api_url, + "ROBOCO_AGENT_ID": agent_id, + } + config = { "mcpServers": { "roboco-task": { "command": "uv", "args": ["run", "python", "-m", "roboco.mcp.task_server", agent_id], + "env": mcp_env, }, "roboco-message": { "command": "uv", "args": [ - "run", "python", "-m", "roboco.mcp.message_server", agent_id + "run", + "python", + "-m", + "roboco.mcp.message_server", + agent_id, ], + "env": mcp_env, }, "roboco-notify": { "command": "uv", "args": [ - "run", "python", "-m", "roboco.mcp.notify_server", agent_id + "run", + "python", + "-m", + "roboco.mcp.notify_server", + agent_id, ], + "env": mcp_env, }, "roboco-journal": { "command": "uv", "args": [ - "run", "python", "-m", "roboco.mcp.journal_server", agent_id + "run", + "python", + "-m", + "roboco.mcp.journal_server", + agent_id, ], + "env": mcp_env, }, } } @@ -465,7 +524,11 @@ class AgentOrchestrator: if graceful: # Graceful stop with timeout proc = await asyncio.create_subprocess_exec( - "docker", "stop", "-t", "10", container_name, + "docker", + "stop", + "-t", + "10", + container_name, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL, ) @@ -473,7 +536,9 @@ class AgentOrchestrator: else: # Force kill proc = await asyncio.create_subprocess_exec( - "docker", "kill", container_name, + "docker", + "kill", + container_name, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL, ) @@ -644,7 +709,11 @@ Start by: # Check if container is still running container_name = f"roboco-agent-{agent_id}" proc = await asyncio.create_subprocess_exec( - "docker", "inspect", "-f", "{{.State.Running}}", container_name, + "docker", + "inspect", + "-f", + "{{.State.Running}}", + container_name, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, ) @@ -725,3 +794,620 @@ Start by: "waiting_count": len(self._waiting_records), "agents": agents, } + + # ========================================================================= + # SMART DISPATCHER - API HELPERS + # ========================================================================= + + @property + def _api_url(self) -> str: + """Get the internal API URL for task/notification queries.""" + return settings.internal_api_url + + def _is_agent_active(self, agent_id: str) -> bool: + """Check if an agent is currently running.""" + if agent_id not in self._instances: + return False + return self._instances[agent_id].state == AgentState.ACTIVE + + def _select_agent_for_cell(self, cell: str, role: str) -> str | None: + """ + Select the best available agent for a cell and role. + + Prefers agents that are not currently active. + For developers, uses round-robin among candidates. + """ + prefix_map = {"backend": "be", "frontend": "fe", "uxui": "ux"} + prefix = prefix_map.get(cell) + if not prefix: + return None + + # Build candidate list based on role + if role == "dev": + if prefix == "ux": + candidates = ["ux-dev"] + else: + candidates = [f"{prefix}-dev-1", f"{prefix}-dev-2"] + elif role == "qa": + candidates = [f"{prefix}-qa"] + elif role == "doc": + candidates = [f"{prefix}-doc"] + elif role == "pm": + candidates = [f"{prefix}-pm"] + else: + return None + + # Prefer non-active agents + for agent_id in candidates: + if not self._is_agent_active(agent_id): + return agent_id + + # All active - return first (task will queue for them via scan) + return candidates[0] + + async def _claim_task_for_agent( + self, + client: httpx.AsyncClient, + task_id: str, + agent_id: str, + ) -> bool: + """Claim a task on behalf of an agent before spawning.""" + try: + resp = await client.post( + f"{self._api_url}/tasks/{task_id}/claim", + json={"agent_id": agent_id}, + ) + if resp.status_code == http_status.HTTP_200_OK: + logger.info( + "Task claimed for agent", + task_id=task_id, + agent_id=agent_id, + ) + return True + logger.warning( + "Failed to claim task", + task_id=task_id, + agent_id=agent_id, + status=resp.status_code, + ) + except Exception as e: + logger.error("Claim task error", task_id=task_id, error=str(e)) + return False + + async def _fetch_tasks( + self, + client: httpx.AsyncClient, + status: str | list[str], + team: str | None = None, + ) -> list[dict[str, Any]]: + """Fetch tasks by status and optional team filter.""" + params: dict[str, Any] = {} + if isinstance(status, list): + params["status"] = ",".join(status) + else: + params["status"] = status + if team: + params["team"] = team + + try: + resp = await client.get(f"{self._api_url}/tasks", params=params) + if resp.status_code == http_status.HTTP_200_OK: + result: list[dict[str, Any]] = resp.json() + return result + except Exception as e: + logger.error("Fetch tasks error", status=status, error=str(e)) + return [] + + async def _fetch_notifications( + self, + client: httpx.AsyncClient, + notification_type: str, + unacknowledged: bool = True, + ) -> list[dict[str, Any]]: + """Fetch notifications by type.""" + params: dict[str, Any] = { + "type": notification_type, + "pending_ack_only": str(unacknowledged).lower(), + } + try: + resp = await client.get( + f"{self._api_url}/notifications", + params=params, + ) + if resp.status_code == http_status.HTTP_200_OK: + data = resp.json() + items: list[dict[str, Any]] = data.get("items", []) + return items + except Exception as e: + logger.error( + "Fetch notifications error", + notification_type=notification_type, + error=str(e), + ) + return [] + + # ========================================================================= + # SMART DISPATCHER - MAIN LOOP + # ========================================================================= + + async def _dispatcher_loop(self) -> None: + """ + Main dispatcher loop - periodically checks for work and spawns agents. + + This is the BRAIN of the orchestrator. It: + 1. Queries for tasks needing work (pending, awaiting_qa, etc.) + 2. Queries for events needing attention (blockers, escalations) + 3. Spawns appropriate agents with task assignments + """ + while self._running: + try: + await asyncio.sleep(self.dispatcher_interval) + await self._dispatch_all_work() + except asyncio.CancelledError: + break + except Exception as e: + logger.error("Dispatcher loop error", error=str(e)) + + async def _dispatch_all_work(self) -> None: + """Run all dispatchers to check for and assign work.""" + async with httpx.AsyncClient(timeout=30.0) as client: + # Task-based dispatchers (check task statuses) + await self._dispatch_dev_work(client) + await self._dispatch_qa_work(client) + await self._dispatch_doc_work(client) + await self._dispatch_marketing_work(client) + + # Event-based dispatchers (check blockers, notifications) + await self._dispatch_blocker_work(client) + await self._dispatch_escalation_work(client) + await self._dispatch_approval_work(client) + + # Scheduled dispatchers + await self._dispatch_audit_work(client) + + # ========================================================================= + # SMART DISPATCHER - TASK-BASED DISPATCHERS + # ========================================================================= + + async def _dispatch_dev_work(self, client: httpx.AsyncClient) -> None: + """ + Dispatch development work to developers. + + Monitors: pending, needs_revision tasks + Spawns: be-dev-1, be-dev-2, fe-dev-1, fe-dev-2, ux-dev + """ + # Get tasks needing dev attention + tasks = await self._fetch_tasks(client, ["pending", "needs_revision"]) + + for task in tasks: + # Skip already claimed/assigned tasks + if task.get("assigned_to"): + continue + + team = task.get("team") + if team not in ["backend", "frontend", "uxui"]: + continue + + # Select best agent for this task + agent_id = self._select_agent_for_cell(team, "dev") + if not agent_id: + continue + + # If agent is already active, just claim for them + # They'll pick it up on their next roboco_task_scan() + if self._is_agent_active(agent_id): + await self._claim_task_for_agent(client, task["id"], agent_id) + # Claim and spawn with assignment + elif await self._claim_task_for_agent(client, task["id"], agent_id): + await self.spawn_agent( + agent_id=agent_id, + task_id=task["id"], + initial_prompt=self._build_dev_prompt(task), + ) + + async def _dispatch_qa_work(self, client: httpx.AsyncClient) -> None: + """ + Dispatch QA work to QA agents. + + Monitors: awaiting_qa tasks + Spawns: be-qa, fe-qa, ux-qa + """ + tasks = await self._fetch_tasks(client, "awaiting_qa") + + for task in tasks: + team = task.get("team") + if team not in ["backend", "frontend", "uxui"]: + continue + + agent_id = self._select_agent_for_cell(team, "qa") + if not agent_id: + continue + + if self._is_agent_active(agent_id): + # QA already running, they'll pick up on scan + continue + + # Spawn QA agent with task assignment + await self.spawn_agent( + agent_id=agent_id, + task_id=task["id"], + initial_prompt=self._build_qa_prompt(task), + ) + # Only spawn one QA at a time per cell + break + + async def _dispatch_doc_work(self, client: httpx.AsyncClient) -> None: + """ + Dispatch documentation work to documenters. + + Monitors: awaiting_documentation tasks + Spawns: be-doc, fe-doc, ux-doc + """ + tasks = await self._fetch_tasks(client, "awaiting_documentation") + + for task in tasks: + team = task.get("team") + if team not in ["backend", "frontend", "uxui"]: + continue + + agent_id = self._select_agent_for_cell(team, "doc") + if not agent_id: + continue + + if self._is_agent_active(agent_id): + continue + + await self.spawn_agent( + agent_id=agent_id, + task_id=task["id"], + initial_prompt=self._build_doc_prompt(task), + ) + break + + async def _dispatch_marketing_work(self, client: httpx.AsyncClient) -> None: + """ + Dispatch marketing work to head-marketing. + + Monitors: pending tasks with team=marketing + Spawns: head-marketing + """ + tasks = await self._fetch_tasks(client, "pending", team="marketing") + + for task in tasks: + # Skip already claimed/assigned tasks + if task.get("assigned_to"): + continue + + if self._is_agent_active("head-marketing"): + # Already running, they'll pick up on scan + continue + + await self.spawn_agent( + agent_id="head-marketing", + task_id=task["id"], + initial_prompt=self._build_marketing_prompt(task), + ) + break + + # ========================================================================= + # SMART DISPATCHER - EVENT-BASED DISPATCHERS + # ========================================================================= + + async def _dispatch_blocker_work(self, client: httpx.AsyncClient) -> None: + """ + Dispatch blocker resolution to Cell PMs. + + Monitors: blocked tasks + Spawns: be-pm, fe-pm, ux-pm + """ + tasks = await self._fetch_tasks(client, "blocked") + + for task in tasks: + team = task.get("team") + if team not in ["backend", "frontend", "uxui"]: + continue + + agent_id = self._select_agent_for_cell(team, "pm") + if not agent_id: + continue + + if self._is_agent_active(agent_id): + continue + + await self.spawn_agent( + agent_id=agent_id, + task_id=task["id"], + initial_prompt=self._build_pm_blocker_prompt(task), + ) + break + + async def _dispatch_escalation_work(self, client: httpx.AsyncClient) -> None: + """ + Dispatch escalations to appropriate managers. + + Monitors: escalation notifications (unacknowledged) + Spawns: be-pm, fe-pm, ux-pm, main-pm, product-owner, head-marketing + """ + notifications = await self._fetch_notifications(client, "escalation") + + for notif in notifications: + targets = notif.get("to_agents", []) + + for agent_id in targets: + valid_targets = [ + "be-pm", + "fe-pm", + "ux-pm", + "main-pm", + "product-owner", + "head-marketing", + ] + if agent_id not in valid_targets: + continue + + if self._is_agent_active(agent_id): + continue + + await self.spawn_agent( + agent_id=agent_id, + initial_prompt=self._build_escalation_prompt(notif), + ) + break + + async def _dispatch_approval_work(self, client: httpx.AsyncClient) -> None: + """ + Dispatch approval requests to approvers. + + Monitors: approval notifications (unacknowledged) + Spawns: product-owner, head-marketing, main-pm + """ + notifications = await self._fetch_notifications(client, "approval") + + for notif in notifications: + targets = notif.get("to_agents", []) + + for agent_id in targets: + if agent_id not in ["product-owner", "head-marketing", "main-pm"]: + continue + + if self._is_agent_active(agent_id): + continue + + await self.spawn_agent( + agent_id=agent_id, + initial_prompt=self._build_approval_prompt(notif), + ) + break + + async def _dispatch_audit_work(self, client: httpx.AsyncClient) -> None: + """ + Dispatch audit work to the auditor. + + Monitors: quality alert notifications + Spawns: auditor + + Note: Periodic scheduled audits can be added here in the future. + """ + alerts = await self._fetch_notifications(client, "alert") + + for alert in alerts: + targets = alert.get("to_agents", []) + if "auditor" in targets and not self._is_agent_active("auditor"): + await self.spawn_agent( + agent_id="auditor", + initial_prompt=self._build_audit_prompt(alert), + ) + return + + # TODO: Add scheduled periodic audits + # Check last audit time, spawn if overdue + + # ========================================================================= + # SMART DISPATCHER - PROMPT BUILDERS + # ========================================================================= + + def _build_dev_prompt(self, task: dict[str, Any]) -> str: + """Build initial prompt for a developer with an assigned task.""" + task_id = task.get("id", "unknown") + title = task.get("title", "Untitled") + status = task.get("status", "unknown") + team = task.get("team", "unknown") + + return f"""You have been assigned a development task. + +TASK ID: {task_id} +TITLE: {title} +STATUS: {status} +TEAM: {team} + +This task is already CLAIMED for you. Begin work immediately: + +1. Call roboco_task_get("{task_id}") for full details and acceptance criteria +2. Follow the workflow: UNDERSTAND → PLAN → EXECUTE → VERIFY → SUBMIT QA +3. When task is submitted for QA, call roboco_task_scan() to check for more work +4. If more work is assigned to you, continue working +5. If no more work, call roboco_agent_idle() to shutdown gracefully + +Do NOT scan for work first - your task is already assigned. Begin now. +""" + + def _build_qa_prompt(self, task: dict[str, Any]) -> str: + """Build initial prompt for a QA agent.""" + task_id = task.get("id", "unknown") + title = task.get("title", "Untitled") + assigned_to = task.get("assigned_to", "unknown") + team = task.get("team", "unknown") + + return f"""A task is ready for QA review. + +TASK ID: {task_id} +TITLE: {title} +DEVELOPER: {assigned_to} +TEAM: {team} + +Begin QA review: + +1. Call roboco_task_get("{task_id}") for full details and acceptance criteria +2. Review the implementation against ALL acceptance criteria +3. Test the changes thoroughly +4. Call roboco_task_qa_pass() with notes if approved + OR roboco_task_qa_fail() with specific issues if rejected +5. Call roboco_task_scan() to check for more QA work +6. If no more work, call roboco_agent_idle() to shutdown gracefully +""" + + def _build_doc_prompt(self, task: dict[str, Any]) -> str: + """Build initial prompt for a documenter.""" + task_id = task.get("id", "unknown") + title = task.get("title", "Untitled") + team = task.get("team", "unknown") + + return f"""A task is ready for documentation. + +TASK ID: {task_id} +TITLE: {title} +TEAM: {team} + +Begin documentation: + +1. Call roboco_task_get("{task_id}") for full details and dev handoff notes +2. Create or update documentation based on what was implemented +3. Ensure code comments, README updates, API docs as needed +4. Call roboco_task_complete("{task_id}") when documentation is done +5. Call roboco_task_scan() to check for more documentation work +6. If no more work, call roboco_agent_idle() to shutdown gracefully +""" + + def _build_marketing_prompt(self, task: dict[str, Any]) -> str: + """Build initial prompt for head-marketing with a marketing task.""" + task_id = task.get("id", "unknown") + title = task.get("title", "Untitled") + description = task.get("description", "No description") + + return f"""You have been assigned a marketing task. + +TASK ID: {task_id} +TITLE: {title} +DESCRIPTION: {description} + +Begin work: + +1. Call roboco_task_get("{task_id}") for full details and acceptance criteria +2. Execute the marketing task (content, campaigns, research, etc.) +3. Coordinate with Product Owner or Main PM if needed +4. Call roboco_task_complete("{task_id}") when done +5. Call roboco_task_scan() to check for more marketing work +6. If no more work, call roboco_agent_idle() to shutdown gracefully +""" + + def _build_pm_blocker_prompt(self, task: dict[str, Any]) -> str: + """Build initial prompt for a Cell PM handling a blocker.""" + task_id = task.get("id", "unknown") + title = task.get("title", "Untitled") + assigned_to = task.get("assigned_to", "unknown") + blocker = task.get("blocker", {}) + reason = blocker.get("reason", "Unknown") + what_needed = blocker.get("what_needed", "Unknown") + + return f"""A task in your cell is BLOCKED and needs your attention. + +TASK ID: {task_id} +TITLE: {title} +ASSIGNED TO: {assigned_to} +BLOCKER REASON: {reason} +WHAT'S NEEDED: {what_needed} + +Your job: + +1. Understand the blocker by reviewing task details +2. Communicate with the blocked developer if needed +3. Resolve the blocker (coordinate resources, make decisions, escalate if needed) +4. Once resolved, the developer can call roboco_task_unblock() +5. Call roboco_task_scan() to check for other blocked tasks in your cell +6. If no more blockers, call roboco_agent_idle() to shutdown gracefully +""" + + def _build_escalation_prompt(self, notification: dict[str, Any]) -> str: + """Build initial prompt for handling an escalation.""" + notif_id = notification.get("id", "unknown") + from_agent = notification.get("from_agent", "unknown") + subject = notification.get("subject", "No subject") + priority = notification.get("priority", "normal") + body = notification.get("body", "No details provided") + + return f"""You have received an ESCALATION that requires your attention. + +FROM: {from_agent} +SUBJECT: {subject} +PRIORITY: {priority} + +DETAILS: +{body} + +Your job: + +1. Acknowledge the notification with roboco_notify_ack("{notif_id}") +2. Assess the escalation and determine action needed +3. Communicate decisions via appropriate channels +4. If this requires further escalation, use roboco_escalate() +5. When resolved, call roboco_task_scan() for other work +6. If no more work, call roboco_agent_idle() to shutdown gracefully +""" + + def _build_approval_prompt(self, notification: dict[str, Any]) -> str: + """Build initial prompt for handling an approval request.""" + notif_id = notification.get("id", "unknown") + from_agent = notification.get("from_agent", "unknown") + subject = notification.get("subject", "No subject") + related_task_id = notification.get("related_task_id", "None") + body = notification.get("body", "No details provided") + + return f"""You have received an APPROVAL REQUEST. + +FROM: {from_agent} +SUBJECT: {subject} +RELATED TASK: {related_task_id} + +REQUEST: +{body} + +Your job: + +1. Review the approval request carefully +2. If related to a task, call roboco_task_get() for context +3. Make your decision and communicate it +4. Acknowledge with roboco_notify_ack("{notif_id}") +5. Call roboco_task_scan() for other work +6. If no more work, call roboco_agent_idle() to shutdown gracefully +""" + + def _build_audit_prompt(self, alert: dict[str, Any] | None = None) -> str: + """Build initial prompt for the auditor.""" + if alert: + subject = alert.get("subject", "Quality issue detected") + body = alert.get("body", "Review system quality metrics") + + return f"""QUALITY ALERT triggered your attention. + +ALERT: {subject} +DETAILS: {body} + +Your job: + +1. Investigate the quality issue +2. Review relevant channels and task history (you have read access to all) +3. Compile your findings +4. Report to CEO via appropriate channel +5. Call roboco_agent_idle() when complete +""" + + return """Periodic AUDIT requested. + +Your job: + +1. Review recent activity across all cells +2. Check quality metrics (QA pass/fail rates, blocker frequency, etc.) +3. Identify any concerns or patterns +4. Compile audit report for CEO +5. Call roboco_agent_idle() when complete +"""