@pjmendonca/devflow 1.13.2 → 1.19.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 (236) hide show
  1. package/.claude/commands/agent.md +1 -1
  2. package/.claude/commands/brainstorm.md +28 -0
  3. package/.claude/commands/bugfix.md +21 -0
  4. package/.claude/commands/checkpoint.md +0 -1
  5. package/.claude/commands/collab.md +0 -1
  6. package/.claude/commands/costs.md +88 -18
  7. package/.claude/commands/devflow.md +26 -0
  8. package/.claude/commands/handoff.md +0 -1
  9. package/.claude/commands/init.md +383 -0
  10. package/.claude/commands/memory.md +0 -1
  11. package/.claude/commands/pair.md +0 -1
  12. package/.claude/commands/review.md +27 -0
  13. package/.claude/commands/route.md +0 -1
  14. package/.claude/commands/swarm.md +0 -1
  15. package/.claude/commands/validate.md +55 -0
  16. package/.claude/hooks/session-notification.sh +44 -0
  17. package/.claude/hooks/session-startup.sh +427 -0
  18. package/.claude/hooks/session-stop.sh +38 -0
  19. package/.claude/hooks/session_tracker.py +272 -0
  20. package/.claude/settings.json +38 -0
  21. package/.claude/skills/brainstorm/SKILL.md +531 -0
  22. package/.claude/skills/costs/SKILL.md +156 -0
  23. package/.claude/skills/validate/SKILL.md +101 -0
  24. package/CHANGELOG.md +284 -0
  25. package/README.md +207 -10
  26. package/bin/devflow-install.js +2 -1
  27. package/bin/devflow.js +4 -0
  28. package/lib/constants.js +0 -1
  29. package/lib/exec-python.js +1 -1
  30. package/package.json +1 -1
  31. package/tooling/.automation/.checkpoint_lock +1 -0
  32. package/tooling/.automation/agents/architect.md +19 -0
  33. package/tooling/.automation/agents/ba.md +19 -0
  34. package/tooling/.automation/agents/maintainer.md +19 -0
  35. package/tooling/.automation/agents/pm.md +19 -0
  36. package/tooling/.automation/agents/reviewer.md +1 -1
  37. package/tooling/.automation/agents/writer.md +19 -0
  38. package/tooling/.automation/benchmarks/benchmark_20251230_100119.json +314 -0
  39. package/tooling/.automation/benchmarks/benchmark_20251230_100216.json +314 -0
  40. package/tooling/.automation/costs/config.json +31 -0
  41. package/tooling/.automation/costs/sessions/2025-12-29_20251229_164128.json +22 -0
  42. package/tooling/.automation/memory/knowledge/kg_integration-test.json +738 -1
  43. package/tooling/.automation/memory/knowledge/kg_test-story.json +3381 -2
  44. package/tooling/.automation/memory/shared/shared_integration-test.json +193 -1
  45. package/tooling/.automation/memory/shared/shared_test-story.json +757 -1
  46. package/tooling/.automation/memory/shared/shared_test.json +1332 -0
  47. package/tooling/.automation/memory/shared/shared_validation-check.json +240 -0
  48. package/tooling/.automation/overrides/templates/architect/cloud-native.yaml +5 -5
  49. package/tooling/.automation/overrides/templates/architect/enterprise-architect.yaml +23 -5
  50. package/tooling/.automation/overrides/templates/architect/pragmatic-minimalist.yaml +24 -6
  51. package/tooling/.automation/overrides/templates/ba/agile-storyteller.yaml +4 -4
  52. package/tooling/.automation/overrides/templates/ba/domain-expert.yaml +4 -4
  53. package/tooling/.automation/overrides/templates/ba/requirements-engineer.yaml +4 -4
  54. package/tooling/.automation/overrides/templates/dev/performance-engineer.yaml +18 -0
  55. package/tooling/.automation/overrides/templates/dev/rapid-prototyper.yaml +19 -1
  56. package/tooling/.automation/overrides/templates/dev/security-focused.yaml +18 -0
  57. package/tooling/.automation/overrides/templates/dev/user-advocate.yaml +54 -0
  58. package/tooling/.automation/overrides/templates/maintainer/devops-maintainer.yaml +4 -4
  59. package/tooling/.automation/overrides/templates/maintainer/legacy-steward.yaml +4 -4
  60. package/tooling/.automation/overrides/templates/maintainer/oss-maintainer.yaml +4 -4
  61. package/tooling/.automation/overrides/templates/maintainer/reliability-engineer.yaml +55 -0
  62. package/tooling/.automation/overrides/templates/pm/agile-pm.yaml +4 -4
  63. package/tooling/.automation/overrides/templates/pm/hybrid-delivery.yaml +3 -3
  64. package/tooling/.automation/overrides/templates/pm/traditional-pm.yaml +4 -4
  65. package/tooling/.automation/overrides/templates/reviewer/quick-sanity.yaml +18 -0
  66. package/tooling/.automation/overrides/templates/reviewer/thorough-critic.yaml +18 -0
  67. package/tooling/.automation/overrides/templates/sm/agile-coach.yaml +2 -2
  68. package/tooling/.automation/overrides/templates/sm/startup-pm.yaml +3 -3
  69. package/tooling/.automation/overrides/templates/writer/api-documentarian.yaml +5 -5
  70. package/tooling/.automation/overrides/templates/writer/docs-as-code.yaml +4 -4
  71. package/tooling/.automation/overrides/templates/writer/user-guide-author.yaml +5 -5
  72. package/tooling/.automation/validation/history/2025-12-29_val_002a28c1.json +32 -0
  73. package/tooling/.automation/validation/history/2025-12-29_val_01273bb1.json +32 -0
  74. package/tooling/.automation/validation/history/2025-12-29_val_03369914.json +41 -0
  75. package/tooling/.automation/validation/history/2025-12-29_val_07a449ba.json +32 -0
  76. package/tooling/.automation/validation/history/2025-12-29_val_0df1f0a2.json +41 -0
  77. package/tooling/.automation/validation/history/2025-12-29_val_10ff3d34.json +41 -0
  78. package/tooling/.automation/validation/history/2025-12-29_val_110771d7.json +32 -0
  79. package/tooling/.automation/validation/history/2025-12-29_val_13f3a7f9.json +32 -0
  80. package/tooling/.automation/validation/history/2025-12-29_val_17ba9d21.json +41 -0
  81. package/tooling/.automation/validation/history/2025-12-29_val_22247089.json +32 -0
  82. package/tooling/.automation/validation/history/2025-12-29_val_227ea6a4.json +32 -0
  83. package/tooling/.automation/validation/history/2025-12-29_val_2335d5ae.json +32 -0
  84. package/tooling/.automation/validation/history/2025-12-29_val_246824bb.json +41 -0
  85. package/tooling/.automation/validation/history/2025-12-29_val_28b4b9cd.json +32 -0
  86. package/tooling/.automation/validation/history/2025-12-29_val_2abd12cc.json +32 -0
  87. package/tooling/.automation/validation/history/2025-12-29_val_2c801b2f.json +59 -0
  88. package/tooling/.automation/validation/history/2025-12-29_val_2c8cfa8e.json +32 -0
  89. package/tooling/.automation/validation/history/2025-12-29_val_2ce76eb0.json +32 -0
  90. package/tooling/.automation/validation/history/2025-12-29_val_30351948.json +41 -0
  91. package/tooling/.automation/validation/history/2025-12-29_val_30eb7229.json +41 -0
  92. package/tooling/.automation/validation/history/2025-12-29_val_34df0e77.json +41 -0
  93. package/tooling/.automation/validation/history/2025-12-29_val_376e4d6a.json +32 -0
  94. package/tooling/.automation/validation/history/2025-12-29_val_3a4e8a1a.json +59 -0
  95. package/tooling/.automation/validation/history/2025-12-29_val_3b77a628.json +32 -0
  96. package/tooling/.automation/validation/history/2025-12-29_val_3ea4e1cf.json +59 -0
  97. package/tooling/.automation/validation/history/2025-12-29_val_44aacdb4.json +59 -0
  98. package/tooling/.automation/validation/history/2025-12-29_val_457ddfa8.json +32 -0
  99. package/tooling/.automation/validation/history/2025-12-29_val_45af6238.json +41 -0
  100. package/tooling/.automation/validation/history/2025-12-29_val_4735dba1.json +41 -0
  101. package/tooling/.automation/validation/history/2025-12-29_val_486b203c.json +41 -0
  102. package/tooling/.automation/validation/history/2025-12-29_val_49dc56cd.json +59 -0
  103. package/tooling/.automation/validation/history/2025-12-29_val_4d863d6d.json +32 -0
  104. package/tooling/.automation/validation/history/2025-12-29_val_5149a808.json +59 -0
  105. package/tooling/.automation/validation/history/2025-12-29_val_52e0bb43.json +32 -0
  106. package/tooling/.automation/validation/history/2025-12-29_val_585d6319.json +59 -0
  107. package/tooling/.automation/validation/history/2025-12-29_val_5b2d859a.json +32 -0
  108. package/tooling/.automation/validation/history/2025-12-29_val_635a7081.json +41 -0
  109. package/tooling/.automation/validation/history/2025-12-29_val_64df4905.json +32 -0
  110. package/tooling/.automation/validation/history/2025-12-29_val_70634cee.json +41 -0
  111. package/tooling/.automation/validation/history/2025-12-29_val_714553f9.json +32 -0
  112. package/tooling/.automation/validation/history/2025-12-29_val_7f7bfdbf.json +41 -0
  113. package/tooling/.automation/validation/history/2025-12-29_val_7faad91d.json +32 -0
  114. package/tooling/.automation/validation/history/2025-12-29_val_81821f8f.json +41 -0
  115. package/tooling/.automation/validation/history/2025-12-29_val_8249f3c9.json +32 -0
  116. package/tooling/.automation/validation/history/2025-12-29_val_8422b50f.json +41 -0
  117. package/tooling/.automation/validation/history/2025-12-29_val_8446c134.json +32 -0
  118. package/tooling/.automation/validation/history/2025-12-29_val_879f4e26.json +59 -0
  119. package/tooling/.automation/validation/history/2025-12-29_val_8b6d5bd7.json +32 -0
  120. package/tooling/.automation/validation/history/2025-12-29_val_8c5cd787.json +32 -0
  121. package/tooling/.automation/validation/history/2025-12-29_val_91d20bc7.json +32 -0
  122. package/tooling/.automation/validation/history/2025-12-29_val_958a12b7.json +41 -0
  123. package/tooling/.automation/validation/history/2025-12-29_val_95d91108.json +41 -0
  124. package/tooling/.automation/validation/history/2025-12-29_val_980dbb74.json +32 -0
  125. package/tooling/.automation/validation/history/2025-12-29_val_9e40c79b.json +32 -0
  126. package/tooling/.automation/validation/history/2025-12-29_val_9f499b7c.json +32 -0
  127. package/tooling/.automation/validation/history/2025-12-29_val_9f7c3b57.json +32 -0
  128. package/tooling/.automation/validation/history/2025-12-29_val_a30d5bd4.json +32 -0
  129. package/tooling/.automation/validation/history/2025-12-29_val_a6eb09c7.json +32 -0
  130. package/tooling/.automation/validation/history/2025-12-29_val_a86f7b83.json +41 -0
  131. package/tooling/.automation/validation/history/2025-12-29_val_ad5347e1.json +41 -0
  132. package/tooling/.automation/validation/history/2025-12-29_val_b0a5a993.json +32 -0
  133. package/tooling/.automation/validation/history/2025-12-29_val_bcb0192e.json +32 -0
  134. package/tooling/.automation/validation/history/2025-12-29_val_bf3c9aaa.json +32 -0
  135. package/tooling/.automation/validation/history/2025-12-29_val_c461ff88.json +32 -0
  136. package/tooling/.automation/validation/history/2025-12-29_val_c4f4e258.json +41 -0
  137. package/tooling/.automation/validation/history/2025-12-29_val_c7f0fa6d.json +41 -0
  138. package/tooling/.automation/validation/history/2025-12-29_val_c911b0e6.json +32 -0
  139. package/tooling/.automation/validation/history/2025-12-29_val_cc581964.json +32 -0
  140. package/tooling/.automation/validation/history/2025-12-29_val_cdd5a33b.json +32 -0
  141. package/tooling/.automation/validation/history/2025-12-29_val_cfd42495.json +32 -0
  142. package/tooling/.automation/validation/history/2025-12-29_val_d1c7a4ee.json +41 -0
  143. package/tooling/.automation/validation/history/2025-12-29_val_d2280d0e.json +32 -0
  144. package/tooling/.automation/validation/history/2025-12-29_val_d2a6ff69.json +32 -0
  145. package/tooling/.automation/validation/history/2025-12-29_val_d8c53ab2.json +59 -0
  146. package/tooling/.automation/validation/history/2025-12-29_val_d9c1247a.json +41 -0
  147. package/tooling/.automation/validation/history/2025-12-29_val_d9d58569.json +32 -0
  148. package/tooling/.automation/validation/history/2025-12-29_val_dabb4fd9.json +32 -0
  149. package/tooling/.automation/validation/history/2025-12-29_val_dd8fe359.json +32 -0
  150. package/tooling/.automation/validation/history/2025-12-29_val_decdffc9.json +32 -0
  151. package/tooling/.automation/validation/history/2025-12-29_val_e3a95476.json +59 -0
  152. package/tooling/.automation/validation/history/2025-12-29_val_e776dfca.json +32 -0
  153. package/tooling/.automation/validation/history/2025-12-29_val_ea70969f.json +59 -0
  154. package/tooling/.automation/validation/history/2025-12-29_val_ef41ea95.json +32 -0
  155. package/tooling/.automation/validation/history/2025-12-29_val_f384f9b1.json +32 -0
  156. package/tooling/.automation/validation/history/2025-12-29_val_f8adc38c.json +41 -0
  157. package/tooling/.automation/validation/history/2025-12-29_val_fa40b69e.json +32 -0
  158. package/tooling/.automation/validation/history/2025-12-29_val_fc538d54.json +41 -0
  159. package/tooling/.automation/validation/history/2025-12-29_val_fe814665.json +32 -0
  160. package/tooling/.automation/validation/history/2025-12-29_val_ffea4b12.json +32 -0
  161. package/tooling/.automation/validation/history/2025-12-30_val_02d001e5.json +59 -0
  162. package/tooling/.automation/validation/history/2025-12-30_val_0b8966dc.json +32 -0
  163. package/tooling/.automation/validation/history/2025-12-30_val_15455fbf.json +59 -0
  164. package/tooling/.automation/validation/history/2025-12-30_val_157e34b9.json +32 -0
  165. package/tooling/.automation/validation/history/2025-12-30_val_28d1d933.json +32 -0
  166. package/tooling/.automation/validation/history/2025-12-30_val_3442a52c.json +32 -0
  167. package/tooling/.automation/validation/history/2025-12-30_val_37f1ce1e.json +32 -0
  168. package/tooling/.automation/validation/history/2025-12-30_val_4f1d8a93.json +32 -0
  169. package/tooling/.automation/validation/history/2025-12-30_val_56ff1de3.json +32 -0
  170. package/tooling/.automation/validation/history/2025-12-30_val_664fd4e2.json +41 -0
  171. package/tooling/.automation/validation/history/2025-12-30_val_66afb0a7.json +32 -0
  172. package/tooling/.automation/validation/history/2025-12-30_val_7634663c.json +41 -0
  173. package/tooling/.automation/validation/history/2025-12-30_val_8ea830c3.json +41 -0
  174. package/tooling/.automation/validation/history/2025-12-30_val_998957c2.json +32 -0
  175. package/tooling/.automation/validation/history/2025-12-30_val_a52177db.json +32 -0
  176. package/tooling/.automation/validation/history/2025-12-30_val_a5b65a63.json +32 -0
  177. package/tooling/.automation/validation/history/2025-12-30_val_ae391d0e.json +32 -0
  178. package/tooling/.automation/validation/history/2025-12-30_val_c7895339.json +41 -0
  179. package/tooling/.automation/validation/history/2025-12-30_val_ca416593.json +41 -0
  180. package/tooling/.automation/validation/history/2025-12-30_val_cee19422.json +32 -0
  181. package/tooling/.automation/validation/history/2025-12-30_val_ddd4f4e6.json +32 -0
  182. package/tooling/.automation/validation/history/2025-12-30_val_f2e1394b.json +32 -0
  183. package/tooling/.automation/validation/history/2025-12-30_val_f4a7fa06.json +41 -0
  184. package/tooling/.automation/validation/history/2025-12-30_val_ffea3369.json +32 -0
  185. package/tooling/.automation/validation/history/2026-01-03_val_1287a74c.json +41 -0
  186. package/tooling/.automation/validation/history/2026-01-03_val_3b24071f.json +32 -0
  187. package/tooling/.automation/validation/history/2026-01-03_val_44d77573.json +32 -0
  188. package/tooling/.automation/validation/history/2026-01-03_val_5b31dc51.json +32 -0
  189. package/tooling/.automation/validation/history/2026-01-03_val_74267244.json +32 -0
  190. package/tooling/.automation/validation/history/2026-01-03_val_8b2d95c7.json +59 -0
  191. package/tooling/.automation/validation/history/2026-01-03_val_d875b297.json +41 -0
  192. package/tooling/.automation/validation-config.yaml +103 -0
  193. package/tooling/completions/DevflowCompletion.ps1 +21 -21
  194. package/tooling/completions/_run-story +3 -3
  195. package/tooling/completions/run-story-completion.bash +8 -8
  196. package/tooling/docs/DOC-STANDARD.md +14 -14
  197. package/tooling/docs/stories/.gitkeep +0 -0
  198. package/tooling/docs/templates/brainstorm-guide.md +314 -0
  199. package/tooling/docs/templates/migration-spec.md +4 -4
  200. package/tooling/docs/templates/story.md +66 -0
  201. package/tooling/scripts/context_checkpoint.py +5 -15
  202. package/tooling/scripts/cost_dashboard.py +610 -13
  203. package/tooling/scripts/create-persona.py +1 -12
  204. package/tooling/scripts/create-persona.sh +44 -44
  205. package/tooling/scripts/lib/__init__.py +12 -1
  206. package/tooling/scripts/lib/agent_handoff.py +11 -2
  207. package/tooling/scripts/lib/agent_router.py +31 -10
  208. package/tooling/scripts/lib/colors.py +106 -0
  209. package/tooling/scripts/lib/context_monitor.py +766 -0
  210. package/tooling/scripts/lib/cost_config.py +229 -10
  211. package/tooling/scripts/lib/cost_display.py +20 -45
  212. package/tooling/scripts/lib/cost_tracker.py +462 -15
  213. package/tooling/scripts/lib/currency_converter.py +28 -5
  214. package/tooling/scripts/lib/pair_programming.py +102 -3
  215. package/tooling/scripts/lib/personality_system.py +949 -0
  216. package/tooling/scripts/lib/platform.py +55 -0
  217. package/tooling/scripts/lib/shared_memory.py +9 -3
  218. package/tooling/scripts/lib/swarm_orchestrator.py +514 -75
  219. package/tooling/scripts/lib/validation_loop.py +1014 -0
  220. package/tooling/scripts/memory_summarize.py +9 -2
  221. package/tooling/scripts/new-doc.py +2 -9
  222. package/tooling/scripts/personalize_agent.py +1 -12
  223. package/tooling/scripts/rollback-migration.sh +60 -60
  224. package/tooling/scripts/run-collab.ps1 +16 -16
  225. package/tooling/scripts/run-collab.py +88 -53
  226. package/tooling/scripts/run-collab.sh +4 -4
  227. package/tooling/scripts/run-story.py +278 -20
  228. package/tooling/scripts/run-story.sh +3 -3
  229. package/tooling/scripts/setup-checkpoint-service.py +2 -9
  230. package/tooling/scripts/tech-debt-tracker.py +1 -12
  231. package/tooling/scripts/test_adversarial_swarm.py +452 -0
  232. package/tooling/scripts/validate-overrides.py +1 -10
  233. package/tooling/scripts/validate-overrides.sh +40 -40
  234. package/tooling/scripts/validate_loop.py +162 -0
  235. package/tooling/scripts/validate_setup.py +2 -30
  236. package/.claude/skills/init/SKILL.md +0 -496
