@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
@@ -0,0 +1,949 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Personality System - Dynamic Adversarial Persona Management
4
+
5
+ Provides dynamic personality selection and adversarial stance management
6
+ for multi-agent swarm debates. Enables agents to take opposing viewpoints
7
+ and challenge each other's designs.
8
+
9
+ Features:
10
+ - Persona loading from YAML templates
11
+ - Adversarial stance matching (finds natural oppositions)
12
+ - Convergence detection (identifies when debate is stabilizing)
13
+ - Personality handoff (preserves persona context across phases)
14
+
15
+ Usage:
16
+ from lib.personality_system import (
17
+ PersonalitySelector,
18
+ ConvergenceDetector,
19
+ PersonalityHandoff
20
+ )
21
+
22
+ selector = PersonalitySelector()
23
+ personas = selector.select_adversarial_personas(
24
+ task="Design authentication system",
25
+ num_agents=3
26
+ )
27
+
28
+ detector = ConvergenceDetector()
29
+ if detector.has_converged(iterations):
30
+ # Debate has stabilized
31
+ """
32
+
33
+ import hashlib
34
+ import re
35
+ from dataclasses import dataclass, field
36
+ from difflib import SequenceMatcher
37
+ from pathlib import Path
38
+ from typing import Any, Optional
39
+
40
+ try:
41
+ import yaml
42
+
43
+ HAS_YAML = True
44
+ except ImportError:
45
+ HAS_YAML = False
46
+ yaml = None
47
+
48
+ PROJECT_ROOT = Path(__file__).parent.parent.parent.parent
49
+ TEMPLATES_DIR = PROJECT_ROOT / "tooling" / ".automation" / "overrides" / "templates"
50
+
51
+
52
+ @dataclass
53
+ class AdversarialStance:
54
+ """Defines an agent's adversarial position in debates."""
55
+
56
+ primary_concern: str # e.g., "security", "velocity", "simplicity"
57
+ opposes: list[str] = field(default_factory=list) # Stances this naturally opposes
58
+ challenge_triggers: list[str] = field(default_factory=list) # Phrases that trigger challenges
59
+ debate_style: str = "assertive" # assertive, questioning, evidence-based
60
+ concession_threshold: float = 0.7 # How much agreement needed to concede
61
+
62
+ def conflicts_with(self, other: "AdversarialStance") -> bool:
63
+ """Check if this stance naturally conflicts with another."""
64
+ return other.primary_concern in self.opposes or self.primary_concern in other.opposes
65
+
66
+
67
+ @dataclass
68
+ class PersonalityProfile:
69
+ """Complete personality profile for an agent."""
70
+
71
+ name: str
72
+ agent_type: str # DEV, REVIEWER, ARCHITECT, etc.
73
+ template_path: str
74
+ role: str
75
+ identity: str
76
+ communication_style: str
77
+ principles: list[str] = field(default_factory=list)
78
+ additional_rules: list[str] = field(default_factory=list)
79
+ memories: list[str] = field(default_factory=list)
80
+ critical_actions: list[str] = field(default_factory=list)
81
+ model: str = "sonnet"
82
+ max_budget_usd: float = 10.0
83
+ adversarial_stance: Optional[AdversarialStance] = None
84
+
85
+ # Extended attributes for complex personas
86
+ technical_preferences: dict = field(default_factory=dict)
87
+ behavior: dict = field(default_factory=dict)
88
+ constraints: dict = field(default_factory=dict)
89
+ mantras: list[str] = field(default_factory=list)
90
+
91
+ def to_prompt_injection(self) -> str:
92
+ """Generate prompt text to inject this personality."""
93
+ lines = [
94
+ f"## Your Persona: {self.name}",
95
+ "",
96
+ f"**Role**: {self.role}",
97
+ f"**Identity**: {self.identity}",
98
+ f"**Communication Style**: {self.communication_style}",
99
+ "",
100
+ ]
101
+
102
+ if self.principles:
103
+ lines.append("### Core Principles")
104
+ for p in self.principles:
105
+ lines.append(f"- {p}")
106
+ lines.append("")
107
+
108
+ if self.adversarial_stance:
109
+ lines.append("### Your Stance in This Debate")
110
+ lines.append(f"**Primary Concern**: {self.adversarial_stance.primary_concern}")
111
+ lines.append(f"**Debate Style**: {self.adversarial_stance.debate_style}")
112
+ if self.adversarial_stance.opposes:
113
+ lines.append(
114
+ f"**Challenge perspectives focused on**: {', '.join(self.adversarial_stance.opposes)}"
115
+ )
116
+ lines.append("")
117
+ lines.append("When you see arguments that prioritize the concerns you oppose,")
118
+ lines.append("push back firmly with evidence and alternative approaches.")
119
+ lines.append("")
120
+
121
+ if self.additional_rules:
122
+ lines.append("### Rules to Follow")
123
+ for r in self.additional_rules[:5]: # Limit for token efficiency
124
+ lines.append(f"- {r}")
125
+ lines.append("")
126
+
127
+ if self.mantras:
128
+ lines.append("### Mantras")
129
+ for m in self.mantras[:3]:
130
+ lines.append(f'- "{m}"')
131
+ lines.append("")
132
+
133
+ return "\n".join(lines)
134
+
135
+ def to_dict(self) -> dict:
136
+ """Serialize to dictionary."""
137
+ return {
138
+ "name": self.name,
139
+ "agent_type": self.agent_type,
140
+ "template_path": self.template_path,
141
+ "role": self.role,
142
+ "identity": self.identity,
143
+ "model": self.model,
144
+ "adversarial_stance": (
145
+ {
146
+ "primary_concern": self.adversarial_stance.primary_concern,
147
+ "opposes": self.adversarial_stance.opposes,
148
+ "debate_style": self.adversarial_stance.debate_style,
149
+ }
150
+ if self.adversarial_stance
151
+ else None
152
+ ),
153
+ }
154
+
155
+
156
+ @dataclass
157
+ class DebatePosition:
158
+ """Tracks an agent's position during debate."""
159
+
160
+ agent: str
161
+ persona_name: str
162
+ key_arguments: list[str] = field(default_factory=list)
163
+ concessions_made: list[str] = field(default_factory=list)
164
+ challenges_raised: list[str] = field(default_factory=list)
165
+ confidence: float = 1.0 # Decreases as agent concedes points
166
+
167
+ def position_hash(self) -> str:
168
+ """Generate hash of current position for change detection."""
169
+ content = "|".join(sorted(self.key_arguments))
170
+ return hashlib.md5(content.encode()).hexdigest()[:8]
171
+
172
+
173
+ @dataclass
174
+ class PersonalityHandoff:
175
+ """Handoff context that includes personality state."""
176
+
177
+ spawned_by: str # Agent that initiated the swarm
178
+ selected_personas: list[PersonalityProfile]
179
+ debate_summary: str
180
+ consensus_points: list[str]
181
+ unresolved_tensions: list[str]
182
+ recommended_approach: str
183
+ confidence: float # How strong the consensus is (0-1)
184
+ total_rounds: int
185
+ termination_reason: str # "consensus", "convergence", "budget", "max_rounds"
186
+ positions: list[DebatePosition] = field(default_factory=list)
187
+
188
+ def to_markdown(self) -> str:
189
+ """Generate markdown summary for handoff."""
190
+ lines = [
191
+ "## Adversarial Debate Summary",
192
+ "",
193
+ f"**Initiated by**: {self.spawned_by}",
194
+ f"**Rounds**: {self.total_rounds}",
195
+ f"**Termination**: {self.termination_reason}",
196
+ f"**Consensus Confidence**: {self.confidence:.0%}",
197
+ "",
198
+ "### Participating Personas",
199
+ ]
200
+
201
+ for p in self.selected_personas:
202
+ stance = (
203
+ f" (Focus: {p.adversarial_stance.primary_concern})" if p.adversarial_stance else ""
204
+ )
205
+ lines.append(f"- **{p.name}** ({p.agent_type}){stance}")
206
+
207
+ lines.append("")
208
+ lines.append("### Consensus Points")
209
+ for point in self.consensus_points:
210
+ lines.append(f"- {point}")
211
+
212
+ if self.unresolved_tensions:
213
+ lines.append("")
214
+ lines.append("### Unresolved Tensions (for human review)")
215
+ for tension in self.unresolved_tensions:
216
+ lines.append(f"- [WARNING] {tension}")
217
+
218
+ lines.append("")
219
+ lines.append("### Recommended Approach")
220
+ lines.append(self.recommended_approach)
221
+
222
+ if self.debate_summary:
223
+ lines.append("")
224
+ lines.append("### Debate Summary")
225
+ lines.append(self.debate_summary)
226
+
227
+ return "\n".join(lines)
228
+
229
+ def to_dict(self) -> dict:
230
+ """Serialize to dictionary."""
231
+ return {
232
+ "spawned_by": self.spawned_by,
233
+ "selected_personas": [p.to_dict() for p in self.selected_personas],
234
+ "debate_summary": self.debate_summary,
235
+ "consensus_points": self.consensus_points,
236
+ "unresolved_tensions": self.unresolved_tensions,
237
+ "recommended_approach": self.recommended_approach,
238
+ "confidence": self.confidence,
239
+ "total_rounds": self.total_rounds,
240
+ "termination_reason": self.termination_reason,
241
+ }
242
+
243
+
244
+ class PersonalityLoader:
245
+ """Loads personality profiles from YAML templates."""
246
+
247
+ def __init__(self, templates_dir: Optional[Path] = None):
248
+ self.templates_dir = templates_dir or TEMPLATES_DIR
249
+ self._cache: dict[str, PersonalityProfile] = {}
250
+
251
+ def list_available(self, agent_type: Optional[str] = None) -> list[str]:
252
+ """List available persona templates."""
253
+ templates = []
254
+ search_dir = self.templates_dir
255
+
256
+ if agent_type:
257
+ search_dir = self.templates_dir / agent_type.lower()
258
+ if not search_dir.exists():
259
+ return []
260
+
261
+ for yaml_file in search_dir.rglob("*.yaml"):
262
+ if yaml_file.name.startswith("user-"):
263
+ continue # Skip user templates
264
+ rel_path = yaml_file.relative_to(self.templates_dir)
265
+ templates.append(str(rel_path))
266
+
267
+ return sorted(templates)
268
+
269
+ def load(self, template_path: str) -> Optional[PersonalityProfile]:
270
+ """Load a persona from template path."""
271
+ if template_path in self._cache:
272
+ return self._cache[template_path]
273
+
274
+ full_path = self.templates_dir / template_path
275
+ if not full_path.exists():
276
+ return None
277
+
278
+ if not HAS_YAML:
279
+ # Create a basic profile from path when yaml not available
280
+ return self._create_fallback_profile(template_path)
281
+
282
+ try:
283
+ with open(full_path) as f:
284
+ data = yaml.safe_load(f)
285
+
286
+ profile = self._parse_profile(data, template_path)
287
+ self._cache[template_path] = profile
288
+ return profile
289
+
290
+ except Exception as e:
291
+ print(f"[WARNING] Failed to load persona {template_path}: {e}")
292
+ return self._create_fallback_profile(template_path)
293
+
294
+ def _create_fallback_profile(self, template_path: str) -> PersonalityProfile:
295
+ """Create a basic profile when yaml loading isn't available."""
296
+ parts = template_path.split("/")
297
+ agent_type = parts[0].upper() if len(parts) > 1 else "UNKNOWN"
298
+ name = Path(template_path).stem.replace("-", " ").title()
299
+
300
+ return PersonalityProfile(
301
+ name=name,
302
+ agent_type=agent_type,
303
+ template_path=template_path,
304
+ role=name,
305
+ identity=f"A {name.lower()} focused on quality",
306
+ communication_style="professional",
307
+ )
308
+
309
+ def _parse_profile(self, data: dict, template_path: str) -> PersonalityProfile:
310
+ """Parse YAML data into PersonalityProfile."""
311
+ # Extract agent type from path
312
+ parts = template_path.split("/")
313
+ agent_type = parts[0].upper() if len(parts) > 1 else "UNKNOWN"
314
+
315
+ # Handle different schema formats
316
+ persona = data.get("persona", {})
317
+ personality = data.get("personality", {})
318
+
319
+ # Extract name
320
+ name = (
321
+ persona.get("name")
322
+ or persona.get("role")
323
+ or data.get("name")
324
+ or Path(template_path).stem.replace("-", " ").title()
325
+ )
326
+
327
+ # Extract role and identity
328
+ role = persona.get("role", name)
329
+ identity = (
330
+ persona.get("identity")
331
+ or persona.get("description")
332
+ or personality.get("description", "")
333
+ )
334
+
335
+ # Extract communication style
336
+ comm_style = persona.get("communication_style", "")
337
+ if not comm_style and personality.get("communication_style"):
338
+ cs = personality["communication_style"]
339
+ comm_style = cs.get("tone", "") if isinstance(cs, dict) else str(cs)
340
+
341
+ # Extract adversarial stance if present
342
+ adversarial_stance = None
343
+ stance_data = data.get("adversarial_stance")
344
+ if stance_data:
345
+ adversarial_stance = AdversarialStance(
346
+ primary_concern=stance_data.get("primary_concern", "quality"),
347
+ opposes=stance_data.get("opposes", []),
348
+ challenge_triggers=stance_data.get("challenge_triggers", []),
349
+ debate_style=stance_data.get("debate_style", "assertive"),
350
+ concession_threshold=stance_data.get("concession_threshold", 0.7),
351
+ )
352
+
353
+ return PersonalityProfile(
354
+ name=name,
355
+ agent_type=agent_type,
356
+ template_path=template_path,
357
+ role=role,
358
+ identity=identity,
359
+ communication_style=comm_style,
360
+ principles=persona.get("principles", []),
361
+ additional_rules=data.get("additional_rules", []),
362
+ memories=data.get("memories", []),
363
+ critical_actions=data.get("critical_actions", []),
364
+ model=data.get("model", "sonnet"),
365
+ max_budget_usd=data.get("max_budget_usd", 10.0),
366
+ adversarial_stance=adversarial_stance,
367
+ technical_preferences=data.get("technical_preferences", {}),
368
+ behavior=data.get("behavior", {}),
369
+ constraints=data.get("constraints", {}),
370
+ mantras=data.get("mantras", []),
371
+ )
372
+
373
+
374
+ # Predefined adversarial pairings - personas that naturally oppose each other
375
+ ADVERSARIAL_PAIRINGS = {
376
+ # Security vs Velocity
377
+ "security_velocity": [
378
+ ("dev/security-focused.yaml", "dev/rapid-prototyper.yaml"),
379
+ ("reviewer/thorough-critic.yaml", "reviewer/quick-sanity.yaml"),
380
+ ],
381
+ # Enterprise vs Pragmatic
382
+ "enterprise_pragmatic": [
383
+ ("architect/enterprise-architect.yaml", "architect/pragmatic-minimalist.yaml"),
384
+ ],
385
+ # Thoroughness vs Speed
386
+ "thorough_speed": [
387
+ ("reviewer/thorough-critic.yaml", "reviewer/quick-sanity.yaml"),
388
+ ("ba/requirements-engineer.yaml", "ba/agile-storyteller.yaml"),
389
+ ],
390
+ # Traditional vs Agile
391
+ "traditional_agile": [
392
+ ("pm/traditional-pm.yaml", "pm/agile-pm.yaml"),
393
+ ],
394
+ # Stability vs Innovation
395
+ "stability_innovation": [
396
+ ("maintainer/legacy-steward.yaml", "dev/rapid-prototyper.yaml"),
397
+ ("architect/enterprise-architect.yaml", "architect/cloud-native.yaml"),
398
+ ],
399
+ }
400
+
401
+ # Task keyword to concern mapping
402
+ TASK_CONCERN_MAPPING = {
403
+ # Security-related keywords
404
+ "auth": "security",
405
+ "login": "security",
406
+ "password": "security",
407
+ "encrypt": "security",
408
+ "token": "security",
409
+ "permission": "security",
410
+ "access": "security",
411
+ "vulnerability": "security",
412
+ # Performance-related keywords
413
+ "performance": "performance",
414
+ "optimize": "performance",
415
+ "speed": "performance",
416
+ "latency": "performance",
417
+ "cache": "performance",
418
+ "scale": "performance",
419
+ # Architecture-related keywords
420
+ "design": "architecture",
421
+ "architect": "architecture",
422
+ "structure": "architecture",
423
+ "pattern": "architecture",
424
+ "microservice": "architecture",
425
+ "monolith": "architecture",
426
+ # Quality-related keywords
427
+ "test": "quality",
428
+ "review": "quality",
429
+ "refactor": "quality",
430
+ "clean": "quality",
431
+ "maintain": "quality",
432
+ # Velocity-related keywords
433
+ "mvp": "velocity",
434
+ "prototype": "velocity",
435
+ "quick": "velocity",
436
+ "fast": "velocity",
437
+ "deadline": "velocity",
438
+ "ship": "velocity",
439
+ }
440
+
441
+ # Concern to persona mapping (which personas care about which concerns)
442
+ CONCERN_PERSONAS = {
443
+ "security": [
444
+ "dev/security-focused.yaml",
445
+ "reviewer/thorough-critic.yaml",
446
+ ],
447
+ "performance": [
448
+ "dev/performance-engineer.yaml",
449
+ "architect/cloud-native.yaml",
450
+ ],
451
+ "architecture": [
452
+ "architect/enterprise-architect.yaml",
453
+ "architect/pragmatic-minimalist.yaml",
454
+ "architect/cloud-native.yaml",
455
+ ],
456
+ "quality": [
457
+ "reviewer/thorough-critic.yaml",
458
+ "reviewer/mentoring-reviewer.yaml",
459
+ "maintainer/legacy-steward.yaml",
460
+ ],
461
+ "velocity": [
462
+ "dev/rapid-prototyper.yaml",
463
+ "reviewer/quick-sanity.yaml",
464
+ "pm/agile-pm.yaml",
465
+ ],
466
+ "simplicity": [
467
+ "architect/pragmatic-minimalist.yaml",
468
+ "dev/senior-fullstack.yaml",
469
+ ],
470
+ "compliance": [
471
+ "architect/enterprise-architect.yaml",
472
+ "ba/requirements-engineer.yaml",
473
+ ],
474
+ "user_experience": [
475
+ "ba/domain-expert.yaml",
476
+ "writer/user-guide-author.yaml",
477
+ ],
478
+ }
479
+
480
+
481
+ class PersonalitySelector:
482
+ """Selects adversarial personas based on task analysis."""
483
+
484
+ def __init__(self, templates_dir: Optional[Path] = None):
485
+ self.loader = PersonalityLoader(templates_dir)
486
+
487
+ def analyze_task(self, task: str) -> list[str]:
488
+ """Analyze task to determine relevant concerns."""
489
+ task_lower = task.lower()
490
+ concerns = set()
491
+
492
+ for keyword, concern in TASK_CONCERN_MAPPING.items():
493
+ if keyword in task_lower:
494
+ concerns.add(concern)
495
+
496
+ # Default concerns if none detected
497
+ if not concerns:
498
+ concerns = {"quality", "architecture", "velocity"}
499
+
500
+ return list(concerns)
501
+
502
+ def find_opposing_personas(self, concerns: list[str], num_agents: int = 3) -> list[str]:
503
+ """Find personas that will naturally oppose each other on these concerns."""
504
+ candidates = []
505
+
506
+ # Get personas for each concern
507
+ for concern in concerns:
508
+ if concern in CONCERN_PERSONAS:
509
+ candidates.extend(CONCERN_PERSONAS[concern])
510
+
511
+ # Dedupe while preserving order
512
+ seen = set()
513
+ unique_candidates = []
514
+ for c in candidates:
515
+ if c not in seen:
516
+ seen.add(c)
517
+ unique_candidates.append(c)
518
+
519
+ # If we have adversarial pairings, prefer those
520
+ for _pairing_type, pairs in ADVERSARIAL_PAIRINGS.items():
521
+ for pair in pairs:
522
+ if pair[0] in unique_candidates and pair[1] in unique_candidates:
523
+ # This pair is relevant - prioritize them
524
+ unique_candidates.remove(pair[0])
525
+ unique_candidates.remove(pair[1])
526
+ unique_candidates = [pair[0], pair[1]] + unique_candidates
527
+
528
+ return unique_candidates[:num_agents]
529
+
530
+ def select_adversarial_personas(
531
+ self,
532
+ task: str,
533
+ num_agents: int = 3,
534
+ required_agents: Optional[list[str]] = None,
535
+ ) -> list[PersonalityProfile]:
536
+ """Select adversarial personas for a task.
537
+
538
+ Args:
539
+ task: The task description
540
+ num_agents: Number of agents to select
541
+ required_agents: Optional list of agent types that must be included
542
+
543
+ Returns:
544
+ List of PersonalityProfile with adversarial stances
545
+ """
546
+ concerns = self.analyze_task(task)
547
+ profiles = []
548
+
549
+ # If required agents specified, select best persona for each agent type
550
+ if required_agents:
551
+ for agent_type in required_agents:
552
+ profile = self._select_best_persona_for_agent(agent_type, concerns)
553
+ if profile:
554
+ if not profile.adversarial_stance:
555
+ profile.adversarial_stance = self._infer_stance(profile, concerns)
556
+ profiles.append(profile)
557
+ else:
558
+ # No specific agents required - select based on concerns
559
+ template_paths = self.find_opposing_personas(concerns, num_agents)
560
+ for path in template_paths:
561
+ profile = self.loader.load(path)
562
+ if profile:
563
+ if not profile.adversarial_stance:
564
+ profile.adversarial_stance = self._infer_stance(profile, concerns)
565
+ profiles.append(profile)
566
+
567
+ return profiles[:num_agents]
568
+
569
+ def _select_best_persona_for_agent(
570
+ self, agent_type: str, concerns: list[str]
571
+ ) -> Optional[PersonalityProfile]:
572
+ """Select the best persona for a given agent type based on task concerns."""
573
+ # Map agent types to their persona directories
574
+ agent_dir_map = {
575
+ "DEV": "dev",
576
+ "REVIEWER": "reviewer",
577
+ "ARCHITECT": "architect",
578
+ "SM": "sm",
579
+ "PM": "pm",
580
+ "BA": "ba",
581
+ "WRITER": "writer",
582
+ "MAINTAINER": "maintainer",
583
+ "SECURITY": "dev", # Use dev/security-focused for SECURITY agent
584
+ }
585
+
586
+ agent_dir = agent_dir_map.get(agent_type, agent_type.lower())
587
+ available = self.loader.list_available(agent_dir)
588
+
589
+ if not available:
590
+ return None
591
+
592
+ # Score personas based on concern alignment
593
+ best_score = -1
594
+ best_path = available[0] # Default to first available
595
+
596
+ # Concern-to-persona preference mapping
597
+ concern_preferences = {
598
+ "security": ["security-focused", "thorough-critic"],
599
+ "performance": ["performance-engineer", "cloud-native"],
600
+ "velocity": ["rapid-prototyper", "quick-sanity", "agile"],
601
+ "architecture": ["enterprise-architect", "pragmatic-minimalist", "cloud-native"],
602
+ "quality": ["thorough-critic", "mentoring", "senior"],
603
+ "simplicity": ["pragmatic-minimalist", "rapid-prototyper"],
604
+ }
605
+
606
+ for path in available:
607
+ path_lower = path.lower()
608
+ score = 0
609
+
610
+ for concern in concerns:
611
+ preferences = concern_preferences.get(concern, [])
612
+ for pref in preferences:
613
+ if pref in path_lower:
614
+ score += 1
615
+
616
+ if score > best_score:
617
+ best_score = score
618
+ best_path = path
619
+
620
+ return self.loader.load(best_path)
621
+
622
+ def _infer_stance(
623
+ self, profile: PersonalityProfile, task_concerns: list[str]
624
+ ) -> AdversarialStance:
625
+ """Infer adversarial stance from profile characteristics."""
626
+ # Default stance based on agent type and profile attributes
627
+ stance_map = {
628
+ "DEV": {
629
+ "security-focused": AdversarialStance(
630
+ primary_concern="security",
631
+ opposes=["velocity", "shortcuts"],
632
+ debate_style="evidence-based",
633
+ ),
634
+ "rapid-prototyper": AdversarialStance(
635
+ primary_concern="velocity",
636
+ opposes=["over_engineering", "premature_optimization"],
637
+ debate_style="assertive",
638
+ ),
639
+ "performance-engineer": AdversarialStance(
640
+ primary_concern="performance",
641
+ opposes=["bloat", "inefficiency"],
642
+ debate_style="evidence-based",
643
+ ),
644
+ "default": AdversarialStance(
645
+ primary_concern="implementation_quality",
646
+ opposes=["complexity", "ambiguity"],
647
+ debate_style="questioning",
648
+ ),
649
+ },
650
+ "REVIEWER": {
651
+ "thorough-critic": AdversarialStance(
652
+ primary_concern="correctness",
653
+ opposes=["shortcuts", "untested_code", "security_gaps"],
654
+ debate_style="evidence-based",
655
+ ),
656
+ "quick-sanity": AdversarialStance(
657
+ primary_concern="pragmatism",
658
+ opposes=["over_engineering", "bikeshedding"],
659
+ debate_style="assertive",
660
+ ),
661
+ "default": AdversarialStance(
662
+ primary_concern="quality",
663
+ opposes=["technical_debt"],
664
+ debate_style="questioning",
665
+ ),
666
+ },
667
+ "ARCHITECT": {
668
+ "enterprise-architect": AdversarialStance(
669
+ primary_concern="scalability",
670
+ opposes=["shortcuts", "monolith_only", "vendor_lockin"],
671
+ debate_style="evidence-based",
672
+ ),
673
+ "pragmatic-minimalist": AdversarialStance(
674
+ primary_concern="simplicity",
675
+ opposes=["over_engineering", "premature_abstraction"],
676
+ debate_style="assertive",
677
+ ),
678
+ "cloud-native": AdversarialStance(
679
+ primary_concern="scalability",
680
+ opposes=["legacy_patterns", "tight_coupling"],
681
+ debate_style="questioning",
682
+ ),
683
+ "default": AdversarialStance(
684
+ primary_concern="architecture_quality",
685
+ opposes=["accidental_complexity"],
686
+ debate_style="questioning",
687
+ ),
688
+ },
689
+ }
690
+
691
+ agent_stances = stance_map.get(profile.agent_type, {})
692
+ template_name = Path(profile.template_path).stem
693
+
694
+ return agent_stances.get(
695
+ template_name,
696
+ agent_stances.get(
697
+ "default",
698
+ AdversarialStance(
699
+ primary_concern="quality", opposes=["poor_design"], debate_style="questioning"
700
+ ),
701
+ ),
702
+ )
703
+
704
+
705
+ class ConvergenceDetector:
706
+ """Detects when a debate has converged (positions stabilized)."""
707
+
708
+ def __init__(
709
+ self,
710
+ similarity_threshold: float = 0.8,
711
+ stability_rounds: int = 2,
712
+ max_new_arguments_per_round: int = 2,
713
+ ):
714
+ self.similarity_threshold = similarity_threshold
715
+ self.stability_rounds = stability_rounds
716
+ self.max_new_arguments = max_new_arguments_per_round
717
+ self.position_history: list[dict[str, DebatePosition]] = []
718
+
719
+ def record_round(self, positions: dict[str, DebatePosition]):
720
+ """Record positions from a debate round."""
721
+ self.position_history.append(positions)
722
+
723
+ def has_converged(self) -> bool:
724
+ """Check if debate has converged based on position stability."""
725
+ if len(self.position_history) < self.stability_rounds:
726
+ return False
727
+
728
+ # Check last N rounds for stability
729
+ recent = self.position_history[-self.stability_rounds :]
730
+
731
+ # Compare position hashes
732
+ for agent in recent[0].keys():
733
+ hashes = [
734
+ round_pos.get(agent, DebatePosition(agent, "")).position_hash()
735
+ for round_pos in recent
736
+ ]
737
+ if len(set(hashes)) > 1:
738
+ # Positions still changing
739
+ return False
740
+
741
+ return True
742
+
743
+ def calculate_agreement_score(self) -> float:
744
+ """Calculate how much agents agree (0-1)."""
745
+ if not self.position_history:
746
+ return 0.0
747
+
748
+ latest = self.position_history[-1]
749
+ all_arguments = []
750
+
751
+ for pos in latest.values():
752
+ all_arguments.extend(pos.key_arguments)
753
+
754
+ if not all_arguments:
755
+ return 1.0 # No arguments = agreement by default
756
+
757
+ # Count overlapping arguments
758
+ unique_args = set(all_arguments)
759
+ overlap_score = 1 - (len(unique_args) / max(len(all_arguments), 1))
760
+
761
+ return min(1.0, overlap_score + 0.3) # Bias toward agreement
762
+
763
+ def get_convergence_reason(self) -> str:
764
+ """Get human-readable convergence status."""
765
+ if not self.position_history:
766
+ return "No debate recorded"
767
+
768
+ if self.has_converged():
769
+ agreement = self.calculate_agreement_score()
770
+ if agreement > 0.9:
771
+ return "Strong consensus reached"
772
+ elif agreement > 0.7:
773
+ return "Moderate agreement with minor differences"
774
+ else:
775
+ return "Positions stabilized but significant disagreement remains"
776
+
777
+ return "Debate still active"
778
+
779
+ def extract_consensus_points(
780
+ self, responses: list[dict[str, Any]]
781
+ ) -> tuple[list[str], list[str]]:
782
+ """Extract points of consensus and remaining tensions.
783
+
784
+ Args:
785
+ responses: List of agent responses with content
786
+
787
+ Returns:
788
+ Tuple of (consensus_points, tensions)
789
+ """
790
+ consensus = []
791
+ tensions = []
792
+
793
+ # Simple extraction based on common patterns
794
+ all_approvals = []
795
+ all_issues = []
796
+
797
+ for resp in responses:
798
+ content = resp.get("content", "")
799
+
800
+ # Extract approvals
801
+ approval_patterns = [
802
+ r"agree.* (?:that|with)\s+(.+?)(?:\.|$)",
803
+ r"consensus.* (?:on|that)\s+(.+?)(?:\.|$)",
804
+ r"(?:we|all) (?:should|need to)\s+(.+?)(?:\.|$)",
805
+ ]
806
+ for pattern in approval_patterns:
807
+ matches = re.findall(pattern, content, re.IGNORECASE)
808
+ all_approvals.extend(matches)
809
+
810
+ # Extract issues
811
+ issue_patterns = [
812
+ r"disagree.* (?:that|with|about)\s+(.+?)(?:\.|$)",
813
+ r"concern.* (?:about|regarding)\s+(.+?)(?:\.|$)",
814
+ r"(?:but|however).* (.+?)(?:\.|$)",
815
+ ]
816
+ for pattern in issue_patterns:
817
+ matches = re.findall(pattern, content, re.IGNORECASE)
818
+ all_issues.extend(matches)
819
+
820
+ # Dedupe and clean
821
+ consensus = list({a.strip() for a in all_approvals if len(a) > 10})[:5]
822
+ tensions = list({i.strip() for i in all_issues if len(i) > 10})[:5]
823
+
824
+ return consensus, tensions
825
+
826
+ def summarize_debate(self, iterations: list[dict[str, Any]]) -> str:
827
+ """Generate a summary of the debate progression."""
828
+ if not iterations:
829
+ return "No debate occurred."
830
+
831
+ num_rounds = len(iterations)
832
+ lines = [f"The debate progressed through {num_rounds} round(s)."]
833
+
834
+ # Track how positions evolved
835
+ if num_rounds > 1:
836
+ lines.append("Initial positions were challenged and refined through cross-examination.")
837
+
838
+ agreement = self.calculate_agreement_score()
839
+ if agreement > 0.8:
840
+ lines.append("Agents reached substantial agreement on core approach.")
841
+ elif agreement > 0.5:
842
+ lines.append(
843
+ "Agents found common ground on key points while maintaining some differences."
844
+ )
845
+ else:
846
+ lines.append("Significant disagreements remain that may require human arbitration.")
847
+
848
+ return " ".join(lines)
849
+
850
+
851
+ def extract_arguments_from_response(content: str) -> list[str]:
852
+ """Extract key arguments from agent response content."""
853
+ arguments = []
854
+
855
+ # Look for structured arguments
856
+ patterns = [
857
+ r"(?:I (?:believe|think|argue|propose) (?:that )?(.+?)(?:\.|$))",
858
+ r"(?:My (?:position|view|recommendation) is (?:that )?(.+?)(?:\.|$))",
859
+ r"(?:We should (.+?)(?:\.|$))",
860
+ r"(?:The (?:best|right|correct) approach is (.+?)(?:\.|$))",
861
+ ]
862
+
863
+ for pattern in patterns:
864
+ matches = re.findall(pattern, content, re.IGNORECASE)
865
+ arguments.extend(matches)
866
+
867
+ # Also look for bullet points
868
+ bullet_matches = re.findall(r"^[-*]\s+(.+?)$", content, re.MULTILINE)
869
+ arguments.extend(bullet_matches)
870
+
871
+ # Clean and dedupe
872
+ cleaned = []
873
+ for arg in arguments:
874
+ arg = arg.strip()
875
+ if len(arg) > 10 and arg not in cleaned:
876
+ cleaned.append(arg)
877
+
878
+ return cleaned[:10] # Limit
879
+
880
+
881
+ def calculate_position_similarity(pos1: DebatePosition, pos2: DebatePosition) -> float:
882
+ """Calculate similarity between two debate positions."""
883
+ if not pos1.key_arguments and not pos2.key_arguments:
884
+ return 1.0
885
+
886
+ all_args = pos1.key_arguments + pos2.key_arguments
887
+ if not all_args:
888
+ return 1.0
889
+
890
+ # Use sequence matching for similarity
891
+ text1 = " ".join(pos1.key_arguments)
892
+ text2 = " ".join(pos2.key_arguments)
893
+
894
+ return SequenceMatcher(None, text1, text2).ratio()
895
+
896
+
897
+ if __name__ == "__main__":
898
+ print("=== Personality System Demo ===\n")
899
+
900
+ # Demo personality selection
901
+ selector = PersonalitySelector()
902
+
903
+ task = "Design a secure authentication system with OAuth support"
904
+ print(f"Task: {task}\n")
905
+
906
+ concerns = selector.analyze_task(task)
907
+ print(f"Detected concerns: {concerns}\n")
908
+
909
+ personas = selector.select_adversarial_personas(task, num_agents=3)
910
+ print("Selected adversarial personas:")
911
+ for p in personas:
912
+ print(f" - {p.name} ({p.agent_type})")
913
+ if p.adversarial_stance:
914
+ print(f" Focus: {p.adversarial_stance.primary_concern}")
915
+ print(f" Opposes: {p.adversarial_stance.opposes}")
916
+ print()
917
+
918
+ # Demo convergence detection
919
+ print("=== Convergence Detection Demo ===\n")
920
+ detector = ConvergenceDetector()
921
+
922
+ # Simulate debate rounds
923
+ round1 = {
924
+ "agent1": DebatePosition(
925
+ "agent1", "Security Advocate", key_arguments=["Use OAuth 2.0", "Enforce MFA"]
926
+ ),
927
+ "agent2": DebatePosition(
928
+ "agent2", "Pragmatist", key_arguments=["Start with basic auth", "Add OAuth later"]
929
+ ),
930
+ }
931
+ detector.record_round(round1)
932
+ print(f"After round 1: Converged={detector.has_converged()}")
933
+
934
+ round2 = {
935
+ "agent1": DebatePosition(
936
+ "agent1",
937
+ "Security Advocate",
938
+ key_arguments=["Use OAuth 2.0", "Enforce MFA", "OK to start simple"],
939
+ ),
940
+ "agent2": DebatePosition(
941
+ "agent2",
942
+ "Pragmatist",
943
+ key_arguments=["Start with OAuth", "Add MFA in phase 2"],
944
+ ),
945
+ }
946
+ detector.record_round(round2)
947
+ print(f"After round 2: Converged={detector.has_converged()}")
948
+ print(f"Agreement score: {detector.calculate_agreement_score():.2f}")
949
+ print(f"Status: {detector.get_convergence_reason()}")