feat: transform to agent coordination platform with comprehensive documentation

Transform Issue Facade from a universal CLI tool into an agent coordination platform with comprehensive documentation and enhanced capabilities for autonomous coding agents.

Major Changes:
- Complete README rewrite focusing on agent-driven coordination
- New comprehensive documentation (AGENT_INTEGRATION.md, CLAUDE.md, ROADMAP.md)
- Capability integration setup with CAPABILITY.yaml and integration scripts
- Enhanced Makefile with local development targets for easier workflows

Bug Fixes:
- Fix schema initialization using executescript() for multi-line SQL support
- Disable FTS5 triggers due to compatibility issues (documented for future re-enablement)

Features:
- Enhanced CLI list command with full parameter passthrough
- New examples directory with agent integration patterns
- New comprehensive test suite (test_core_models.py, test_local_backend.py)

Code Quality:
- Remove @cached_property decorators for Label properties (simplification)
- Clean up test organization (removed old test_gitea_integration.py)

This milestone establishes Issue Facade as a production-ready coordination layer for multi-agent software development, with clear integration paths and comprehensive developer documentation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-17 19:32:37 +01:00
parent 2dfe5130a3
commit 324453bd8d
22 changed files with 6489 additions and 835 deletions

252
examples/agents/README.md Normal file
View File