@@ -1,20 +1,24 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Swarm Orchestrator - Multi-Agent Collaboration System
3
+ Swarm Orchestrator - Adversarial Multi-Agent Collaboration System
4
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
5
+ Orchestrates multiple agents in ADVERSARIAL debate mode where agents
6
+ take opposing stances and challenge each other's designs. This creates
7
+ robust solutions through competitive pressure.
11
8
 
12
9
  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
10
+ - Adversarial personas: Agents are assigned opposing viewpoints
11
+ - Dynamic personality selection: Task analysis determines best persona mix
12
+ - Convergence detection: Hybrid token/round limits with stability checks
13
+ - Personality handoff: Debate summary passed to implementing agent
14
+ - Cross-challenge rounds: Agents directly challenge each other
15
+
16
+ The adversarial approach:
17
+ 1. General agent (e.g., DEV) receives task
18
+ 2. Spawns sub-agents with opposing personas (e.g., Security vs Velocity)
19
+ 3. Agents debate in rounds, challenging each other's positions
20
+ 4. Convergence is forced after budget/round limits
21
+ 5. Synthesized result returned to implementing agent
18
22
 
19
23
  Usage:
20
24
  from lib.swarm_orchestrator import SwarmOrchestrator, SwarmConfig
@@ -23,13 +27,13 @@ Usage:
23
27
  result = orchestrator.run_swarm(
24
28
  agents=["ARCHITECT", "DEV", "REVIEWER"],
25
29
  task="Design and implement user authentication",
26
- max_iterations=3
30
+ max_iterations=5
27
31
  )
