@pjmendonca/devflow 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +526 -0
- package/LICENSE +21 -0
- package/README.md +620 -0
- package/bin/devflow-checkpoint.js +10 -0
- package/bin/devflow-collab.js +10 -0
- package/bin/devflow-cost.js +10 -0
- package/bin/devflow-create-persona.js +10 -0
- package/bin/devflow-init.js +10 -0
- package/bin/devflow-memory.js +10 -0
- package/bin/devflow-new-doc.js +10 -0
- package/bin/devflow-personalize.js +10 -0
- package/bin/devflow-setup-checkpoint.js +10 -0
- package/bin/devflow-story.js +10 -0
- package/bin/devflow-tech-debt.js +10 -0
- package/bin/devflow-validate-overrides.js +10 -0
- package/bin/devflow-validate.js +10 -0
- package/bin/devflow-version.js +10 -0
- package/lib/constants.js +30 -0
- package/lib/exec-python.js +78 -0
- package/lib/python-check.js +178 -0
- package/package.json +64 -0
- package/tooling/.automation/agents/architect.md +135 -0
- package/tooling/.automation/agents/ba.md +70 -0
- package/tooling/.automation/agents/dev.md +79 -0
- package/tooling/.automation/agents/maintainer.md +97 -0
- package/tooling/.automation/agents/pm.md +116 -0
- package/tooling/.automation/agents/reviewer.md +141 -0
- package/tooling/.automation/agents/sm.md +61 -0
- package/tooling/.automation/agents/writer.md +193 -0
- package/tooling/.automation/config.ps1.template +61 -0
- package/tooling/.automation/config.sh.template +48 -0
- package/tooling/.automation/memory/.gitkeep +6 -0
- package/tooling/.automation/memory/knowledge/kg_integration-test.json +94 -0
- package/tooling/.automation/memory/knowledge/kg_test-story.json +300 -0
- package/tooling/.automation/memory/shared/shared_integration-test.json +30 -0
- package/tooling/.automation/memory/shared/shared_test-story.json +78 -0
- package/tooling/.automation/overrides/templates/README.md +113 -0
- package/tooling/.automation/overrides/templates/architect/README.md +27 -0
- package/tooling/.automation/overrides/templates/architect/cloud-native.yaml +92 -0
- package/tooling/.automation/overrides/templates/architect/enterprise-architect.yaml +85 -0
- package/tooling/.automation/overrides/templates/architect/pragmatic-minimalist.yaml +88 -0
- package/tooling/.automation/overrides/templates/ba/README.md +27 -0
- package/tooling/.automation/overrides/templates/ba/agile-storyteller.yaml +86 -0
- package/tooling/.automation/overrides/templates/ba/domain-expert.yaml +91 -0
- package/tooling/.automation/overrides/templates/ba/requirements-engineer.yaml +89 -0
- package/tooling/.automation/overrides/templates/dev/README.md +32 -0
- package/tooling/.automation/overrides/templates/dev/junior-mentored.yaml +39 -0
- package/tooling/.automation/overrides/templates/dev/performance-engineer.yaml +43 -0
- package/tooling/.automation/overrides/templates/dev/rapid-prototyper.yaml +52 -0
- package/tooling/.automation/overrides/templates/dev/security-focused.yaml +43 -0
- package/tooling/.automation/overrides/templates/dev/senior-fullstack.yaml +39 -0
- package/tooling/.automation/overrides/templates/maintainer/README.md +27 -0
- package/tooling/.automation/overrides/templates/maintainer/devops-maintainer.yaml +113 -0
- package/tooling/.automation/overrides/templates/maintainer/legacy-steward.yaml +94 -0
- package/tooling/.automation/overrides/templates/maintainer/oss-maintainer.yaml +94 -0
- package/tooling/.automation/overrides/templates/pm/README.md +27 -0
- package/tooling/.automation/overrides/templates/pm/agile-pm.yaml +91 -0
- package/tooling/.automation/overrides/templates/pm/hybrid-delivery.yaml +87 -0
- package/tooling/.automation/overrides/templates/pm/traditional-pm.yaml +91 -0
- package/tooling/.automation/overrides/templates/reviewer/README.md +11 -0
- package/tooling/.automation/overrides/templates/reviewer/mentoring-reviewer.yaml +45 -0
- package/tooling/.automation/overrides/templates/reviewer/quick-sanity.yaml +50 -0
- package/tooling/.automation/overrides/templates/reviewer/thorough-critic.yaml +48 -0
- package/tooling/.automation/overrides/templates/sm/README.md +11 -0
- package/tooling/.automation/overrides/templates/sm/agile-coach.yaml +52 -0
- package/tooling/.automation/overrides/templates/sm/startup-pm.yaml +50 -0
- package/tooling/.automation/overrides/templates/sm/technical-lead.yaml +47 -0
- package/tooling/.automation/overrides/templates/user-profile.template.yaml +62 -0
- package/tooling/.automation/overrides/templates/writer/README.md +27 -0
- package/tooling/.automation/overrides/templates/writer/api-documentarian.yaml +99 -0
- package/tooling/.automation/overrides/templates/writer/docs-as-code.yaml +108 -0
- package/tooling/.automation/overrides/templates/writer/user-guide-author.yaml +100 -0
- package/tooling/completions/DevflowCompletion.ps1 +213 -0
- package/tooling/completions/_run-story +116 -0
- package/tooling/completions/run-story-completion.bash +136 -0
- package/tooling/docs/DOC-STANDARD.md +717 -0
- package/tooling/docs/sprint-status.yaml.template +24 -0
- package/tooling/docs/templates/bug-report.md +234 -0
- package/tooling/docs/templates/migration-spec.md +274 -0
- package/tooling/docs/templates/refactor-spec.md +86 -0
- package/tooling/docs/templates/tech-debt.md +86 -0
- package/tooling/scripts/context_checkpoint.py +556 -0
- package/tooling/scripts/cost_dashboard.py +617 -0
- package/tooling/scripts/create-persona.py +690 -0
- package/tooling/scripts/create-persona.sh +435 -0
- package/tooling/scripts/init-project-workflow.ps1 +651 -0
- package/tooling/scripts/init-project-workflow.py +70 -0
- package/tooling/scripts/init-project-workflow.sh +746 -0
- package/tooling/scripts/lib/__init__.py +35 -0
- package/tooling/scripts/lib/agent_handoff.py +526 -0
- package/tooling/scripts/lib/agent_router.py +698 -0
- package/tooling/scripts/lib/checkpoint-integration.ps1 +245 -0
- package/tooling/scripts/lib/checkpoint-integration.sh +191 -0
- package/tooling/scripts/lib/claude-cli.ps1 +952 -0
- package/tooling/scripts/lib/claude-cli.sh +1293 -0
- package/tooling/scripts/lib/cost_config.py +222 -0
- package/tooling/scripts/lib/cost_display.py +443 -0
- package/tooling/scripts/lib/cost_tracker.py +710 -0
- package/tooling/scripts/lib/currency_converter.py +328 -0
- package/tooling/scripts/lib/errors.py +438 -0
- package/tooling/scripts/lib/override-loader.sh +286 -0
- package/tooling/scripts/lib/pair_programming.py +589 -0
- package/tooling/scripts/lib/shared_memory.py +637 -0
- package/tooling/scripts/lib/swarm_orchestrator.py +689 -0
- package/tooling/scripts/memory_summarize.py +324 -0
- package/tooling/scripts/new-doc.ps1 +405 -0
- package/tooling/scripts/new-doc.py +93 -0
- package/tooling/scripts/new-doc.sh +534 -0
- package/tooling/scripts/personalize_agent.py +385 -0
- package/tooling/scripts/rollback-migration.sh +540 -0
- package/tooling/scripts/run-collab.ps1 +251 -0
- package/tooling/scripts/run-collab.py +605 -0
- package/tooling/scripts/run-collab.sh +110 -0
- package/tooling/scripts/run-story.ps1 +490 -0
- package/tooling/scripts/run-story.py +387 -0
- package/tooling/scripts/run-story.sh +467 -0
- package/tooling/scripts/setup-checkpoint-service.ps1 +219 -0
- package/tooling/scripts/setup-checkpoint-service.py +87 -0
- package/tooling/scripts/setup-checkpoint-service.sh +236 -0
- package/tooling/scripts/tech-debt-tracker.py +608 -0
- package/tooling/scripts/update_version.py +244 -0
- package/tooling/scripts/validate-overrides.py +511 -0
- package/tooling/scripts/validate-overrides.sh +432 -0
- package/tooling/scripts/validate_setup.py +539 -0
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Swarm Orchestrator - Multi-Agent Collaboration System
|
|
4
|
+
|
|
5
|
+
Orchestrates multiple agents working together with:
|
|
6
|
+
- Debate/consensus loops
|
|
7
|
+
- Automatic iteration on feedback
|
|
8
|
+
- Parallel agent execution
|
|
9
|
+
- Conflict resolution
|
|
10
|
+
- Convergence detection
|
|
11
|
+
|
|
12
|
+
Features:
|
|
13
|
+
- Swarm mode: Multiple agents debate until consensus
|
|
14
|
+
- Iteration loops: DEV → REVIEWER → DEV cycles
|
|
15
|
+
- Parallel execution: Independent agents run simultaneously
|
|
16
|
+
- Voting mechanisms for decisions
|
|
17
|
+
- Automatic termination on convergence
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
from lib.swarm_orchestrator import SwarmOrchestrator, SwarmConfig
|
|
21
|
+
|
|
22
|
+
orchestrator = SwarmOrchestrator(story_key="3-5")
|
|
23
|
+
result = orchestrator.run_swarm(
|
|
24
|
+
agents=["ARCHITECT", "DEV", "REVIEWER"],
|
|
25
|
+
task="Design and implement user authentication",
|
|
26
|
+
max_iterations=3
|
|
27
|
+
)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import re
|
|
31
|
+
import subprocess
|
|
32
|
+
import sys
|
|
33
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from datetime import datetime
|
|
36
|
+
from enum import Enum
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Optional
|
|
39
|
+
|
|
40
|
+
# Import dependencies
|
|
41
|
+
try:
|
|
42
|
+
from agent_handoff import HandoffGenerator
|
|
43
|
+
from agent_router import AgentRouter
|
|
44
|
+
from shared_memory import get_knowledge_graph, get_shared_memory
|
|
45
|
+
except ImportError:
|
|
46
|
+
from lib.agent_handoff import HandoffGenerator
|
|
47
|
+
from lib.agent_router import AgentRouter
|
|
48
|
+
from lib.shared_memory import get_knowledge_graph, get_shared_memory
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
PROJECT_ROOT = Path(__file__).parent.parent.parent.parent
|
|
52
|
+
CLAUDE_CLI = "claude.cmd" if sys.platform == "win32" else "claude"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class SwarmState(Enum):
|
|
56
|
+
"""State of the swarm orchestration."""
|
|
57
|
+
|
|
58
|
+
INITIALIZING = "initializing"
|
|
59
|
+
RUNNING = "running"
|
|
60
|
+
DEBATING = "debating"
|
|
61
|
+
CONVERGING = "converging"
|
|
62
|
+
CONSENSUS = "consensus"
|
|
63
|
+
COMPLETED = "completed"
|
|
64
|
+
FAILED = "failed"
|
|
65
|
+
MAX_ITERATIONS = "max_iterations"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ConsensusType(Enum):
|
|
69
|
+
"""Types of consensus mechanisms."""
|
|
70
|
+
|
|
71
|
+
UNANIMOUS = "unanimous" # All must agree
|
|
72
|
+
MAJORITY = "majority" # >50% agree
|
|
73
|
+
QUORUM = "quorum" # N of M agree
|
|
74
|
+
REVIEWER_APPROVAL = "reviewer_approval" # REVIEWER must approve
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class AgentResponse:
|
|
79
|
+
"""Response from an agent."""
|
|
80
|
+
|
|
81
|
+
agent: str
|
|
82
|
+
model: str
|
|
83
|
+
content: str
|
|
84
|
+
timestamp: str
|
|
85
|
+
iteration: int
|
|
86
|
+
tokens_used: int = 0
|
|
87
|
+
cost_usd: float = 0.0
|
|
88
|
+
issues_found: list[str] = field(default_factory=list)
|
|
89
|
+
approvals: list[str] = field(default_factory=list)
|
|
90
|
+
suggestions: list[str] = field(default_factory=list)
|
|
91
|
+
vote: Optional[str] = None # approve, reject, abstain
|
|
92
|
+
|
|
93
|
+
def to_dict(self) -> dict:
|
|
94
|
+
return {
|
|
95
|
+
"agent": self.agent,
|
|
96
|
+
"model": self.model,
|
|
97
|
+
"content": self.content[:500] + "..." if len(self.content) > 500 else self.content,
|
|
98
|
+
"timestamp": self.timestamp,
|
|
99
|
+
"iteration": self.iteration,
|
|
100
|
+
"tokens_used": self.tokens_used,
|
|
101
|
+
"cost_usd": self.cost_usd,
|
|
102
|
+
"issues_found": self.issues_found,
|
|
103
|
+
"approvals": self.approvals,
|
|
104
|
+
"suggestions": self.suggestions,
|
|
105
|
+
"vote": self.vote,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class SwarmIteration:
|
|
111
|
+
"""One iteration of the swarm."""
|
|
112
|
+
|
|
113
|
+
iteration_num: int
|
|
114
|
+
responses: list[AgentResponse] = field(default_factory=list)
|
|
115
|
+
consensus_reached: bool = False
|
|
116
|
+
issues_remaining: list[str] = field(default_factory=list)
|
|
117
|
+
decisions_made: list[str] = field(default_factory=list)
|
|
118
|
+
|
|
119
|
+
def to_dict(self) -> dict:
|
|
120
|
+
return {
|
|
121
|
+
"iteration_num": self.iteration_num,
|
|
122
|
+
"responses": [r.to_dict() for r in self.responses],
|
|
123
|
+
"consensus_reached": self.consensus_reached,
|
|
124
|
+
"issues_remaining": self.issues_remaining,
|
|
125
|
+
"decisions_made": self.decisions_made,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class SwarmResult:
|
|
131
|
+
"""Result of swarm orchestration."""
|
|
132
|
+
|
|
133
|
+
story_key: str
|
|
134
|
+
task: str
|
|
135
|
+
state: SwarmState
|
|
136
|
+
iterations: list[SwarmIteration]
|
|
137
|
+
final_output: str
|
|
138
|
+
agents_involved: list[str]
|
|
139
|
+
total_tokens: int
|
|
140
|
+
total_cost_usd: float
|
|
141
|
+
start_time: str
|
|
142
|
+
end_time: str
|
|
143
|
+
consensus_type: ConsensusType
|
|
144
|
+
|
|
145
|
+
def to_dict(self) -> dict:
|
|
146
|
+
return {
|
|
147
|
+
"story_key": self.story_key,
|
|
148
|
+
"task": self.task,
|
|
149
|
+
"state": self.state.value,
|
|
150
|
+
"iterations": [i.to_dict() for i in self.iterations],
|
|
151
|
+
"final_output": self.final_output,
|
|
152
|
+
"agents_involved": self.agents_involved,
|
|
153
|
+
"total_tokens": self.total_tokens,
|
|
154
|
+
"total_cost_usd": self.total_cost_usd,
|
|
155
|
+
"start_time": self.start_time,
|
|
156
|
+
"end_time": self.end_time,
|
|
157
|
+
"consensus_type": self.consensus_type.value,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
def to_summary(self) -> str:
|
|
161
|
+
"""Generate a human-readable summary."""
|
|
162
|
+
lines = [
|
|
163
|
+
f"## Swarm Result: {self.story_key}",
|
|
164
|
+
"",
|
|
165
|
+
f"**Task**: {self.task}",
|
|
166
|
+
f"**State**: {self.state.value}",
|
|
167
|
+
f"**Iterations**: {len(self.iterations)}",
|
|
168
|
+
f"**Agents**: {', '.join(self.agents_involved)}",
|
|
169
|
+
f"**Total Cost**: ${self.total_cost_usd:.4f}",
|
|
170
|
+
"",
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
if self.state == SwarmState.CONSENSUS:
|
|
174
|
+
lines.append("✅ **Consensus reached!**")
|
|
175
|
+
elif self.state == SwarmState.MAX_ITERATIONS:
|
|
176
|
+
lines.append("⚠️ **Max iterations reached without full consensus**")
|
|
177
|
+
|
|
178
|
+
lines.append("")
|
|
179
|
+
lines.append("### Final Output")
|
|
180
|
+
lines.append(
|
|
181
|
+
self.final_output[:1000] if len(self.final_output) > 1000 else self.final_output
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
return "\n".join(lines)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass
|
|
188
|
+
class SwarmConfig:
|
|
189
|
+
"""Configuration for swarm orchestration."""
|
|
190
|
+
|
|
191
|
+
max_iterations: int = 3
|
|
192
|
+
consensus_type: ConsensusType = ConsensusType.REVIEWER_APPROVAL
|
|
193
|
+
quorum_size: int = 2 # For QUORUM type
|
|
194
|
+
timeout_seconds: int = 300
|
|
195
|
+
parallel_execution: bool = False
|
|
196
|
+
auto_fix_enabled: bool = True # DEV automatically addresses REVIEWER issues
|
|
197
|
+
verbose: bool = True
|
|
198
|
+
budget_limit_usd: float = 20.0
|
|
199
|
+
|
|
200
|
+
def to_dict(self) -> dict:
|
|
201
|
+
return {
|
|
202
|
+
"max_iterations": self.max_iterations,
|
|
203
|
+
"consensus_type": self.consensus_type.value,
|
|
204
|
+
"quorum_size": self.quorum_size,
|
|
205
|
+
"timeout_seconds": self.timeout_seconds,
|
|
206
|
+
"parallel_execution": self.parallel_execution,
|
|
207
|
+
"auto_fix_enabled": self.auto_fix_enabled,
|
|
208
|
+
"budget_limit_usd": self.budget_limit_usd,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class SwarmOrchestrator:
|
|
213
|
+
"""Orchestrates multi-agent collaboration."""
|
|
214
|
+
|
|
215
|
+
def __init__(self, story_key: str, config: Optional[SwarmConfig] = None):
|
|
216
|
+
self.story_key = story_key
|
|
217
|
+
self.config = config or SwarmConfig()
|
|
218
|
+
self.project_root = PROJECT_ROOT
|
|
219
|
+
self.shared_memory = get_shared_memory(story_key)
|
|
220
|
+
self.knowledge_graph = get_knowledge_graph(story_key)
|
|
221
|
+
self.handoff_generator = HandoffGenerator(story_key)
|
|
222
|
+
self.router = AgentRouter()
|
|
223
|
+
|
|
224
|
+
self.state = SwarmState.INITIALIZING
|
|
225
|
+
self.iterations: list[SwarmIteration] = []
|
|
226
|
+
self.total_tokens = 0
|
|
227
|
+
self.total_cost = 0.0
|
|
228
|
+
|
|
229
|
+
# Agent model mapping
|
|
230
|
+
self.agent_models = {
|
|
231
|
+
"SM": "sonnet",
|
|
232
|
+
"DEV": "opus",
|
|
233
|
+
"REVIEWER": "opus",
|
|
234
|
+
"ARCHITECT": "sonnet",
|
|
235
|
+
"BA": "sonnet",
|
|
236
|
+
"PM": "sonnet",
|
|
237
|
+
"WRITER": "sonnet",
|
|
238
|
+
"MAINTAINER": "sonnet",
|
|
239
|
+
"SECURITY": "opus",
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
def _log(self, message: str, level: str = "INFO"):
|
|
243
|
+
"""Log a message with timestamp."""
|
|
244
|
+
if self.config.verbose:
|
|
245
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
246
|
+
emoji = {
|
|
247
|
+
"INFO": "ℹ️",
|
|
248
|
+
"SUCCESS": "✅",
|
|
249
|
+
"WARNING": "⚠️",
|
|
250
|
+
"ERROR": "❌",
|
|
251
|
+
"DEBUG": "🔍",
|
|
252
|
+
}.get(level, "•")
|
|
253
|
+
print(f"[{timestamp}] {emoji} {message}")
|
|
254
|
+
|
|
255
|
+
def _invoke_agent(self, agent: str, prompt: str, iteration: int = 0) -> AgentResponse:
|
|
256
|
+
"""Invoke a single agent with Claude CLI."""
|
|
257
|
+
model = self.agent_models.get(agent, "sonnet")
|
|
258
|
+
|
|
259
|
+
self._log(f"Invoking {agent} (model: {model})...")
|
|
260
|
+
|
|
261
|
+
# Build the full prompt with context
|
|
262
|
+
context = self.handoff_generator.generate_context_for_agent(agent)
|
|
263
|
+
full_prompt = f"{context}\n\n---\n\n{prompt}"
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
result = subprocess.run(
|
|
267
|
+
[CLAUDE_CLI, "--print", "--model", model, "-p", full_prompt],
|
|
268
|
+
capture_output=True,
|
|
269
|
+
text=True,
|
|
270
|
+
timeout=self.config.timeout_seconds,
|
|
271
|
+
cwd=str(self.project_root),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
content = result.stdout + result.stderr
|
|
275
|
+
|
|
276
|
+
# Parse response for issues, approvals, suggestions
|
|
277
|
+
issues = self._extract_issues(content)
|
|
278
|
+
approvals = self._extract_approvals(content)
|
|
279
|
+
suggestions = self._extract_suggestions(content)
|
|
280
|
+
vote = self._determine_vote(content, issues, approvals)
|
|
281
|
+
|
|
282
|
+
# Estimate tokens (rough)
|
|
283
|
+
tokens = len(full_prompt.split()) + len(content.split())
|
|
284
|
+
cost = self._estimate_cost(tokens, model)
|
|
285
|
+
|
|
286
|
+
self.total_tokens += tokens
|
|
287
|
+
self.total_cost += cost
|
|
288
|
+
|
|
289
|
+
return AgentResponse(
|
|
290
|
+
agent=agent,
|
|
291
|
+
model=model,
|
|
292
|
+
content=content,
|
|
293
|
+
timestamp=datetime.now().isoformat(),
|
|
294
|
+
iteration=iteration,
|
|
295
|
+
tokens_used=tokens,
|
|
296
|
+
cost_usd=cost,
|
|
297
|
+
issues_found=issues,
|
|
298
|
+
approvals=approvals,
|
|
299
|
+
suggestions=suggestions,
|
|
300
|
+
vote=vote,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
except subprocess.TimeoutExpired:
|
|
304
|
+
self._log(f"{agent} timed out", "WARNING")
|
|
305
|
+
return AgentResponse(
|
|
306
|
+
agent=agent,
|
|
307
|
+
model=model,
|
|
308
|
+
content="[TIMEOUT]",
|
|
309
|
+
timestamp=datetime.now().isoformat(),
|
|
310
|
+
iteration=iteration,
|
|
311
|
+
vote="abstain",
|
|
312
|
+
)
|
|
313
|
+
except Exception as e:
|
|
314
|
+
self._log(f"{agent} failed: {e}", "ERROR")
|
|
315
|
+
return AgentResponse(
|
|
316
|
+
agent=agent,
|
|
317
|
+
model=model,
|
|
318
|
+
content=f"[ERROR: {str(e)}]",
|
|
319
|
+
timestamp=datetime.now().isoformat(),
|
|
320
|
+
iteration=iteration,
|
|
321
|
+
vote="abstain",
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
def _extract_issues(self, content: str) -> list[str]:
|
|
325
|
+
"""Extract issues/problems from response."""
|
|
326
|
+
issues = []
|
|
327
|
+
patterns = [
|
|
328
|
+
r"(?:issue|problem|bug|error|fix needed|must fix|should fix):\s*(.+)",
|
|
329
|
+
r"❌\s*(.+)",
|
|
330
|
+
r"\[ISSUE\]\s*(.+)",
|
|
331
|
+
r"- (?:Issue|Problem|Bug):\s*(.+)",
|
|
332
|
+
]
|
|
333
|
+
|
|
334
|
+
for pattern in patterns:
|
|
335
|
+
matches = re.findall(pattern, content, re.IGNORECASE | re.MULTILINE)
|
|
336
|
+
issues.extend(matches)
|
|
337
|
+
|
|
338
|
+
return list(set(issues))[:10] # Limit to 10
|
|
339
|
+
|
|
340
|
+
def _extract_approvals(self, content: str) -> list[str]:
|
|
341
|
+
"""Extract approvals/LGTMs from response."""
|
|
342
|
+
approvals = []
|
|
343
|
+
patterns = [
|
|
344
|
+
r"(?:lgtm|approved|looks good|well done|excellent)[\s:]*(.+)?",
|
|
345
|
+
r"✅\s*(.+)",
|
|
346
|
+
r"\[APPROVED\]\s*(.+)?",
|
|
347
|
+
]
|
|
348
|
+
|
|
349
|
+
for pattern in patterns:
|
|
350
|
+
matches = re.findall(pattern, content, re.IGNORECASE)
|
|
351
|
+
for match in matches:
|
|
352
|
+
approvals.append(match if match else "Approved")
|
|
353
|
+
|
|
354
|
+
return list(set(approvals))[:5]
|
|
355
|
+
|
|
356
|
+
def _extract_suggestions(self, content: str) -> list[str]:
|
|
357
|
+
"""Extract suggestions from response."""
|
|
358
|
+
suggestions = []
|
|
359
|
+
patterns = [
|
|
360
|
+
r"(?:suggest|consider|recommend|might want to|could):\s*(.+)",
|
|
361
|
+
r"💡\s*(.+)",
|
|
362
|
+
r"\[SUGGESTION\]\s*(.+)",
|
|
363
|
+
]
|
|
364
|
+
|
|
365
|
+
for pattern in patterns:
|
|
366
|
+
matches = re.findall(pattern, content, re.IGNORECASE | re.MULTILINE)
|
|
367
|
+
suggestions.extend(matches)
|
|
368
|
+
|
|
369
|
+
return list(set(suggestions))[:5]
|
|
370
|
+
|
|
371
|
+
def _determine_vote(self, content: str, issues: list[str], approvals: list[str]) -> str:
|
|
372
|
+
"""Determine agent's vote based on response."""
|
|
373
|
+
content_lower = content.lower()
|
|
374
|
+
|
|
375
|
+
# Explicit votes
|
|
376
|
+
if any(
|
|
377
|
+
word in content_lower for word in ["approved", "lgtm", "ship it", "looks good to me"]
|
|
378
|
+
):
|
|
379
|
+
return "approve"
|
|
380
|
+
if any(
|
|
381
|
+
word in content_lower for word in ["rejected", "do not merge", "needs work", "blocking"]
|
|
382
|
+
):
|
|
383
|
+
return "reject"
|
|
384
|
+
|
|
385
|
+
# Implicit from issues/approvals
|
|
386
|
+
if len(issues) > len(approvals):
|
|
387
|
+
return "reject"
|
|
388
|
+
if len(approvals) > 0 and len(issues) == 0:
|
|
389
|
+
return "approve"
|
|
390
|
+
|
|
391
|
+
return "abstain"
|
|
392
|
+
|
|
393
|
+
def _estimate_cost(self, tokens: int, model: str) -> float:
|
|
394
|
+
"""Estimate cost in USD."""
|
|
395
|
+
# Approximate pricing per 1M tokens
|
|
396
|
+
pricing = {
|
|
397
|
+
"opus": {"input": 15.0, "output": 75.0},
|
|
398
|
+
"sonnet": {"input": 3.0, "output": 15.0},
|
|
399
|
+
"haiku": {"input": 0.8, "output": 4.0},
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
rates = pricing.get(model, pricing["sonnet"])
|
|
403
|
+
# Assume 50/50 input/output split
|
|
404
|
+
cost = (tokens / 2 / 1_000_000 * rates["input"]) + (
|
|
405
|
+
tokens / 2 / 1_000_000 * rates["output"]
|
|
406
|
+
)
|
|
407
|
+
return cost
|
|
408
|
+
|
|
409
|
+
def _check_consensus(self, responses: list[AgentResponse]) -> bool:
|
|
410
|
+
"""Check if consensus is reached based on config."""
|
|
411
|
+
votes = [r.vote for r in responses if r.vote]
|
|
412
|
+
approvals = votes.count("approve")
|
|
413
|
+
rejects = votes.count("reject")
|
|
414
|
+
total = len(votes)
|
|
415
|
+
|
|
416
|
+
if total == 0:
|
|
417
|
+
return False
|
|
418
|
+
|
|
419
|
+
if self.config.consensus_type == ConsensusType.UNANIMOUS:
|
|
420
|
+
return approvals == total
|
|
421
|
+
|
|
422
|
+
elif self.config.consensus_type == ConsensusType.MAJORITY:
|
|
423
|
+
return approvals > total / 2
|
|
424
|
+
|
|
425
|
+
elif self.config.consensus_type == ConsensusType.QUORUM:
|
|
426
|
+
return approvals >= self.config.quorum_size
|
|
427
|
+
|
|
428
|
+
elif self.config.consensus_type == ConsensusType.REVIEWER_APPROVAL:
|
|
429
|
+
reviewer_responses = [r for r in responses if r.agent == "REVIEWER"]
|
|
430
|
+
if not reviewer_responses:
|
|
431
|
+
return approvals > rejects
|
|
432
|
+
return reviewer_responses[0].vote == "approve"
|
|
433
|
+
|
|
434
|
+
return False
|
|
435
|
+
|
|
436
|
+
def _collect_issues(self, responses: list[AgentResponse]) -> list[str]:
|
|
437
|
+
"""Collect all unique issues from responses."""
|
|
438
|
+
all_issues = []
|
|
439
|
+
for r in responses:
|
|
440
|
+
all_issues.extend(r.issues_found)
|
|
441
|
+
return list(set(all_issues))
|
|
442
|
+
|
|
443
|
+
def _build_iteration_prompt(
|
|
444
|
+
self,
|
|
445
|
+
agent: str,
|
|
446
|
+
task: str,
|
|
447
|
+
iteration: int,
|
|
448
|
+
previous_responses: list[AgentResponse],
|
|
449
|
+
issues_to_fix: list[str],
|
|
450
|
+
) -> str:
|
|
451
|
+
"""Build prompt for a specific iteration."""
|
|
452
|
+
|
|
453
|
+
if iteration == 0:
|
|
454
|
+
# First iteration - just the task
|
|
455
|
+
return f"""You are the {agent} agent. Your task is:
|
|
456
|
+
|
|
457
|
+
{task}
|
|
458
|
+
|
|
459
|
+
Please complete this task according to your role and expertise.
|
|
460
|
+
At the end, clearly indicate if you APPROVE or have ISSUES with the work.
|
|
461
|
+
"""
|
|
462
|
+
|
|
463
|
+
# Subsequent iterations - include feedback
|
|
464
|
+
feedback_lines = []
|
|
465
|
+
for r in previous_responses:
|
|
466
|
+
if r.agent == agent:
|
|
467
|
+
continue
|
|
468
|
+
|
|
469
|
+
feedback_lines.append(f"### Feedback from {r.agent}")
|
|
470
|
+
if r.issues_found:
|
|
471
|
+
feedback_lines.append("**Issues found:**")
|
|
472
|
+
for issue in r.issues_found:
|
|
473
|
+
feedback_lines.append(f"- ❌ {issue}")
|
|
474
|
+
if r.suggestions:
|
|
475
|
+
feedback_lines.append("**Suggestions:**")
|
|
476
|
+
for sug in r.suggestions:
|
|
477
|
+
feedback_lines.append(f"- 💡 {sug}")
|
|
478
|
+
if r.approvals:
|
|
479
|
+
feedback_lines.append("**Approvals:**")
|
|
480
|
+
for app in r.approvals:
|
|
481
|
+
feedback_lines.append(f"- ✅ {app}")
|
|
482
|
+
feedback_lines.append("")
|
|
483
|
+
|
|
484
|
+
prompt = f"""You are the {agent} agent. This is iteration {iteration + 1}.
|
|
485
|
+
|
|
486
|
+
## Original Task
|
|
487
|
+
{task}
|
|
488
|
+
|
|
489
|
+
## Feedback from Other Agents
|
|
490
|
+
{chr(10).join(feedback_lines)}
|
|
491
|
+
|
|
492
|
+
## Issues to Address
|
|
493
|
+
{chr(10).join(f"- {issue}" for issue in issues_to_fix) if issues_to_fix else "No outstanding issues."}
|
|
494
|
+
|
|
495
|
+
Please address the feedback and issues above.
|
|
496
|
+
At the end, clearly indicate if you APPROVE the current state or have remaining ISSUES.
|
|
497
|
+
"""
|
|
498
|
+
|
|
499
|
+
return prompt
|
|
500
|
+
|
|
501
|
+
def run_swarm(
|
|
502
|
+
self, agents: list[str], task: str, max_iterations: Optional[int] = None
|
|
503
|
+
) -> SwarmResult:
|
|
504
|
+
"""Run swarm orchestration with multiple agents."""
|
|
505
|
+
|
|
506
|
+
max_iter = max_iterations or self.config.max_iterations
|
|
507
|
+
start_time = datetime.now().isoformat()
|
|
508
|
+
|
|
509
|
+
self.state = SwarmState.RUNNING
|
|
510
|
+
self._log(f"Starting swarm with agents: {', '.join(agents)}")
|
|
511
|
+
self._log(f"Task: {task[:100]}...")
|
|
512
|
+
|
|
513
|
+
issues_to_fix: list[str] = []
|
|
514
|
+
previous_responses: list[AgentResponse] = []
|
|
515
|
+
|
|
516
|
+
for iteration in range(max_iter):
|
|
517
|
+
self._log(f"=== Iteration {iteration + 1}/{max_iter} ===")
|
|
518
|
+
self.state = SwarmState.DEBATING
|
|
519
|
+
|
|
520
|
+
# Check budget
|
|
521
|
+
if self.total_cost >= self.config.budget_limit_usd:
|
|
522
|
+
self._log("Budget limit reached!", "WARNING")
|
|
523
|
+
break
|
|
524
|
+
|
|
525
|
+
iter_responses: list[AgentResponse] = []
|
|
526
|
+
|
|
527
|
+
if self.config.parallel_execution and iteration == 0:
|
|
528
|
+
# Parallel execution for first iteration
|
|
529
|
+
with ThreadPoolExecutor(max_workers=len(agents)) as executor:
|
|
530
|
+
futures = {}
|
|
531
|
+
for agent in agents:
|
|
532
|
+
prompt = self._build_iteration_prompt(
|
|
533
|
+
agent, task, iteration, previous_responses, issues_to_fix
|
|
534
|
+
)
|
|
535
|
+
futures[executor.submit(self._invoke_agent, agent, prompt, iteration)] = (
|
|
536
|
+
agent
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
for future in as_completed(futures):
|
|
540
|
+
response = future.result()
|
|
541
|
+
iter_responses.append(response)
|
|
542
|
+
else:
|
|
543
|
+
# Sequential execution
|
|
544
|
+
for agent in agents:
|
|
545
|
+
prompt = self._build_iteration_prompt(
|
|
546
|
+
agent, task, iteration, previous_responses, issues_to_fix
|
|
547
|
+
)
|
|
548
|
+
response = self._invoke_agent(agent, prompt, iteration)
|
|
549
|
+
iter_responses.append(response)
|
|
550
|
+
|
|
551
|
+
# Record in shared memory
|
|
552
|
+
self.shared_memory.add(
|
|
553
|
+
agent=agent,
|
|
554
|
+
content=f"Iteration {iteration + 1}: {response.vote or 'no vote'}",
|
|
555
|
+
tags=["swarm", "iteration"],
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
# Collect issues
|
|
559
|
+
issues_to_fix = self._collect_issues(iter_responses)
|
|
560
|
+
|
|
561
|
+
# Check consensus
|
|
562
|
+
consensus = self._check_consensus(iter_responses)
|
|
563
|
+
|
|
564
|
+
swarm_iter = SwarmIteration(
|
|
565
|
+
iteration_num=iteration,
|
|
566
|
+
responses=iter_responses,
|
|
567
|
+
consensus_reached=consensus,
|
|
568
|
+
issues_remaining=issues_to_fix,
|
|
569
|
+
decisions_made=[f"{r.agent}: {r.vote}" for r in iter_responses],
|
|
570
|
+
)
|
|
571
|
+
self.iterations.append(swarm_iter)
|
|
572
|
+
|
|
573
|
+
previous_responses = iter_responses
|
|
574
|
+
|
|
575
|
+
self._log(f"Issues remaining: {len(issues_to_fix)}")
|
|
576
|
+
self._log(f"Consensus: {'✅ Yes' if consensus else '❌ No'}")
|
|
577
|
+
|
|
578
|
+
if consensus:
|
|
579
|
+
self.state = SwarmState.CONSENSUS
|
|
580
|
+
self._log("Consensus reached!", "SUCCESS")
|
|
581
|
+
break
|
|
582
|
+
|
|
583
|
+
# Determine final state
|
|
584
|
+
if self.state != SwarmState.CONSENSUS:
|
|
585
|
+
self.state = SwarmState.MAX_ITERATIONS
|
|
586
|
+
|
|
587
|
+
# Generate final output
|
|
588
|
+
final_output = self._generate_final_output(previous_responses)
|
|
589
|
+
|
|
590
|
+
# Create result
|
|
591
|
+
result = SwarmResult(
|
|
592
|
+
story_key=self.story_key,
|
|
593
|
+
task=task,
|
|
594
|
+
state=self.state,
|
|
595
|
+
iterations=self.iterations,
|
|
596
|
+
final_output=final_output,
|
|
597
|
+
agents_involved=agents,
|
|
598
|
+
total_tokens=self.total_tokens,
|
|
599
|
+
total_cost_usd=self.total_cost,
|
|
600
|
+
start_time=start_time,
|
|
601
|
+
end_time=datetime.now().isoformat(),
|
|
602
|
+
consensus_type=self.config.consensus_type,
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
# Save to knowledge graph
|
|
606
|
+
self.knowledge_graph.add_decision(
|
|
607
|
+
agent="SWARM",
|
|
608
|
+
topic="swarm-result",
|
|
609
|
+
decision=f"Completed with state: {self.state.value}",
|
|
610
|
+
context={"iterations": len(self.iterations), "cost": self.total_cost},
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
return result
|
|
614
|
+
|
|
615
|
+
def _generate_final_output(self, responses: list[AgentResponse]) -> str:
|
|
616
|
+
"""Generate consolidated final output."""
|
|
617
|
+
# Use the DEV response as primary, or last response
|
|
618
|
+
dev_response = next((r for r in responses if r.agent == "DEV"), None)
|
|
619
|
+
primary = dev_response or responses[-1] if responses else None
|
|
620
|
+
|
|
621
|
+
if not primary:
|
|
622
|
+
return "No output generated."
|
|
623
|
+
|
|
624
|
+
return primary.content
|
|
625
|
+
|
|
626
|
+
def run_iteration_loop(
|
|
627
|
+
self,
|
|
628
|
+
primary_agent: str,
|
|
629
|
+
reviewer_agent: str,
|
|
630
|
+
task: str,
|
|
631
|
+
max_iterations: Optional[int] = None,
|
|
632
|
+
) -> SwarmResult:
|
|
633
|
+
"""Run a simple iteration loop between two agents (e.g., DEV → REVIEWER → DEV)."""
|
|
634
|
+
return self.run_swarm(
|
|
635
|
+
agents=[primary_agent, reviewer_agent], task=task, max_iterations=max_iterations
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
# Convenience functions
|
|
640
|
+
def run_swarm(
|
|
641
|
+
story_key: str, agents: list[str], task: str, max_iterations: int = 3, **config_kwargs
|
|
642
|
+
) -> SwarmResult:
|
|
643
|
+
"""Quick function to run a swarm."""
|
|
644
|
+
config = SwarmConfig(**config_kwargs)
|
|
645
|
+
orchestrator = SwarmOrchestrator(story_key, config)
|
|
646
|
+
return orchestrator.run_swarm(agents, task, max_iterations)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def run_dev_review_loop(story_key: str, task: str, max_iterations: int = 3) -> SwarmResult:
|
|
650
|
+
"""Run a DEV → REVIEWER iteration loop."""
|
|
651
|
+
config = SwarmConfig(
|
|
652
|
+
max_iterations=max_iterations, consensus_type=ConsensusType.REVIEWER_APPROVAL
|
|
653
|
+
)
|
|
654
|
+
orchestrator = SwarmOrchestrator(story_key, config)
|
|
655
|
+
return orchestrator.run_iteration_loop("DEV", "REVIEWER", task, max_iterations)
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def run_architecture_review(story_key: str, task: str) -> SwarmResult:
|
|
659
|
+
"""Run an architecture review swarm."""
|
|
660
|
+
config = SwarmConfig(
|
|
661
|
+
max_iterations=2, consensus_type=ConsensusType.MAJORITY, parallel_execution=True
|
|
662
|
+
)
|
|
663
|
+
orchestrator = SwarmOrchestrator(story_key, config)
|
|
664
|
+
return orchestrator.run_swarm(["ARCHITECT", "DEV", "REVIEWER"], task)
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
if __name__ == "__main__":
|
|
668
|
+
print("=== Swarm Orchestrator Demo ===\n")
|
|
669
|
+
print("This module orchestrates multi-agent collaboration.")
|
|
670
|
+
print("\nExample usage:")
|
|
671
|
+
print("""
|
|
672
|
+
from lib.swarm_orchestrator import run_swarm, run_dev_review_loop
|
|
673
|
+
|
|
674
|
+
# Full swarm with multiple agents
|
|
675
|
+
result = run_swarm(
|
|
676
|
+
story_key="3-5",
|
|
677
|
+
agents=["ARCHITECT", "DEV", "REVIEWER"],
|
|
678
|
+
task="Design and implement user authentication",
|
|
679
|
+
max_iterations=3
|
|
680
|
+
)
|
|
681
|
+
print(result.to_summary())
|
|
682
|
+
|
|
683
|
+
# Simple DEV → REVIEWER loop
|
|
684
|
+
result = run_dev_review_loop(
|
|
685
|
+
story_key="3-5",
|
|
686
|
+
task="Implement login endpoint",
|
|
687
|
+
max_iterations=2
|
|
688
|
+
)
|
|
689
|
+
""")
|