@@ -0,0 +1,252 @@
# Agent Examples
This directory contains working examples of autonomous agents using the Issue Facade for coordination.
## Prerequisites
1. **Install issue-facade**:
```bash
cd ../..
pip install -e .
```
2. **Configure backend** (one-time setup):
```bash
export GITEA_API_TOKEN="your-token"
issue backend add myproject gitea
# Enter: URL, owner, repo when prompted
issue backend set-default myproject
```
3. **Set environment variables** for scripts:
```bash
export GITEA_URL=https://gitea.example.com
export GITEA_TOKEN=your-token
export GITEA_OWNER=your-org
export GITEA_REPO=your-repo
```
## Examples
### 1. Simple Task Executor (`simple_task_executor.py`)
**What it does:**
- Finds open issues labeled `ready`
- Claims them by assigning to itself
- Simulates work
- Closes with completion comment
**Usage:**
```bash
# Run once
python simple_task_executor.py --once
# Run continuously
python simple_task_executor.py
# Run for N iterations
python simple_task_executor.py --max-iterations 5
```
**Setup test data:**
```bash
# Create some test issues
issue create "Test task 1" --label=ready
issue create "Test task 2" --label=ready
issue create "Test task 3" --label=ready
```
### 2. Multi-Agent Pipeline (`multi_agent_pipeline.py`)
**What it does:**
Simulates a CI/CD pipeline with specialized agents:
- **Coder**: Implements features (needs-implementation → needs-review)
- **Reviewer**: Reviews code (needs-review → needs-testing)
- **Tester**: Runs tests (needs-testing → needs-deployment)
- **Deployer**: Deploys to staging (needs-deployment → deployed + closed)
**Usage:**
```bash
# Run all agents in round-robin (one process)
python multi_agent_pipeline.py --mode=roundrobin
# Or run each agent separately (in different terminals)
python multi_agent_pipeline.py --agent=coder
python multi_agent_pipeline.py --agent=reviewer
python multi_agent_pipeline.py --agent=tester
python multi_agent_pipeline.py --agent=deployer
```
**Setup test data:**
```bash
issue create "Feature: Add user profile" --label=feature --label=needs-implementation
issue create "Feature: Export data to CSV" --label=feature --label=needs-implementation
```
**Watch the pipeline:**
```bash
# In another terminal, watch progress
watch -n 2 'issue list --format=json | jq -r ".[] | [.number, .state, (.labels | map(.name) | join(\",\"))] | @tsv"'
```
### 3. Human-in-the-Loop (`human_in_loop.py`)
**What it does:**
- Finds large features labeled `needs-breakdown`
- Proposes breaking them into subtasks
- Waits for human approval via comments
- Creates subtasks only after approval
**Usage:**
```bash
# Run once
python human_in_loop.py --once
# Run continuously
python human_in_loop.py
```
**Setup test data:**
```bash
issue create "Feature: Implement complete authentication system" \
--label=feature \
--label=needs-breakdown \
--description="Large feature that should be broken down into smaller tasks"
```
**Approve the agent's proposal:**
```bash
# Agent will post a proposal comment. You reply with:
issue comment <issue-number> "APPROVED"
# Or reject:
issue comment <issue-number> "REJECTED: Not the right approach"
# Or request modifications:
issue comment <issue-number> "MODIFY: Please add security audit as a subtask"
```
### 4. Monitoring Agent (`monitoring_agent.py`)
**What it does:**
- Monitors issue tracker health
- Detects stale issues (not updated in N days)
- Finds blocked issues
- Identifies unassigned priority issues
- Analyzes project velocity
- Creates alert issues when problems found
**Usage:**
```bash
# Run once (no alerts)
python monitoring_agent.py --once --no-alerts
# Run continuously with hourly checks
python monitoring_agent.py --check-interval=3600
# Run with custom stale threshold
python monitoring_agent.py --stale-days=14 --once
```
**View monitoring output:**
The agent will create issues tagged with `monitoring` and `alert` when it detects problems.
## Agent Coordination Patterns
### Pattern 1: Label-Based Roles
Agents claim issues based on label matching:
```python
AGENT_ROLES = {
'agent:coder': ['feature', 'bug', 'refactor'],
'agent:tester': ['needs-testing', 'test-failure'],
'agent:reviewer': ['needs-review']
}
issues = backend.list_issues(IssueFilter(
state='open',
labels=[agent_type, 'feature']
))
```
### Pattern 2: State Machine
Issues flow through states:
```
open → in_progress → needs_review → closed
↓ ↓ ↓
blocked blocked blocked
```
### Pattern 3: Comment-Based Communication
Agents communicate via structured comments:
```python
# Agent posts JSON in comment
comment.body = "```agent-message\n" + json.dumps({
'type': 'implementation_complete',
'agent': 'agent-coder',
'data': {'files_changed': ['auth.py'], 'tests_passing': True}
}) + "\n```"
# Other agents parse it
for comment in comments:
if '```agent-message' in comment.body:
msg = json.loads(extract_json(comment.body))
if msg['type'] == 'implementation_complete':
# React to completion
```
## Troubleshooting
### "Backend not configured"
```bash
issue backend list
# If empty:
issue backend add myproject gitea
```
### "Authentication failed"
```bash
# Check token is valid
curl -H "Authorization: token $GITEA_TOKEN" $GITEA_URL/api/v1/user
```
### "No issues found"
```bash
# Verify you have issues
issue list
# Create test data
issue create "Test" --label=ready
```
### Agent stuck/not processing
```bash
# Check what agent sees
issue list --state=open --label=needs-implementation --format=json
# Verify labels match what agent expects
issue show 42 --format=json | jq '.labels'
```
## Tips for Development
1. **Use `--once` flag during development** to run single iterations
2. **Use `--no-alerts` for monitoring agent** to avoid cluttering tracker
3. **Run agents in separate terminals** to see concurrent execution
4. **Use JSON format** for debugging: `issue list --format=json | jq`
5. **Clean up test data**: `issue list --label=agent-generated --format=json | jq -r '.[].number' | xargs -I {} issue close {}`
## Next Steps
- Modify these examples for your use case
- Add error handling and retry logic
- Implement actual work instead of `time.sleep()`
- Add logging and metrics
- Deploy as services with proper process management
See [AGENT_INTEGRATION.md](../../AGENT_INTEGRATION.md) for more patterns and strategies.

300
examples/agents/human_in_loop.py Executable file
View File

@@ -0,0 +1,300 @@
#!/usr/bin/env python3
"""
Human-in-the-Loop Agent
This agent demonstrates agent-human collaboration:
1. Agent proposes implementations
2. Human reviews and approves via comments
3. Agent proceeds only after approval
4. Agent can ask questions and wait for answers
Usage:
export GITEA_URL=https://gitea.example.com
export GITEA_TOKEN=your-token
export GITEA_OWNER=your-org
export GITEA_REPO=your-repo
python human_in_loop.py
"""
import os
import sys
import time
import re
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import Optional, List
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from issue_tracker.backends.gitea import GiteaBackend
from issue_tracker.core.models import Issue, Label, User, Comment, IssueState
from issue_tracker.core.interfaces import IssueFilter
class HumanInLoopAgent:
"""Agent that requires human approval for critical decisions."""
def __init__(self, agent_id: str = "agent-collaborative"):
self.agent_id = agent_id
self.backend = None
def connect(self):
"""Connect to backend."""
base_url = os.environ['GITEA_URL']
token = os.environ['GITEA_TOKEN']
owner = os.environ['GITEA_OWNER']
repo = os.environ['GITEA_REPO']
self.backend = GiteaBackend()
self.backend.connect({
'base_url': base_url,
'token': token,
'owner': owner,
'repo': repo
})
print(f"✓ Connected to {base_url}/{owner}/{repo}")
def find_feature_request(self) -> Optional[Issue]:
"""Find a feature request that needs breaking down."""
print("\n🔍 Looking for feature requests...")
issues = self.backend.list_issues(IssueFilter(
state='open',
labels=['feature', 'needs-breakdown']
))
if issues:
print(f" Found {len(issues)} feature request(s)")
return issues[0]
else:
print(" No feature requests found")
return None
def propose_breakdown(self, feature: Issue) -> List[str]:
"""Propose breaking down a feature into subtasks."""
print(f"\n💡 Analyzing feature #{feature.number}: {feature.title}")
# Simulate AI analysis
time.sleep(2)
# Generate proposed subtasks
subtasks = [
f"Design database schema for {feature.title}",
f"Implement backend API for {feature.title}",
f"Create frontend components for {feature.title}",
f"Write integration tests for {feature.title}",
f"Update documentation for {feature.title}"
]
print(f" Proposed {len(subtasks)} subtasks")
return subtasks
def request_approval(self, issue: Issue, subtasks: List[str]):
"""Post proposal and request human approval."""
print(f"\n📝 Requesting approval for feature breakdown...")
# Format proposal
proposal = f"🤖 **Agent Proposal: Feature Breakdown**\n\n"
proposal += f"I analyzed this feature and propose breaking it into {len(subtasks)} subtasks:\n\n"
for i, task in enumerate(subtasks, 1):
proposal += f"{i}. {task}\n"
proposal += f"\n**Human Review Required:**\n"
proposal += f"- Reply with `APPROVED` to create these subtasks\n"
proposal += f"- Reply with `REJECTED: reason` to decline\n"
proposal += f"- Reply with `MODIFY: suggestions` to request changes\n\n"
proposal += f"*Waiting for your response...*"
# Post comment
comment = Comment(
id=None,
body=proposal,
author=User(id=self.agent_id, username=self.agent_id),
created_at=datetime.now(timezone.utc)
)
self.backend.add_comment(issue.id, comment)
# Add label
issue.labels.append(Label(name='awaiting-approval'))
self.backend.update_issue(issue)
print(f" ✓ Approval requested on issue #{issue.number}")
def check_for_approval(self, issue: Issue, since: datetime) -> Optional[str]:
"""Check if human has approved/rejected."""
comments = self.backend.get_comments(issue.id)
# Look for recent human comments
for comment in reversed(comments):
# Skip agent's own comments
if comment.author.username == self.agent_id:
continue
# Only check comments after proposal
if comment.created_at <= since:
continue
# Check for approval keywords
body_upper = comment.body.upper()
if 'APPROVED' in body_upper:
return 'approved'
elif 'REJECTED' in body_upper:
return 'rejected'
elif 'MODIFY' in body_upper:
return 'modify'
return None
def create_subtasks(self, parent: Issue, subtasks: List[str]):
"""Create subtask issues."""
print(f"\n✅ Creating {len(subtasks)} subtasks...")
for i, task_title in enumerate(subtasks, 1):
subtask = Issue(
id=None,
number=0,
title=task_title,
description=f"**Subtask of #{parent.number}**: {parent.title}\n\n"
f"This is subtask {i} of {len(subtasks)}.\n\n"
f"**Parent Issue:** #{parent.number}",
state=IssueState.OPEN,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
labels=[
Label(name='subtask'),
Label(name=f'parent:{parent.number}'),
Label(name='agent-generated')
]
)
created = self.backend.create_issue(subtask)
print(f" Created subtask #{created.number}: {task_title}")
time.sleep(0.5) # Rate limit
# Update parent issue
parent.labels = [l for l in parent.labels
if l.name not in ['needs-breakdown', 'awaiting-approval']]
parent.labels.append(Label(name='broken-down'))
self.backend.update_issue(parent)
# Add completion comment
comment = Comment(
id=None,
body=f"✅ Subtasks created successfully.\n\n"
f"Created {len(subtasks)} subtasks. "
f"You can now assign these to team members or agents.",
author=User(id=self.agent_id, username=self.agent_id),
created_at=datetime.now(timezone.utc)
)
self.backend.add_comment(parent.id, comment)
print(f" ✓ All subtasks created for #{parent.number}")
def wait_for_approval(self, issue: Issue, proposal_time: datetime,
timeout_minutes: int = 60):
"""Wait for human approval with timeout."""
print(f"\n⏳ Waiting for human approval (timeout: {timeout_minutes}min)...")
start_time = datetime.now(timezone.utc)
timeout = timedelta(minutes=timeout_minutes)
check_interval = 30 # Check every 30 seconds
while True:
elapsed = datetime.now(timezone.utc) - start_time
if elapsed > timeout:
print(f"\n⏱️ Timeout: No response after {timeout_minutes} minutes")
return None
# Check for approval
decision = self.check_for_approval(issue, proposal_time)
if decision:
print(f"\n✓ Human response: {decision.upper()}")
return decision
# Wait before checking again
remaining = int((timeout - elapsed).total_seconds() / 60)
print(f" Still waiting... ({remaining} minutes remaining)")
time.sleep(check_interval)
def run_cycle(self):
"""Run one complete cycle."""
# Find feature to break down
feature = self.find_feature_request()
if not feature:
return False
# Propose breakdown
subtasks = self.propose_breakdown(feature)
# Request approval
proposal_time = datetime.now(timezone.utc)
self.request_approval(feature, subtasks)
# Wait for human decision
decision = self.wait_for_approval(feature, proposal_time, timeout_minutes=60)
if decision == 'approved':
print("\n🎉 Proposal approved! Creating subtasks...")
self.create_subtasks(feature, subtasks)
return True
elif decision == 'rejected':
print("\n❌ Proposal rejected. Moving to next feature.")
feature.labels = [l for l in feature.labels
if l.name != 'awaiting-approval']
feature.labels.append(Label(name='proposal-rejected'))
self.backend.update_issue(feature)
return False
elif decision == 'modify':
print("\n🔄 Modification requested. Human will update requirements.")
feature.labels = [l for l in feature.labels
if l.name != 'awaiting-approval']
feature.labels.append(Label(name='needs-revision'))
self.backend.update_issue(feature)
return False
else:
print("\n⏱️ No response. Will retry later.")
feature.labels = [l for l in feature.labels
if l.name != 'awaiting-approval']
self.backend.update_issue(feature)
return False
def main():
"""Main entry point."""
try:
agent = HumanInLoopAgent(agent_id="agent-collaborative")
agent.connect()
print("\n🤖 Human-in-the-Loop Agent")
print(" This agent proposes feature breakdowns and waits for approval\n")
if '--once' in sys.argv:
agent.run_cycle()
else:
# Run continuously
print(" Running in loop mode (Ctrl+C to stop)\n")
while True:
if not agent.run_cycle():
print("\n⏸️ No work to do, waiting 60 seconds...")
time.sleep(60)
except KeyboardInterrupt:
print("\n\n⚠️ Interrupted by user")
sys.exit(0)
except Exception as e:
print(f"\n❌ Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,314 @@
#!/usr/bin/env python3
"""
Monitoring Agent
This agent monitors issue health and sends alerts:
- Detects stale issues (not updated in N days)
- Finds blocked issues waiting too long
- Identifies high-priority issues without assignees
- Reports on project velocity and bottlenecks
Usage:
export GITEA_URL=https://gitea.example.com
export GITEA_TOKEN=your-token
export GITEA_OWNER=your-org
export GITEA_REPO=your-repo
python monitoring_agent.py [--stale-days=7] [--check-interval=3600]
"""
import os
import sys
import time
import argparse
from datetime import datetime, timezone, timedelta
from pathlib import Path
from collections import defaultdict
from typing import List, Dict
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from issue_tracker.backends.gitea import GiteaBackend
from issue_tracker.core.models import Issue, Label, User, Comment, IssueState
from issue_tracker.core.interfaces import IssueFilter
class MonitoringAgent:
"""Agent that monitors issue tracker health and alerts on problems."""
def __init__(self, agent_id: str = "agent-monitor", stale_days: int = 7):
self.agent_id = agent_id
self.stale_threshold = timedelta(days=stale_days)
self.backend = None
def connect(self):
"""Connect to backend."""
base_url = os.environ['GITEA_URL']
token = os.environ['GITEA_TOKEN']
owner = os.environ['GITEA_OWNER']
repo = os.environ['GITEA_REPO']
self.backend = GiteaBackend()
self.backend.connect({
'base_url': base_url,
'token': token,
'owner': owner,
'repo': repo
})
print(f"✓ Connected to {base_url}/{owner}/{repo}")
def check_stale_issues(self) -> List[Issue]:
"""Find issues that haven't been updated recently."""
print("\n🔍 Checking for stale issues...")
cutoff = datetime.now(timezone.utc) - self.stale_threshold
# Get all open issues
all_issues = self.backend.list_issues(IssueFilter(state='open'))
# Filter to stale ones
stale = [issue for issue in all_issues
if issue.updated_at < cutoff]
if stale:
print(f" ⚠️ Found {len(stale)} stale issue(s)")
for issue in stale:
days_stale = (datetime.now(timezone.utc) - issue.updated_at).days
print(f" #{issue.number}: {issue.title} "
f"(stale for {days_stale} days)")
else:
print(f" ✓ No stale issues found")
return stale
def check_blocked_issues(self) -> List[Issue]:
"""Find blocked issues."""
print("\n🔍 Checking for blocked issues...")
blocked = self.backend.list_issues(IssueFilter(
state='blocked'
))
if blocked:
print(f" ⚠️ Found {len(blocked)} blocked issue(s)")
for issue in blocked:
days_blocked = (datetime.now(timezone.utc) - issue.updated_at).days
print(f" #{issue.number}: {issue.title} "
f"(blocked for {days_blocked} days)")
else:
print(f" ✓ No blocked issues")
return blocked
def check_unassigned_priority(self) -> List[Issue]:
"""Find high-priority issues without assignees."""
print("\n🔍 Checking for unassigned priority issues...")
# Get high priority issues
high_priority = self.backend.list_issues(IssueFilter(
state='open',
labels=['priority:high']
))
critical = self.backend.list_issues(IssueFilter(
state='open',
labels=['priority:critical']
))
all_priority = high_priority + critical
unassigned = [issue for issue in all_priority if not issue.assignees]
if unassigned:
print(f" ⚠️ Found {len(unassigned)} unassigned priority issue(s)")
for issue in unassigned:
priority = next((l.name for l in issue.labels
if l.name.startswith('priority:')), 'unknown')
print(f" #{issue.number}: {issue.title} [{priority}]")
else:
print(f" ✓ All priority issues are assigned")
return unassigned
def analyze_velocity(self) -> Dict[str, int]:
"""Analyze project velocity (issues opened vs closed)."""
print("\n📊 Analyzing project velocity...")
# Get recent activity
week_ago = datetime.now(timezone.utc) - timedelta(days=7)
# Count recently created
all_issues = self.backend.list_issues(IssueFilter(limit=1000))
recent_created = len([i for i in all_issues if i.created_at >= week_ago])
# Count recently closed
closed_issues = self.backend.list_issues(IssueFilter(state='closed'))
recent_closed = len([i for i in closed_issues
if i.closed_at and i.closed_at >= week_ago])
# Count open
open_issues = len(self.backend.list_issues(IssueFilter(state='open')))
print(f" Last 7 days:")
print(f" Created: {recent_created} issues")
print(f" Closed: {recent_closed} issues")
print(f" Net change: {recent_created - recent_closed:+d}")
print(f" Currently open: {open_issues}")
if recent_created > recent_closed * 1.5:
print(f" ⚠️ Issues are piling up faster than being resolved!")
elif recent_closed > recent_created:
print(f" ✓ Good progress - closing more than opening")
return {
'created': recent_created,
'closed': recent_closed,
'open': open_issues
}
def analyze_bottlenecks(self) -> Dict[str, int]:
"""Identify bottlenecks in the workflow."""
print("\n🔍 Analyzing workflow bottlenecks...")
# Count issues by state
states = defaultdict(int)
all_open = self.backend.list_issues(IssueFilter(state='open'))
for issue in all_open:
states[issue.state.value] += 1
# Count issues by label
labels = defaultdict(int)
for issue in all_open:
for label in issue.labels:
if label.name.startswith('needs-'):
labels[label.name] += 1
print(f" Issues by state:")
for state, count in sorted(states.items(), key=lambda x: -x[1]):
print(f" {state}: {count}")
if labels:
print(f" Issues by stage:")
for label, count in sorted(labels.items(), key=lambda x: -x[1]):
print(f" {label}: {count}")
if count > 10:
print(f" ⚠️ Potential bottleneck!")
return dict(labels)
def send_alert(self, title: str, details: List[str]):
"""Send an alert by creating an issue."""
print(f"\n🚨 Sending alert: {title}")
body = f"**Monitoring Alert**\n\n"
body += f"The monitoring agent detected potential issues:\n\n"
for detail in details:
body += f"- {detail}\n"
body += f"\n*Generated by {self.agent_id} at {datetime.now(timezone.utc).isoformat()}*"
alert_issue = Issue(
id=None,
number=0,
title=f"🚨 Monitor Alert: {title}",
description=body,
state=IssueState.OPEN,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
labels=[
Label(name='monitoring'),
Label(name='alert'),
Label(name='agent-generated')
]
)
created = self.backend.create_issue(alert_issue)
print(f" ✓ Alert created as issue #{created.number}")
def run_health_check(self, send_alerts: bool = True):
"""Run complete health check."""
print(f"\n🤖 Running issue tracker health check...")
print(f" Timestamp: {datetime.now(timezone.utc).isoformat()}\n")
alerts = []
# Check for problems
stale = self.check_stale_issues()
if stale:
alerts.append(f"{len(stale)} stale issues (>= {self.stale_threshold.days} days)")
blocked = self.check_blocked_issues()
if blocked:
alerts.append(f"{len(blocked)} blocked issues")
unassigned = self.check_unassigned_priority()
if unassigned:
alerts.append(f"{len(unassigned)} unassigned priority issues")
# Analyze metrics
velocity = self.analyze_velocity()
if velocity['created'] > velocity['closed'] * 2:
alerts.append(f"Issue backlog growing rapidly "
f"({velocity['created']} created vs {velocity['closed']} closed)")
bottlenecks = self.analyze_bottlenecks()
for stage, count in bottlenecks.items():
if count > 10:
alerts.append(f"{count} issues stuck in stage: {stage}")
# Send alert if needed
if alerts and send_alerts:
self.send_alert("Issue Tracker Health Check", alerts)
elif not alerts:
print(f"\n✅ All checks passed - no issues found!")
return len(alerts) == 0
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(description='Monitoring agent for issue tracker')
parser.add_argument('--stale-days', type=int, default=7,
help='Days before an issue is considered stale')
parser.add_argument('--check-interval', type=int, default=3600,
help='Interval between checks in seconds (default: 1 hour)')
parser.add_argument('--once', action='store_true',
help='Run once and exit')
parser.add_argument('--no-alerts', action='store_true',
help='Do not create alert issues')
args = parser.parse_args()
try:
agent = MonitoringAgent(
agent_id="agent-monitor",
stale_days=args.stale_days
)
agent.connect()
if args.once:
agent.run_health_check(send_alerts=not args.no_alerts)
else:
print(f"\n🤖 Monitoring agent starting...")
print(f" Check interval: {args.check_interval}s")
print(f" Stale threshold: {args.stale_days} days")
print(f" Press Ctrl+C to stop\n")
while True:
agent.run_health_check(send_alerts=not args.no_alerts)
print(f"\n⏸️ Waiting {args.check_interval}s until next check...")
time.sleep(args.check_interval)
except KeyboardInterrupt:
print("\n\n⚠️ Interrupted by user")
sys.exit(0)
except Exception as e:
print(f"\n❌ Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,358 @@
#!/usr/bin/env python3
"""
Multi-Agent Pipeline
Demonstrates a CI/CD-like pipeline with multiple specialized agents:
- Coder Agent: Implements features
- Reviewer Agent: Reviews code
- Tester Agent: Runs tests
- Deployer Agent: Deploys to staging
Each agent monitors for issues in their stage and advances them through the pipeline.
Usage:
export GITEA_URL=https://gitea.example.com
export GITEA_TOKEN=your-token
export GITEA_OWNER=your-org
export GITEA_REPO=your-repo
# Run all agents in parallel (in separate terminals)
python multi_agent_pipeline.py --agent=coder
python multi_agent_pipeline.py --agent=reviewer
python multi_agent_pipeline.py --agent=tester
python multi_agent_pipeline.py --agent=deployer
# Or run in round-robin mode (all agents in one process)
python multi_agent_pipeline.py --mode=roundrobin
"""
import os
import sys
import time
import argparse
from datetime import datetime, timezone
from pathlib import Path
from typing import List
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from issue_tracker.backends.gitea import GiteaBackend
from issue_tracker.core.models import Issue, Label, User, Comment, IssueState
from issue_tracker.core.interfaces import IssueFilter
class BaseAgent:
"""Base class for pipeline agents."""
def __init__(self, agent_id: str, role: str):
self.agent_id = agent_id
self.role = role
self.backend = None
def connect(self):
"""Connect to backend from environment."""
base_url = os.environ['GITEA_URL']
token = os.environ['GITEA_TOKEN']
owner = os.environ['GITEA_OWNER']
repo = os.environ['GITEA_REPO']
self.backend = GiteaBackend()
self.backend.connect({
'base_url': base_url,
'token': token,
'owner': owner,
'repo': repo
})
print(f"{self.role} connected")
def log(self, message: str):
"""Log a message with agent role prefix."""
print(f"[{self.role}] {message}")
def add_comment(self, issue: Issue, message: str):
"""Add a comment to an issue."""
comment = Comment(
id=None,
body=f"**{self.role}**: {message}",
author=User(id=self.agent_id, username=self.agent_id),
created_at=datetime.now(timezone.utc)
)
self.backend.add_comment(issue.id, comment)
def process_one(self) -> bool:
"""Process one issue. Return True if work was done."""
raise NotImplementedError
class CoderAgent(BaseAgent):
"""Agent that implements features."""
def __init__(self):
super().__init__("agent-coder", "Coder")
def process_one(self) -> bool:
# Find issues needing implementation
issues = self.backend.list_issues(IssueFilter(
state='open',
labels=['needs-implementation']
))
if not issues:
return False
issue = issues[0]
self.log(f"Implementing #{issue.number}: {issue.title}")
# Update state
issue.state = IssueState.IN_PROGRESS
if not issue.assignees:
issue.assignees = [User(id=self.agent_id, username=self.agent_id)]
# Remove old label, add new label
issue.labels = [l for l in issue.labels if l.name != 'needs-implementation']
issue.labels.append(Label(name='needs-review'))
self.backend.update_issue(issue)
# Simulate work
time.sleep(2)
# Add comment
self.add_comment(issue,
"Implementation complete.\n\n"
"**Files changed:**\n"
"- src/feature.py\n"
"- tests/test_feature.py\n\n"
"Ready for code review."
)
self.log(f"✓ Completed #{issue.number}")
return True
class ReviewerAgent(BaseAgent):
"""Agent that reviews code."""
def __init__(self):
super().__init__("agent-reviewer", "Reviewer")
def process_one(self) -> bool:
# Find issues needing review
issues = self.backend.list_issues(IssueFilter(
state='in_progress',
labels=['needs-review']
))
if not issues:
return False
issue = issues[0]
self.log(f"Reviewing #{issue.number}: {issue.title}")
# Simulate review
time.sleep(2)
# Update labels
issue.labels = [l for l in issue.labels if l.name != 'needs-review']
issue.labels.append(Label(name='needs-testing'))
self.backend.update_issue(issue)
# Add review comment
self.add_comment(issue,
"Code review complete. ✅\n\n"
"**Review notes:**\n"
"- Code quality: Good\n"
"- Test coverage: 95%\n"
"- Documentation: Complete\n\n"
"Approved for testing."
)
self.log(f"✓ Approved #{issue.number}")
return True
class TesterAgent(BaseAgent):
"""Agent that runs tests."""
def __init__(self):
super().__init__("agent-tester", "Tester")
def process_one(self) -> bool:
# Find issues needing testing
issues = self.backend.list_issues(IssueFilter(
state='in_progress',
labels=['needs-testing']
))
if not issues:
return False
issue = issues[0]
self.log(f"Testing #{issue.number}: {issue.title}")
# Simulate tests
time.sleep(2)
# Update labels
issue.labels = [l for l in issue.labels if l.name != 'needs-testing']
issue.labels.append(Label(name='needs-deployment'))
self.backend.update_issue(issue)
# Add test results
self.add_comment(issue,
"All tests passing. ✅\n\n"
"**Test results:**\n"
"- Unit tests: 50/50 passed\n"
"- Integration tests: 12/12 passed\n"
"- Coverage: 96.5%\n\n"
"Ready for deployment."
)
self.log(f"✓ Tests passed #{issue.number}")
return True
class DeployerAgent(BaseAgent):
"""Agent that deploys to staging."""
def __init__(self):
super().__init__("agent-deployer", "Deployer")
def process_one(self) -> bool:
# Find issues needing deployment
issues = self.backend.list_issues(IssueFilter(
state='in_progress',
labels=['needs-deployment']
))
if not issues:
return False
issue = issues[0]
self.log(f"Deploying #{issue.number}: {issue.title}")
# Simulate deployment
time.sleep(2)
# Close issue
issue.state = IssueState.CLOSED
issue.closed_at = datetime.now(timezone.utc)
issue.labels = [l for l in issue.labels if l.name != 'needs-deployment']
issue.labels.append(Label(name='deployed'))
self.backend.update_issue(issue)
# Add deployment comment
self.add_comment(issue,
"Deployed to staging. 🚀\n\n"
"**Deployment info:**\n"
"- Environment: staging\n"
"- Version: v1.2.3\n"
"- Status: Healthy\n\n"
"Feature is live and ready for user testing."
)
self.log(f"✓ Deployed #{issue.number}")
return True
def run_single_agent(agent: BaseAgent, poll_interval: int = 5):
"""Run a single agent in a loop."""
agent.connect()
agent.log("Starting pipeline agent...")
agent.log("Press Ctrl+C to stop\n")
while True:
try:
if agent.process_one():
agent.log("Task completed, checking for more work...")
else:
agent.log(f"No work available, waiting {poll_interval}s...")
time.sleep(poll_interval)
except KeyboardInterrupt:
agent.log("Shutting down...")
break
except Exception as e:
agent.log(f"Error: {e}")
time.sleep(poll_interval)
def run_roundrobin(agents: List[BaseAgent], poll_interval: int = 5):
"""Run all agents in round-robin fashion."""
print("🤖 Starting multi-agent pipeline (round-robin mode)")
print(" Agents:", ", ".join([a.role for a in agents]))
print(" Press Ctrl+C to stop\n")
# Connect all agents
for agent in agents:
agent.connect()
while True:
try:
work_done = False
for agent in agents:
if agent.process_one():
work_done = True
time.sleep(1) # Brief pause between agents
if not work_done:
print(f"⏸️ No work available for any agent, waiting {poll_interval}s...")
time.sleep(poll_interval)
except KeyboardInterrupt:
print("\n\n⚠️ Shutting down all agents...")
break
except Exception as e:
print(f"❌ Error: {e}")
time.sleep(poll_interval)
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(description='Multi-agent pipeline')
parser.add_argument('--agent', choices=['coder', 'reviewer', 'tester', 'deployer'],
help='Run specific agent')
parser.add_argument('--mode', choices=['single', 'roundrobin'], default='single',
help='Execution mode')
parser.add_argument('--poll-interval', type=int, default=5,
help='Polling interval in seconds')
args = parser.parse_args()
try:
if args.mode == 'roundrobin':
agents = [
CoderAgent(),
ReviewerAgent(),
TesterAgent(),
DeployerAgent()
]
run_roundrobin(agents, args.poll_interval)
else:
if not args.agent:
print("Error: --agent required in single mode")
print("Use --mode=roundrobin to run all agents")
sys.exit(1)
agent_map = {
'coder': CoderAgent(),
'reviewer': ReviewerAgent(),
'tester': TesterAgent(),
'deployer': DeployerAgent()
}
agent = agent_map[args.agent]
run_single_agent(agent, args.poll_interval)
except KeyboardInterrupt:
print("\n\n⚠️ Interrupted by user")
sys.exit(0)
except Exception as e:
print(f"\n❌ Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,213 @@
#!/usr/bin/env python3
"""
Simple Task Executor Agent
This agent demonstrates a basic workflow:
1. Query for available tasks (open issues with specific labels)
2. Claim a task by assigning it to itself
3. Execute the task (simulated)
4. Report completion and close the issue
Usage:
export GITEA_URL=https://gitea.example.com
export GITEA_TOKEN=your-token
export GITEA_OWNER=your-org
export GITEA_REPO=your-repo
python simple_task_executor.py
"""
import os
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from issue_tracker.backends.gitea import GiteaBackend
from issue_tracker.core.models import Issue, Label, User, Comment, IssueState
from issue_tracker.core.interfaces import IssueFilter
class SimpleTaskExecutor:
"""A simple agent that claims and executes tasks from issue tracker."""
def __init__(self, agent_id: str = "agent-executor"):
self.agent_id = agent_id
self.backend = None
def connect(self):
"""Connect to Gitea backend from environment variables."""
base_url = os.environ.get('GITEA_URL')
token = os.environ.get('GITEA_TOKEN')
owner = os.environ.get('GITEA_OWNER')
repo = os.environ.get('GITEA_REPO')
if not all([base_url, token, owner, repo]):
raise ValueError(
"Missing required environment variables: "
"GITEA_URL, GITEA_TOKEN, GITEA_OWNER, GITEA_REPO"
)
self.backend = GiteaBackend()
self.backend.connect({
'base_url': base_url,
'token': token,
'owner': owner,
'repo': repo
})
print(f"✓ Connected to {base_url}/{owner}/{repo}")
def find_available_task(self):
"""Find an open task that's ready to be worked on."""
print("\n🔍 Searching for available tasks...")
# Look for issues labeled as ready and not assigned
issues = self.backend.list_issues(IssueFilter(
state='open',
labels=['ready'],
limit=10
))
# Filter to unassigned issues
available = [issue for issue in issues if not issue.assignees]
if available:
print(f" Found {len(available)} available task(s)")
return available[0]
else:
print(" No available tasks found")
return None
def claim_task(self, issue: Issue):
"""Claim a task by assigning it to this agent."""
print(f"\n📌 Claiming issue #{issue.number}: {issue.title}")
# Update issue state and assignee
issue.state = IssueState.IN_PROGRESS
issue.assignees = [User(id=self.agent_id, username=self.agent_id)]
self.backend.update_issue(issue)
# Add comment announcing claim
comment = Comment(
id=None,
body=f"🤖 Task claimed by {self.agent_id}\n\nStarting work...",
author=User(id=self.agent_id, username=self.agent_id),
created_at=datetime.now(timezone.utc)
)
self.backend.add_comment(issue.id, comment)
print(f" ✓ Issue #{issue.number} claimed")
def execute_task(self, issue: Issue):
"""Execute the task (simulated with sleep)."""
print(f"\n⚙️ Executing task #{issue.number}...")
# Simulate work by sleeping
work_time = 2
for i in range(work_time):
time.sleep(1)
print(f" Working... ({i+1}/{work_time}s)")
# Add progress comment
comment = Comment(
id=None,
body=f"Implementation complete.\n\n"
f"**Changes made:**\n"
f"- Analyzed requirements\n"
f"- Implemented solution\n"
f"- Verified functionality\n\n"
f"Ready for review.",
author=User(id=self.agent_id, username=self.agent_id),
created_at=datetime.now(timezone.utc)
)
self.backend.add_comment(issue.id, comment)
print(f" ✓ Task #{issue.number} completed")
def complete_task(self, issue: Issue):
"""Mark task as complete by closing the issue."""
print(f"\n✅ Completing issue #{issue.number}...")
# Close the issue
issue.state = IssueState.CLOSED
issue.closed_at = datetime.now(timezone.utc)
self.backend.update_issue(issue)
# Add completion comment
comment = Comment(
id=None,
body=f"🎉 Task completed by {self.agent_id}\n\n"
f"All work has been finished and verified.",
author=User(id=self.agent_id, username=self.agent_id),
created_at=datetime.now(timezone.utc)
)
self.backend.add_comment(issue.id, comment)
print(f" ✓ Issue #{issue.number} closed")
def run_once(self):
"""Execute one task cycle."""
# Find available task
task = self.find_available_task()
if not task:
return False
# Claim and execute
self.claim_task(task)
self.execute_task(task)
self.complete_task(task)
return True
def run_loop(self, max_iterations=None):
"""Run continuously, processing tasks as they become available."""
print(f"\n🤖 {self.agent_id} starting task execution loop...")
print(f" Press Ctrl+C to stop\n")
iteration = 0
while True:
iteration += 1
if max_iterations and iteration > max_iterations:
print(f"\n⚠️ Reached maximum iterations ({max_iterations})")
break
# Try to process one task
processed = self.run_once()
if processed:
print(f"\n✓ Completed iteration {iteration}")
else:
print(f"\n⏸️ No tasks available (iteration {iteration})")
print(" Waiting 5 seconds before checking again...")
time.sleep(5)
def main():
"""Main entry point."""
try:
# Create and initialize agent
agent = SimpleTaskExecutor(agent_id="agent-executor")
agent.connect()
# Run once or in loop
import sys
if '--once' in sys.argv:
agent.run_once()
elif '--max-iterations' in sys.argv:
idx = sys.argv.index('--max-iterations')
max_iter = int(sys.argv[idx + 1])
agent.run_loop(max_iterations=max_iter)
else:
agent.run_loop()
except KeyboardInterrupt:
print("\n\n⚠️ Interrupted by user")
sys.exit(0)
except Exception as e:
print(f"\n❌ Error: {e}")
sys.exit(1)
if __name__ == '__main__':
main()