32
+ # result includes PersonalityHandoff with debate summary
28
33
  """
29
34
 
30
35
  import re
31
36
  import subprocess
32
- import sys
33
37
  from concurrent.futures import ThreadPoolExecutor, as_completed
34
38
  from dataclasses import dataclass, field
35
39
  from datetime import datetime
@@ -39,17 +43,87 @@ from typing import Optional
39
43
 
40
44
  # Import dependencies
41
45
  try:
46
+ from platform import IS_WINDOWS
47
+
42
48
  from agent_handoff import HandoffGenerator
43
49
  from agent_router import AgentRouter
50
+ from personality_system import (
51
+ ConvergenceDetector,
52
+ DebatePosition,
53
+ PersonalityHandoff,
54
+ PersonalityProfile,
55
+ PersonalitySelector,
56
+ extract_arguments_from_response,
57
+ )
44
58
  from shared_memory import get_knowledge_graph, get_shared_memory
45
59
  except ImportError:
46
60
  from lib.agent_handoff import HandoffGenerator
47
61
  from lib.agent_router import AgentRouter
62
+ from lib.personality_system import (
63
+ ConvergenceDetector,
64
+ DebatePosition,
65
+ PersonalityHandoff,
66
+ PersonalityProfile,
67
+ PersonalitySelector,
68
+ extract_arguments_from_response,
69
+ )
70
+ from lib.platform import IS_WINDOWS
48
71
  from lib.shared_memory import get_knowledge_graph, get_shared_memory
49
72
 
73
+ # Try to import validation loop
74
+ try:
75
+ from validation_loop import (
76
+ INTER_PHASE_GATES,
77
+ LoopContext,
78
+ ValidationLoop,
79
+ )
80
+
81
+ HAS_VALIDATION = True
82
+ except ImportError:
83
+ try:
84
+ from lib.validation_loop import (
85
+ INTER_PHASE_GATES,
86
+ LoopContext,
87
+ ValidationLoop,
88
+ )
89
+
90
+ HAS_VALIDATION = True
91
+ except ImportError:
92
+ HAS_VALIDATION = False
93
+
50
94
 
51
95
  PROJECT_ROOT = Path(__file__).parent.parent.parent.parent
52
- CLAUDE_CLI = "claude.cmd" if sys.platform == "win32" else "claude"
96
+ CLAUDE_CLI = "claude.cmd" if IS_WINDOWS else "claude"
97
+
98
+ # Security: Maximum prompt length to prevent resource exhaustion
99
+ MAX_PROMPT_LENGTH = 500_000 # ~500KB
100
+
101
+
102
+ def _sanitize_prompt(prompt: str) -> str:
103
+ """Sanitize prompt for safe subprocess execution.
104
+
105
+ - Removes null bytes and control characters (except newlines/tabs)
106
+ - Truncates to maximum length
107
+ - Ensures valid UTF-8
108
+
109
+ Args:
110
+ prompt: Raw prompt string
111
+
112
+ Returns:
113
+ Sanitized prompt safe for subprocess
114
+ """
115
+ if not prompt:
116
+ return ""
117
+
118
+ # Remove null bytes and control characters (keep newlines, tabs, and printable chars)
119
+ # Keep: \n (10), \t (9), \r (13), and all chars >= 32 (printable ASCII + UTF-8)
120
+ sanitized = "".join(char for char in prompt if char in "\n\t\r" or ord(char) >= 32)
121
+
122
+ # Truncate if too long
123
+ if len(sanitized) > MAX_PROMPT_LENGTH:
124
+ sanitized = sanitized[:MAX_PROMPT_LENGTH] + "\n[TRUNCATED]"
125
+
126
+ return sanitized
53
127
 
54
128
 
55
129
  class SwarmState(Enum):
@@ -76,7 +150,7 @@ class ConsensusType(Enum):
76
150
 
77
151
  @dataclass
78
152
  class AgentResponse:
79
- """Response from an agent."""
153
+ """Response from an agent in adversarial debate."""
80
154
 
81
155
  agent: str
82
156
  model: str
@@ -89,6 +163,11 @@ class AgentResponse:
89
163
  approvals: list[str] = field(default_factory=list)
90
164
  suggestions: list[str] = field(default_factory=list)
91
165
  vote: Optional[str] = None # approve, reject, abstain
166
+ # Adversarial debate fields
167
+ persona_name: Optional[str] = None
168
+ challenges_raised: list[str] = field(default_factory=list)
169
+ concessions_made: list[str] = field(default_factory=list)
170
+ key_arguments: list[str] = field(default_factory=list)
92
171
 
93
172
  def to_dict(self) -> dict:
94
173
  return {
@@ -103,6 +182,9 @@ class AgentResponse:
103
182
  "approvals": self.approvals,
104
183
  "suggestions": self.suggestions,
105
184
  "vote": self.vote,
185
+ "persona_name": self.persona_name,
186
+ "challenges_raised": self.challenges_raised,
187
+ "key_arguments": self.key_arguments[:5],
106
188
  }
107
189
 
108
190
 
@@ -128,7 +210,7 @@ class SwarmIteration:
128
210
 
129
211
  @dataclass
130
212
  class SwarmResult:
131
- """Result of swarm orchestration."""
213
+ """Result of adversarial swarm orchestration."""
132
214
 
133
215
  story_key: str
134
216
  task: str
@@ -141,6 +223,9 @@ class SwarmResult:
141
223
  start_time: str
142
224
  end_time: str
143
225
  consensus_type: ConsensusType
226
+ # Adversarial debate results
227
+ personality_handoff: Optional[PersonalityHandoff] = None
228
+ termination_reason: str = "max_iterations" # consensus, convergence, budget, max_iterations
144
229
 
145
230
  def to_dict(self) -> dict:
146
231
  return {
@@ -155,25 +240,63 @@ class SwarmResult:
155
240
  "start_time": self.start_time,
156
241
  "end_time": self.end_time,
157
242
  "consensus_type": self.consensus_type.value,
243
+ "termination_reason": self.termination_reason,
244
+ "personality_handoff": (
245
+ self.personality_handoff.to_dict() if self.personality_handoff else None
246
+ ),
158
247
  }
159
248
 
160
249
  def to_summary(self) -> str:
161
- """Generate a human-readable summary."""
250
+ """Generate a human-readable summary of the adversarial debate."""
162
251
  lines = [
163
- f"## Swarm Result: {self.story_key}",
252
+ f"## Adversarial Swarm Result: {self.story_key}",
164
253
  "",
165
254
  f"**Task**: {self.task}",
166
255
  f"**State**: {self.state.value}",
167
- f"**Iterations**: {len(self.iterations)}",
168
- f"**Agents**: {', '.join(self.agents_involved)}",
256
+ f"**Debate Rounds**: {len(self.iterations)}",
257
+ f"**Termination**: {self.termination_reason}",
169
258
  f"**Total Cost**: ${self.total_cost_usd:.4f}",
170
259
  "",
171
260
  ]
172
261
 
262
+ # Show participating personas
263
+ if self.personality_handoff:
264
+ lines.append("### Adversarial Personas")
265
+ for p in self.personality_handoff.selected_personas:
266
+ stance = (
267
+ f" [Focus: {p.adversarial_stance.primary_concern}]"
268
+ if p.adversarial_stance
269
+ else ""
270
+ )
271
+ lines.append(f"- **{p.name}** ({p.agent_type}){stance}")
272
+ lines.append("")
273
+
173
274
  if self.state == SwarmState.CONSENSUS:
174
- lines.append(" **Consensus reached!**")
275
+ lines.append("[CONSENSUS] Agents reached agreement!")
276
+ elif self.termination_reason == "convergence":
277
+ lines.append("[CONVERGED] Positions stabilized - forcing consensus")
278
+ elif self.termination_reason == "budget":
279
+ lines.append("[BUDGET] Token budget reached - forcing consensus")
175
280
  elif self.state == SwarmState.MAX_ITERATIONS:
176
- lines.append(" **Max iterations reached without full consensus**")
281
+ lines.append("[MAX ROUNDS] Iteration limit reached")
282
+
283
+ # Show consensus points and tensions
284
+ if self.personality_handoff:
285
+ if self.personality_handoff.consensus_points:
286
+ lines.append("")
287
+ lines.append("### Points of Agreement")
288
+ for point in self.personality_handoff.consensus_points:
289
+ lines.append(f"- {point}")
290
+
291
+ if self.personality_handoff.unresolved_tensions:
292
+ lines.append("")
293
+ lines.append("### Unresolved Tensions")
294
+ for tension in self.personality_handoff.unresolved_tensions:
295
+ lines.append(f"- [WARNING] {tension}")
296
+
297
+ lines.append("")
298
+ lines.append("### Recommended Approach")
299
+ lines.append(self.personality_handoff.recommended_approach)
177
300
 
178
301
  lines.append("")
179
302
  lines.append("### Final Output")
@@ -186,16 +309,22 @@ class SwarmResult:
186
309
 
187
310
  @dataclass
188
311
  class SwarmConfig:
189
- """Configuration for swarm orchestration."""
312
+ """Configuration for adversarial swarm orchestration."""
190
313
 
191
- max_iterations: int = 3
192
- consensus_type: ConsensusType = ConsensusType.REVIEWER_APPROVAL
314
+ max_iterations: int = 3 # Limited to 3 rounds - diminishing returns after this
315
+ consensus_type: ConsensusType = ConsensusType.MAJORITY
193
316
  quorum_size: int = 2 # For QUORUM type
194
317
  timeout_seconds: int = 300
195
318
  parallel_execution: bool = False
196
319
  auto_fix_enabled: bool = True # DEV automatically addresses REVIEWER issues
197
320
  verbose: bool = True
198
- budget_limit_usd: float = 20.0
321
+ budget_limit_usd: float = 25.0 # Higher budget for debate
322
+ validation_enabled: bool = True # Enable inter-iteration validation
323
+ # Adversarial-specific settings
324
+ convergence_threshold: float = 0.8 # Position similarity to consider converged
325
+ stability_rounds: int = 2 # Rounds of stability before convergence
326
+ force_consensus_on_budget: bool = True # Force agreement when budget exhausted
327
+ cross_challenge_enabled: bool = True # Agents directly challenge each other
199
328
 
200
329
  def to_dict(self) -> dict:
201
330
  return {
@@ -206,11 +335,20 @@ class SwarmConfig:
206
335
  "parallel_execution": self.parallel_execution,
207
336
  "auto_fix_enabled": self.auto_fix_enabled,
208
337
  "budget_limit_usd": self.budget_limit_usd,
338
+ "validation_enabled": self.validation_enabled,
339
+ "convergence_threshold": self.convergence_threshold,
340
+ "stability_rounds": self.stability_rounds,
341
+ "cross_challenge_enabled": self.cross_challenge_enabled,
209
342
  }
210
343
 
211
344
 
212
345
  class SwarmOrchestrator:
213
- """Orchestrates multi-agent collaboration."""
346
+ """Orchestrates adversarial multi-agent collaboration.
347
+
348
+ Agents are assigned opposing personas and debate until consensus
349
+ or convergence is reached. This creates robust solutions through
350
+ competitive pressure and cross-examination.
351
+ """
214
352
 
215
353
  def __init__(self, story_key: str, config: Optional[SwarmConfig] = None):
216
354
  self.story_key = story_key
@@ -225,8 +363,19 @@ class SwarmOrchestrator:
225
363
  self.iterations: list[SwarmIteration] = []
226
364
  self.total_tokens = 0
227
365
  self.total_cost = 0.0
366
+ self.termination_reason = "max_iterations"
367
+
368
+ # Adversarial personality system
369
+ self.personality_selector = PersonalitySelector()
370
+ self.convergence_detector = ConvergenceDetector(
371
+ similarity_threshold=self.config.convergence_threshold,
372
+ stability_rounds=self.config.stability_rounds,
373
+ )
374
+ self.selected_personas: list[PersonalityProfile] = []
375
+ self.agent_personas: dict[str, PersonalityProfile] = {}
376
+ self.debate_positions: dict[str, DebatePosition] = {}
228
377
 
229
- # Agent model mapping
378
+ # Agent model mapping (can be overridden by persona)
230
379
  self.agent_models = {
231
380
  "SM": "sonnet",
232
381
  "DEV": "opus",
@@ -239,6 +388,20 @@ class SwarmOrchestrator:
239
388
  "SECURITY": "opus",
240
389
  }
241
390
 
391
+ # Initialize validation loop if available and enabled
392
+ self.validation_loop = None
393
+ self.validation_context = None
394
+ if HAS_VALIDATION and self.config.validation_enabled:
395
+ self.validation_loop = ValidationLoop(
396
+ gates=INTER_PHASE_GATES,
397
+ config={"auto_fix_enabled": self.config.auto_fix_enabled},
398
+ story_key=story_key,
399
+ )
400
+ self.validation_context = LoopContext(
401
+ story_key=story_key,
402
+ max_iterations=self.config.max_iterations,
403
+ )
404
+
242
405
  def _log(self, message: str, level: str = "INFO"):
243
406
  """Log a message with timestamp."""
244
407
  if self.config.verbose:
@@ -252,15 +415,54 @@ class SwarmOrchestrator:
252
415
  }.get(level, "•")
253
416
  print(f"[{timestamp}] {emoji} {message}")
254
417
 
418
+ def _select_adversarial_personas(self, agents: list[str], task: str):
419
+ """Select adversarial personas for the given agents and task."""
420
+ self._log("Selecting adversarial personas for debate...")
421
+
422
+ # Get personas from personality selector
423
+ self.selected_personas = self.personality_selector.select_adversarial_personas(
424
+ task=task,
425
+ num_agents=len(agents),
426
+ required_agents=agents,
427
+ )
428
+
429
+ # Map personas to agents
430
+ for i, agent in enumerate(agents):
431
+ if i < len(self.selected_personas):
432
+ persona = self.selected_personas[i]
433
+ self.agent_personas[agent] = persona
434
+ # Override model if persona specifies
435
+ if persona.model:
436
+ self.agent_models[agent] = persona.model
437
+ self._log(
438
+ f" {agent} -> {persona.name} "
439
+ f"[{persona.adversarial_stance.primary_concern if persona.adversarial_stance else 'general'}]"
440
+ )
441
+
442
+ # Initialize debate position tracking
443
+ self.debate_positions[agent] = DebatePosition(
444
+ agent=agent,
445
+ persona_name=persona.name,
446
+ )
447
+
255
448
  def _invoke_agent(self, agent: str, prompt: str, iteration: int = 0) -> AgentResponse:
256
- """Invoke a single agent with Claude CLI."""
449
+ """Invoke a single agent with Claude CLI, including adversarial persona."""
257
450
  model = self.agent_models.get(agent, "sonnet")
451
+ persona = self.agent_personas.get(agent)
452
+ persona_name = persona.name if persona else None
258
453
 
259
- self._log(f"Invoking {agent} (model: {model})...")
454
+ persona_label = f" as '{persona_name}'" if persona_name else ""
455
+ self._log(f"Invoking {agent}{persona_label} (model: {model})...")
260
456
 
261
- # Build the full prompt with context
457
+ # Build the full prompt with context and persona
262
458
  context = self.handoff_generator.generate_context_for_agent(agent)
263
- full_prompt = f"{context}\n\n---\n\n{prompt}"
459
+
460
+ # Inject persona if available
461
+ persona_injection = ""
462
+ if persona:
463
+ persona_injection = persona.to_prompt_injection() + "\n\n---\n\n"
464
+
465
+ full_prompt = _sanitize_prompt(f"{persona_injection}{context}\n\n---\n\n{prompt}")
264
466
 
265
467
  try:
266
468
  result = subprocess.run(
@@ -279,6 +481,18 @@ class SwarmOrchestrator:
279
481
  suggestions = self._extract_suggestions(content)
280
482
  vote = self._determine_vote(content, issues, approvals)
281
483
 
484
+ # Extract adversarial elements
485
+ key_arguments = extract_arguments_from_response(content)
486
+ challenges = self._extract_challenges(content)
487
+ concessions = self._extract_concessions(content)
488
+
489
+ # Update debate position
490
+ if agent in self.debate_positions:
491
+ pos = self.debate_positions[agent]
492
+ pos.key_arguments = key_arguments
493
+ pos.challenges_raised = challenges
494
+ pos.concessions_made.extend(concessions)
495
+
282
496
  # Estimate tokens (rough)
283
497
  tokens = len(full_prompt.split()) + len(content.split())
284
498
  cost = self._estimate_cost(tokens, model)
@@ -298,6 +512,10 @@ class SwarmOrchestrator:
298
512
  approvals=approvals,
299
513
  suggestions=suggestions,
300
514
  vote=vote,
515
+ persona_name=persona_name,
516
+ challenges_raised=challenges,
517
+ concessions_made=concessions,
518
+ key_arguments=key_arguments,
301
519
  )
302
520
 
303
521
  except subprocess.TimeoutExpired:
@@ -309,6 +527,7 @@ class SwarmOrchestrator:
309
527
  timestamp=datetime.now().isoformat(),
310
528
  iteration=iteration,
311
529
  vote="abstain",
530
+ persona_name=persona_name,
312
531
  )
313
532
  except Exception as e:
314
533
  self._log(f"{agent} failed: {e}", "ERROR")
@@ -319,8 +538,39 @@ class SwarmOrchestrator:
319
538
  timestamp=datetime.now().isoformat(),
320
539
  iteration=iteration,
321
540
  vote="abstain",
541
+ persona_name=persona_name,
322
542
  )
323
543
 
544
+ def _extract_challenges(self, content: str) -> list[str]:
545
+ """Extract challenges/objections from adversarial response."""
546
+ challenges = []
547
+ patterns = [
548
+ r"(?:I challenge|I disagree|I object|However|But|My concern is)[\s:]+(.+?)(?:\.|$)",
549
+ r"(?:This ignores|This overlooks|What about)[\s:]+(.+?)(?:\.|$)",
550
+ r"\[CHALLENGE\]\s*(.+)",
551
+ ]
552
+
553
+ for pattern in patterns:
554
+ matches = re.findall(pattern, content, re.IGNORECASE | re.MULTILINE)
555
+ challenges.extend(matches)
556
+
557
+ return list({c.strip() for c in challenges if len(c) > 10})[:5]
558
+
559
+ def _extract_concessions(self, content: str) -> list[str]:
560
+ """Extract concessions from adversarial response."""
561
+ concessions = []
562
+ patterns = [
563
+ r"(?:I concede|I agree that|Fair point|You're right that|I accept)[\s:]+(.+?)(?:\.|$)",
564
+ r"(?:On reflection|Reconsidering)[\s:]+(.+?)(?:\.|$)",
565
+ r"\[CONCEDE\]\s*(.+)",
566
+ ]
567
+
568
+ for pattern in patterns:
569
+ matches = re.findall(pattern, content, re.IGNORECASE | re.MULTILINE)
570
+ concessions.extend(matches)
571
+
572
+ return list({c.strip() for c in concessions if len(c) > 10})[:5]
573
+
324
574
  def _extract_issues(self, content: str) -> list[str]:
325
575
  """Extract issues/problems from response."""
326
576
  issues = []
@@ -440,6 +690,40 @@ class SwarmOrchestrator:
440
690
  all_issues.extend(r.issues_found)
441
691
  return list(set(all_issues))
442
692
 
693
+ def _run_iteration_validation(self, iteration: int, responses: list[AgentResponse]) -> bool:
694
+ """Run validation between iterations.
695
+
696
+ Args:
697
+ iteration: Current iteration number
698
+ responses: Agent responses from this iteration
699
+
700
+ Returns:
701
+ True if validation passed, False otherwise
702
+ """
703
+ if not self.validation_loop or not self.validation_context:
704
+ return True
705
+
706
+ self.validation_context.iteration = iteration
707
+ self.validation_context.phase = f"iteration_{iteration}"
708
+
709
+ # Update context with iteration data
710
+ self.validation_context.accumulated_issues = self._collect_issues(responses)
711
+
712
+ report = self.validation_loop.run_gates(self.validation_context, tier=2)
713
+
714
+ if report.passed:
715
+ self._log(f"[VALIDATION] Iteration {iteration + 1} passed validation")
716
+ return True
717
+ else:
718
+ # Log warnings but don't block swarm - just record issues
719
+ for failure in report.failures:
720
+ self._log(f"[VALIDATION] {failure.gate_name}: {failure.message}", "WARN")
721
+ # Add to accumulated issues for next iteration
722
+ self.validation_context.accumulated_issues.append(
723
+ f"Validation: {failure.gate_name} - {failure.message}"
724
+ )
725
+ return True # Don't block swarm, just inform
726
+
443
727
  def _build_iteration_prompt(
444
728
  self,
445
729
  agent: str,
@@ -448,52 +732,110 @@ class SwarmOrchestrator:
448
732
  previous_responses: list[AgentResponse],
449
733
  issues_to_fix: list[str],
450
734
  ) -> str:
451
- """Build prompt for a specific iteration."""
735
+ """Build adversarial debate prompt for a specific iteration."""
736
+ persona = self.agent_personas.get(agent)
737
+ stance_info = ""
738
+ if persona and persona.adversarial_stance:
739
+ stance = persona.adversarial_stance
740
+ stance_info = f"""
741
+ Your stance in this debate:
742
+ - Primary concern: {stance.primary_concern}
743
+ - Challenge arguments that prioritize: {', '.join(stance.opposes) if stance.opposes else 'shortcuts or poor quality'}
744
+ - Debate style: {stance.debate_style}
745
+ """
452
746
 
453
747
  if iteration == 0:
454
- # First iteration - just the task
455
- return f"""You are the {agent} agent. Your task is:
748
+ # First iteration - state position and challenge others
749
+ return f"""## ADVERSARIAL DEBATE - Round 1
750
+
751
+ You are the {agent} agent participating in an adversarial design debate.
752
+ Your goal is to advocate for your perspective while challenging others.
456
753
 
754
+ ### The Task
457
755
  {task}
458
756
 
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.
757
+ {stance_info}
758
+
759
+ ### Instructions
760
+ 1. State your position on how to approach this task
761
+ 2. Identify potential issues, risks, or overlooked concerns
762
+ 3. Be specific about what approach you advocate for and WHY
763
+ 4. Anticipate counter-arguments and address them preemptively
764
+
765
+ ### Response Format
766
+ Structure your response with:
767
+ - **My Position**: Your recommended approach
768
+ - **Key Arguments**: Why this is the right approach (be specific)
769
+ - **Concerns**: What could go wrong with other approaches
770
+ - **Challenges for Others**: Questions or objections for other agents to address
771
+
772
+ At the end, indicate: APPROVE (if you'd accept current direction) or ISSUES (if concerns remain)
461
773
  """
