@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.
Files changed (124) hide show
  1. package/CHANGELOG.md +526 -0
  2. package/LICENSE +21 -0
  3. package/README.md +620 -0
  4. package/bin/devflow-checkpoint.js +10 -0
  5. package/bin/devflow-collab.js +10 -0
  6. package/bin/devflow-cost.js +10 -0
  7. package/bin/devflow-create-persona.js +10 -0
  8. package/bin/devflow-init.js +10 -0
  9. package/bin/devflow-memory.js +10 -0
  10. package/bin/devflow-new-doc.js +10 -0
  11. package/bin/devflow-personalize.js +10 -0
  12. package/bin/devflow-setup-checkpoint.js +10 -0
  13. package/bin/devflow-story.js +10 -0
  14. package/bin/devflow-tech-debt.js +10 -0
  15. package/bin/devflow-validate-overrides.js +10 -0
  16. package/bin/devflow-validate.js +10 -0
  17. package/bin/devflow-version.js +10 -0
  18. package/lib/constants.js +30 -0
  19. package/lib/exec-python.js +78 -0
  20. package/lib/python-check.js +178 -0
  21. package/package.json +64 -0
  22. package/tooling/.automation/agents/architect.md +135 -0
  23. package/tooling/.automation/agents/ba.md +70 -0
  24. package/tooling/.automation/agents/dev.md +79 -0
  25. package/tooling/.automation/agents/maintainer.md +97 -0
  26. package/tooling/.automation/agents/pm.md +116 -0
  27. package/tooling/.automation/agents/reviewer.md +141 -0
  28. package/tooling/.automation/agents/sm.md +61 -0
  29. package/tooling/.automation/agents/writer.md +193 -0
  30. package/tooling/.automation/config.ps1.template +61 -0
  31. package/tooling/.automation/config.sh.template +48 -0
  32. package/tooling/.automation/memory/.gitkeep +6 -0
  33. package/tooling/.automation/memory/knowledge/kg_integration-test.json +94 -0
  34. package/tooling/.automation/memory/knowledge/kg_test-story.json +300 -0
  35. package/tooling/.automation/memory/shared/shared_integration-test.json +30 -0
  36. package/tooling/.automation/memory/shared/shared_test-story.json +78 -0
  37. package/tooling/.automation/overrides/templates/README.md +113 -0
  38. package/tooling/.automation/overrides/templates/architect/README.md +27 -0
  39. package/tooling/.automation/overrides/templates/architect/cloud-native.yaml +92 -0
  40. package/tooling/.automation/overrides/templates/architect/enterprise-architect.yaml +85 -0
  41. package/tooling/.automation/overrides/templates/architect/pragmatic-minimalist.yaml +88 -0
  42. package/tooling/.automation/overrides/templates/ba/README.md +27 -0
  43. package/tooling/.automation/overrides/templates/ba/agile-storyteller.yaml +86 -0
  44. package/tooling/.automation/overrides/templates/ba/domain-expert.yaml +91 -0
  45. package/tooling/.automation/overrides/templates/ba/requirements-engineer.yaml +89 -0
  46. package/tooling/.automation/overrides/templates/dev/README.md +32 -0
  47. package/tooling/.automation/overrides/templates/dev/junior-mentored.yaml +39 -0
  48. package/tooling/.automation/overrides/templates/dev/performance-engineer.yaml +43 -0
  49. package/tooling/.automation/overrides/templates/dev/rapid-prototyper.yaml +52 -0
  50. package/tooling/.automation/overrides/templates/dev/security-focused.yaml +43 -0
  51. package/tooling/.automation/overrides/templates/dev/senior-fullstack.yaml +39 -0
  52. package/tooling/.automation/overrides/templates/maintainer/README.md +27 -0
  53. package/tooling/.automation/overrides/templates/maintainer/devops-maintainer.yaml +113 -0
  54. package/tooling/.automation/overrides/templates/maintainer/legacy-steward.yaml +94 -0
  55. package/tooling/.automation/overrides/templates/maintainer/oss-maintainer.yaml +94 -0
  56. package/tooling/.automation/overrides/templates/pm/README.md +27 -0
  57. package/tooling/.automation/overrides/templates/pm/agile-pm.yaml +91 -0
  58. package/tooling/.automation/overrides/templates/pm/hybrid-delivery.yaml +87 -0
  59. package/tooling/.automation/overrides/templates/pm/traditional-pm.yaml +91 -0
  60. package/tooling/.automation/overrides/templates/reviewer/README.md +11 -0
  61. package/tooling/.automation/overrides/templates/reviewer/mentoring-reviewer.yaml +45 -0
  62. package/tooling/.automation/overrides/templates/reviewer/quick-sanity.yaml +50 -0
  63. package/tooling/.automation/overrides/templates/reviewer/thorough-critic.yaml +48 -0
  64. package/tooling/.automation/overrides/templates/sm/README.md +11 -0
  65. package/tooling/.automation/overrides/templates/sm/agile-coach.yaml +52 -0
  66. package/tooling/.automation/overrides/templates/sm/startup-pm.yaml +50 -0
  67. package/tooling/.automation/overrides/templates/sm/technical-lead.yaml +47 -0
  68. package/tooling/.automation/overrides/templates/user-profile.template.yaml +62 -0
  69. package/tooling/.automation/overrides/templates/writer/README.md +27 -0
  70. package/tooling/.automation/overrides/templates/writer/api-documentarian.yaml +99 -0
  71. package/tooling/.automation/overrides/templates/writer/docs-as-code.yaml +108 -0
  72. package/tooling/.automation/overrides/templates/writer/user-guide-author.yaml +100 -0
  73. package/tooling/completions/DevflowCompletion.ps1 +213 -0
  74. package/tooling/completions/_run-story +116 -0
  75. package/tooling/completions/run-story-completion.bash +136 -0
  76. package/tooling/docs/DOC-STANDARD.md +717 -0
  77. package/tooling/docs/sprint-status.yaml.template +24 -0
  78. package/tooling/docs/templates/bug-report.md +234 -0
  79. package/tooling/docs/templates/migration-spec.md +274 -0
  80. package/tooling/docs/templates/refactor-spec.md +86 -0
  81. package/tooling/docs/templates/tech-debt.md +86 -0
  82. package/tooling/scripts/context_checkpoint.py +556 -0
  83. package/tooling/scripts/cost_dashboard.py +617 -0
  84. package/tooling/scripts/create-persona.py +690 -0
  85. package/tooling/scripts/create-persona.sh +435 -0
  86. package/tooling/scripts/init-project-workflow.ps1 +651 -0
  87. package/tooling/scripts/init-project-workflow.py +70 -0
  88. package/tooling/scripts/init-project-workflow.sh +746 -0
  89. package/tooling/scripts/lib/__init__.py +35 -0
  90. package/tooling/scripts/lib/agent_handoff.py +526 -0
  91. package/tooling/scripts/lib/agent_router.py +698 -0
  92. package/tooling/scripts/lib/checkpoint-integration.ps1 +245 -0
  93. package/tooling/scripts/lib/checkpoint-integration.sh +191 -0
  94. package/tooling/scripts/lib/claude-cli.ps1 +952 -0
  95. package/tooling/scripts/lib/claude-cli.sh +1293 -0
  96. package/tooling/scripts/lib/cost_config.py +222 -0
  97. package/tooling/scripts/lib/cost_display.py +443 -0
  98. package/tooling/scripts/lib/cost_tracker.py +710 -0
  99. package/tooling/scripts/lib/currency_converter.py +328 -0
  100. package/tooling/scripts/lib/errors.py +438 -0
  101. package/tooling/scripts/lib/override-loader.sh +286 -0
  102. package/tooling/scripts/lib/pair_programming.py +589 -0
  103. package/tooling/scripts/lib/shared_memory.py +637 -0
  104. package/tooling/scripts/lib/swarm_orchestrator.py +689 -0
  105. package/tooling/scripts/memory_summarize.py +324 -0
  106. package/tooling/scripts/new-doc.ps1 +405 -0
  107. package/tooling/scripts/new-doc.py +93 -0
  108. package/tooling/scripts/new-doc.sh +534 -0
  109. package/tooling/scripts/personalize_agent.py +385 -0
  110. package/tooling/scripts/rollback-migration.sh +540 -0
  111. package/tooling/scripts/run-collab.ps1 +251 -0
  112. package/tooling/scripts/run-collab.py +605 -0
  113. package/tooling/scripts/run-collab.sh +110 -0
  114. package/tooling/scripts/run-story.ps1 +490 -0
  115. package/tooling/scripts/run-story.py +387 -0
  116. package/tooling/scripts/run-story.sh +467 -0
  117. package/tooling/scripts/setup-checkpoint-service.ps1 +219 -0
  118. package/tooling/scripts/setup-checkpoint-service.py +87 -0
  119. package/tooling/scripts/setup-checkpoint-service.sh +236 -0
  120. package/tooling/scripts/tech-debt-tracker.py +608 -0
  121. package/tooling/scripts/update_version.py +244 -0
  122. package/tooling/scripts/validate-overrides.py +511 -0
  123. package/tooling/scripts/validate-overrides.sh +432 -0
  124. 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
+ """)