462
774
 
463
- # Subsequent iterations - include feedback
464
- feedback_lines = []
775
+ # Subsequent iterations - adversarial cross-challenge
776
+ other_positions = []
465
777
  for r in previous_responses:
466
778
  if r.agent == agent:
467
779
  continue
468
780
 
469
- feedback_lines.append(f"### Feedback from {r.agent}")
781
+ persona_label = f" ({r.persona_name})" if r.persona_name else ""
782
+
783
+ position_block = [f"### {r.agent}{persona_label}"]
784
+
785
+ # Show their key arguments
786
+ if r.key_arguments:
787
+ position_block.append("**Their Arguments:**")
788
+ for arg in r.key_arguments[:3]:
789
+ position_block.append(f"- {arg}")
790
+
791
+ # Show challenges they raised
792
+ if r.challenges_raised:
793
+ position_block.append("**Challenges They Raised:**")
794
+ for challenge in r.challenges_raised[:3]:
795
+ position_block.append(f"- {challenge}")
796
+
797
+ # Show their issues
470
798
  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
799
+ position_block.append("**Issues They Identified:**")
800
+ for issue in r.issues_found[:3]:
801
+ position_block.append(f"- {issue}")
802
+
803
+ # Show their vote
804
+ if r.vote:
805
+ position_block.append(f"**Their Vote**: {r.vote.upper()}")
806
+
807
+ position_block.append("")
808
+ other_positions.append("\n".join(position_block))
809
+
810
+ prompt = f"""## ADVERSARIAL DEBATE - Round {iteration + 1}
811
+
812
+ You are the {agent} agent. This is round {iteration + 1} of the debate.
813
+
814
+ ### Original Task
487
815
  {task}
488
816
 
489
- ## Feedback from Other Agents
490
- {chr(10).join(feedback_lines)}
817
+ {stance_info}
818
+
819
+ ### Other Agents' Positions
820
+ {chr(10).join(other_positions) if other_positions else "No other positions yet."}
491
821
 
492
- ## Issues to Address
493
- {chr(10).join(f"- {issue}" for issue in issues_to_fix) if issues_to_fix else "No outstanding issues."}
822
+ ### Outstanding Issues
823
+ {chr(10).join(f"- {issue}" for issue in issues_to_fix) if issues_to_fix else "None identified."}
494
824
 
495
- Please address the feedback and issues above.
496
- At the end, clearly indicate if you APPROVE the current state or have remaining ISSUES.
825
+ ### Your Task This Round
826
+ 1. **Respond to challenges**: Address objections raised against your position
827
+ 2. **Challenge others**: Push back on weak arguments or overlooked concerns
828
+ 3. **Find common ground**: Identify points of agreement where possible
829
+ 4. **Refine your position**: Adjust based on valid critiques (concede if warranted)
830
+
831
+ ### Response Format
832
+ - **Response to Challenges**: Address specific objections to your position
833
+ - **Counter-Arguments**: Challenge weak points in others' positions
834
+ - **Concessions**: Points you're willing to concede (use [CONCEDE] prefix)
835
+ - **Updated Position**: Your refined stance after this round
836
+ - **Remaining Concerns**: Issues that still need resolution
837
+
838
+ End with: APPROVE (ready to move forward) or ISSUES (significant concerns remain)
497
839
  """
498
840
 
499
841
  return prompt
@@ -501,25 +843,40 @@ At the end, clearly indicate if you APPROVE the current state or have remaining
501
843
  def run_swarm(
502
844
  self, agents: list[str], task: str, max_iterations: Optional[int] = None
503
845
  ) -> SwarmResult:
504
- """Run swarm orchestration with multiple agents."""
846
+ """Run adversarial swarm orchestration with multiple agents.
847
+
848
+ Agents are assigned opposing personas and debate until consensus,
849
+ convergence, or budget exhaustion.
850
+ """
505
851
 
506
852
  max_iter = max_iterations or self.config.max_iterations
507
853
  start_time = datetime.now().isoformat()
508
854
 
509
855
  self.state = SwarmState.RUNNING
510
- self._log(f"Starting swarm with agents: {', '.join(agents)}")
856
+ self._log(f"Starting ADVERSARIAL swarm with agents: {', '.join(agents)}")
511
857
  self._log(f"Task: {task[:100]}...")
512
858
 
859
+ # Select adversarial personas for each agent
860
+ self._select_adversarial_personas(agents, task)
861
+
513
862
  issues_to_fix: list[str] = []
514
863
  previous_responses: list[AgentResponse] = []
515
864
 
516
865
  for iteration in range(max_iter):
517
- self._log(f"=== Iteration {iteration + 1}/{max_iter} ===")
866
+ self._log(f"=== Debate Round {iteration + 1}/{max_iter} ===")
518
867
  self.state = SwarmState.DEBATING
519
868
 
520
- # Check budget
869
+ # Check budget limit
521
870
  if self.total_cost >= self.config.budget_limit_usd:
522
- self._log("Budget limit reached!", "WARNING")
871
+ self._log("[BUDGET] Budget limit reached - forcing consensus", "WARNING")
872
+ self.termination_reason = "budget"
873
+ break
874
+
875
+ # Check convergence (hybrid: positions stabilized)
876
+ if iteration > 0 and self.convergence_detector.has_converged():
877
+ self._log("[CONVERGED] Positions have stabilized - ending debate", "SUCCESS")
878
+ self.termination_reason = "convergence"
879
+ self.state = SwarmState.CONVERGING
523
880
  break
524
881
 
525
882
  iter_responses: list[AgentResponse] = []
@@ -558,6 +915,16 @@ At the end, clearly indicate if you APPROVE the current state or have remaining
558
915
  # Collect issues
559
916
  issues_to_fix = self._collect_issues(iter_responses)
560
917
 
918
+ # Record positions for convergence detection
919
+ round_positions = {}
920
+ for r in iter_responses:
921
+ if r.agent in self.debate_positions:
922
+ round_positions[r.agent] = self.debate_positions[r.agent]
923
+ self.convergence_detector.record_round(round_positions)
924
+
925
+ # Run validation between iterations
926
+ self._run_iteration_validation(iteration, iter_responses)
927
+
561
928
  # Check consensus
562
929
  consensus = self._check_consensus(iter_responses)
563
930
 
@@ -572,20 +939,28 @@ At the end, clearly indicate if you APPROVE the current state or have remaining
572
939
 
573
940
  previous_responses = iter_responses
574
941
 
942
+ # Log debate status
943
+ agreement_score = self.convergence_detector.calculate_agreement_score()
575
944
  self._log(f"Issues remaining: {len(issues_to_fix)}")
576
- self._log(f"Consensus: {' Yes' if consensus else ' No'}")
945
+ self._log(f"Agreement score: {agreement_score:.0%}")
946
+ self._log(f"Consensus: {'[YES]' if consensus else '[NO]'}")
577
947
 
578
948
  if consensus:
579
949
  self.state = SwarmState.CONSENSUS
580
- self._log("Consensus reached!", "SUCCESS")
950
+ self.termination_reason = "consensus"
951
+ self._log("[CONSENSUS] Agents reached agreement!", "SUCCESS")
581
952
  break
582
953
 
583
954
  # Determine final state
584
- if self.state != SwarmState.CONSENSUS:
955
+ if self.state not in (SwarmState.CONSENSUS, SwarmState.CONVERGING):
585
956
  self.state = SwarmState.MAX_ITERATIONS
957
+ self.termination_reason = "max_iterations"
586
958
 
587
- # Generate final output
959
+ # Generate final output and personality handoff
588
960
  final_output = self._generate_final_output(previous_responses)
961
+ personality_handoff = self._generate_personality_handoff(
962
+ task, previous_responses, final_output
963
+ )
589
964
 
590
965
  # Create result
591
966
  result = SwarmResult(
@@ -600,18 +975,82 @@ At the end, clearly indicate if you APPROVE the current state or have remaining
600
975
  start_time=start_time,
601
976
  end_time=datetime.now().isoformat(),
602
977
  consensus_type=self.config.consensus_type,
978
+ personality_handoff=personality_handoff,
979
+ termination_reason=self.termination_reason,
603
980
  )
604
981
 
605
982
  # Save to knowledge graph
606
983
  self.knowledge_graph.add_decision(
607
984
  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},
985
+ topic="adversarial-debate-result",
986
+ decision=f"Completed: {self.termination_reason} after {len(self.iterations)} rounds",
987
+ context={
988
+ "iterations": len(self.iterations),
989
+ "cost": self.total_cost,
990
+ "termination": self.termination_reason,
991
+ "personas": [p.name for p in self.selected_personas],
992
+ },
611
993
  )
612
994
 
613
995
  return result
614
996
 
997
+ def _generate_personality_handoff(
998
+ self,
999
+ task: str,
1000
+ responses: list[AgentResponse],
1001
+ final_output: str,
1002
+ ) -> PersonalityHandoff:
1003
+ """Generate a personality handoff summarizing the adversarial debate."""
1004
+
1005
+ # Extract consensus points and tensions
1006
+ response_dicts = [{"content": r.content} for r in responses]
1007
+ consensus_points, tensions = self.convergence_detector.extract_consensus_points(
1008
+ response_dicts
1009
+ )
1010
+
1011
+ # Generate debate summary
1012
+ debate_summary = self.convergence_detector.summarize_debate(
1013
+ [i.to_dict() for i in self.iterations]
1014
+ )
1015
+
1016
+ # Synthesize recommended approach from final responses
1017
+ recommended = self._synthesize_recommendation(responses)
1018
+
1019
+ return PersonalityHandoff(
1020
+ spawned_by="SWARM_ORCHESTRATOR",
1021
+ selected_personas=self.selected_personas,
1022
+ debate_summary=debate_summary,
1023
+ consensus_points=consensus_points,
1024
+ unresolved_tensions=tensions,
1025
+ recommended_approach=recommended,
1026
+ confidence=self.convergence_detector.calculate_agreement_score(),
1027
+ total_rounds=len(self.iterations),
1028
+ termination_reason=self.termination_reason,
1029
+ positions=list(self.debate_positions.values()),
1030
+ )
1031
+
1032
+ def _synthesize_recommendation(self, responses: list[AgentResponse]) -> str:
1033
+ """Synthesize a recommended approach from debate responses."""
1034
+ # Collect all key arguments from approving agents
1035
+ approving_args = []
1036
+ for r in responses:
1037
+ if r.vote == "approve" and r.key_arguments:
1038
+ approving_args.extend(r.key_arguments[:2])
1039
+
1040
+ if approving_args:
1041
+ return "Based on debate consensus: " + "; ".join(approving_args[:3])
1042
+
1043
+ # Fallback to general synthesis
1044
+ all_args = []
1045
+ for r in responses:
1046
+ if r.key_arguments:
1047
+ all_args.extend(r.key_arguments[:1])
1048
+
1049
+ if all_args:
1050
+ return "Synthesized from debate: " + "; ".join(all_args[:3])
1051
+
1052
+ return "No clear recommendation emerged - human review recommended."
1053
+
615
1054
  def _generate_final_output(self, responses: list[AgentResponse]) -> str:
616
1055
  """Generate consolidated final output."""
617
1056
  # Use the DEV response as primary, or last response