@pjmendonca/devflow 1.13.1 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/agent.md +1 -1
- package/.claude/commands/bugfix.md +21 -0
- package/.claude/commands/checkpoint.md +0 -1
- package/.claude/commands/collab.md +0 -1
- package/.claude/commands/costs.md +88 -18
- package/.claude/commands/devflow.md +26 -0
- package/.claude/commands/handoff.md +0 -1
- package/.claude/commands/memory.md +0 -1
- package/.claude/commands/pair.md +0 -1
- package/.claude/commands/review.md +27 -0
- package/.claude/commands/route.md +0 -1
- package/.claude/commands/swarm.md +0 -1
- package/.claude/commands/validate.md +55 -0
- package/.claude/hooks/session-notification.sh +44 -0
- package/.claude/hooks/session-startup.sh +427 -0
- package/.claude/hooks/session-stop.sh +38 -0
- package/.claude/hooks/session_tracker.py +272 -0
- package/.claude/settings.json +38 -0
- package/.claude/skills/costs/SKILL.md +156 -0
- package/.claude/skills/validate/SKILL.md +101 -0
- package/CHANGELOG.md +254 -0
- package/README.md +207 -10
- package/bin/devflow-install.js +2 -1
- package/bin/devflow.js +5 -2
- package/lib/constants.js +0 -1
- package/lib/exec-python.js +1 -1
- package/package.json +1 -2
- package/tooling/.automation/.checkpoint_lock +1 -0
- package/tooling/.automation/agents/architect.md +19 -0
- package/tooling/.automation/agents/ba.md +19 -0
- package/tooling/.automation/agents/maintainer.md +19 -0
- package/tooling/.automation/agents/pm.md +19 -0
- package/tooling/.automation/agents/reviewer.md +1 -1
- package/tooling/.automation/agents/writer.md +19 -0
- package/tooling/.automation/benchmarks/benchmark_20251230_100119.json +314 -0
- package/tooling/.automation/benchmarks/benchmark_20251230_100216.json +314 -0
- package/tooling/.automation/costs/config.json +31 -0
- package/tooling/.automation/costs/sessions/2025-12-29_20251229_164128.json +22 -0
- package/tooling/.automation/memory/knowledge/kg_integration-test.json +707 -1
- package/tooling/.automation/memory/knowledge/kg_test-story.json +3273 -2
- package/tooling/.automation/memory/shared/shared_integration-test.json +181 -1
- package/tooling/.automation/memory/shared/shared_test-story.json +721 -1
- package/tooling/.automation/memory/shared/shared_test.json +1254 -0
- package/tooling/.automation/memory/shared/shared_validation-check.json +227 -0
- package/tooling/.automation/overrides/templates/architect/cloud-native.yaml +5 -5
- package/tooling/.automation/overrides/templates/architect/enterprise-architect.yaml +23 -5
- package/tooling/.automation/overrides/templates/architect/pragmatic-minimalist.yaml +24 -6
- package/tooling/.automation/overrides/templates/ba/agile-storyteller.yaml +4 -4
- package/tooling/.automation/overrides/templates/ba/domain-expert.yaml +4 -4
- package/tooling/.automation/overrides/templates/ba/requirements-engineer.yaml +4 -4
- package/tooling/.automation/overrides/templates/dev/performance-engineer.yaml +18 -0
- package/tooling/.automation/overrides/templates/dev/rapid-prototyper.yaml +19 -1
- package/tooling/.automation/overrides/templates/dev/security-focused.yaml +18 -0
- package/tooling/.automation/overrides/templates/dev/user-advocate.yaml +54 -0
- package/tooling/.automation/overrides/templates/maintainer/devops-maintainer.yaml +4 -4
- package/tooling/.automation/overrides/templates/maintainer/legacy-steward.yaml +4 -4
- package/tooling/.automation/overrides/templates/maintainer/oss-maintainer.yaml +4 -4
- package/tooling/.automation/overrides/templates/maintainer/reliability-engineer.yaml +55 -0
- package/tooling/.automation/overrides/templates/pm/agile-pm.yaml +4 -4
- package/tooling/.automation/overrides/templates/pm/hybrid-delivery.yaml +3 -3
- package/tooling/.automation/overrides/templates/pm/traditional-pm.yaml +4 -4
- package/tooling/.automation/overrides/templates/reviewer/quick-sanity.yaml +18 -0
- package/tooling/.automation/overrides/templates/reviewer/thorough-critic.yaml +18 -0
- package/tooling/.automation/overrides/templates/sm/agile-coach.yaml +2 -2
- package/tooling/.automation/overrides/templates/sm/startup-pm.yaml +3 -3
- package/tooling/.automation/overrides/templates/writer/api-documentarian.yaml +5 -5
- package/tooling/.automation/overrides/templates/writer/docs-as-code.yaml +4 -4
- package/tooling/.automation/overrides/templates/writer/user-guide-author.yaml +5 -5
- package/tooling/.automation/validation/history/2025-12-29_val_002a28c1.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_01273bb1.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_03369914.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_07a449ba.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_0df1f0a2.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_10ff3d34.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_110771d7.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_13f3a7f9.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_17ba9d21.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_22247089.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_227ea6a4.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_2335d5ae.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_246824bb.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_28b4b9cd.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_2abd12cc.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_2c801b2f.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_2c8cfa8e.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_2ce76eb0.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_30351948.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_30eb7229.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_34df0e77.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_376e4d6a.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_3a4e8a1a.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_3b77a628.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_3ea4e1cf.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_44aacdb4.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_457ddfa8.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_45af6238.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_4735dba1.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_486b203c.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_49dc56cd.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_4d863d6d.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_5149a808.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_52e0bb43.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_585d6319.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_5b2d859a.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_635a7081.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_64df4905.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_70634cee.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_714553f9.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_7f7bfdbf.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_7faad91d.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_81821f8f.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_8249f3c9.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_8422b50f.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_8446c134.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_879f4e26.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_8b6d5bd7.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_8c5cd787.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_91d20bc7.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_958a12b7.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_95d91108.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_980dbb74.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_9e40c79b.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_9f499b7c.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_9f7c3b57.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_a30d5bd4.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_a6eb09c7.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_a86f7b83.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_ad5347e1.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_b0a5a993.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_bcb0192e.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_bf3c9aaa.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_c461ff88.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_c4f4e258.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_c7f0fa6d.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_c911b0e6.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_cc581964.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_cdd5a33b.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_cfd42495.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_d1c7a4ee.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_d2280d0e.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_d2a6ff69.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_d8c53ab2.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_d9c1247a.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_d9d58569.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_dabb4fd9.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_dd8fe359.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_decdffc9.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_e3a95476.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_e776dfca.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_ea70969f.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_ef41ea95.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_f384f9b1.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_f8adc38c.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_fa40b69e.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_fc538d54.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_fe814665.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_ffea4b12.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_02d001e5.json +59 -0
- package/tooling/.automation/validation/history/2025-12-30_val_0b8966dc.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_15455fbf.json +59 -0
- package/tooling/.automation/validation/history/2025-12-30_val_157e34b9.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_28d1d933.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_3442a52c.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_37f1ce1e.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_4f1d8a93.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_56ff1de3.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_664fd4e2.json +41 -0
- package/tooling/.automation/validation/history/2025-12-30_val_66afb0a7.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_7634663c.json +41 -0
- package/tooling/.automation/validation/history/2025-12-30_val_8ea830c3.json +41 -0
- package/tooling/.automation/validation/history/2025-12-30_val_998957c2.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_a52177db.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_a5b65a63.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_ae391d0e.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_c7895339.json +41 -0
- package/tooling/.automation/validation/history/2025-12-30_val_ca416593.json +41 -0
- package/tooling/.automation/validation/history/2025-12-30_val_cee19422.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_ddd4f4e6.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_f2e1394b.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_f4a7fa06.json +41 -0
- package/tooling/.automation/validation/history/2025-12-30_val_ffea3369.json +32 -0
- package/tooling/.automation/validation-config.yaml +103 -0
- package/tooling/completions/DevflowCompletion.ps1 +21 -21
- package/tooling/completions/_run-story +3 -3
- package/tooling/completions/run-story-completion.bash +8 -8
- package/tooling/docs/DOC-STANDARD.md +14 -14
- package/tooling/docs/templates/migration-spec.md +4 -4
- package/tooling/scripts/context_checkpoint.py +5 -15
- package/tooling/scripts/cost_dashboard.py +610 -13
- package/tooling/scripts/create-persona.py +1 -12
- package/tooling/scripts/create-persona.sh +44 -44
- package/tooling/scripts/lib/__init__.py +12 -1
- package/tooling/scripts/lib/agent_handoff.py +11 -2
- package/tooling/scripts/lib/agent_router.py +31 -10
- package/tooling/scripts/lib/colors.py +106 -0
- package/tooling/scripts/lib/context_monitor.py +766 -0
- package/tooling/scripts/lib/cost_config.py +229 -10
- package/tooling/scripts/lib/cost_display.py +20 -45
- package/tooling/scripts/lib/cost_tracker.py +462 -15
- package/tooling/scripts/lib/currency_converter.py +28 -5
- package/tooling/scripts/lib/pair_programming.py +102 -3
- package/tooling/scripts/lib/personality_system.py +949 -0
- package/tooling/scripts/lib/platform.py +55 -0
- package/tooling/scripts/lib/shared_memory.py +9 -3
- package/tooling/scripts/lib/swarm_orchestrator.py +514 -75
- package/tooling/scripts/lib/validation_loop.py +1014 -0
- package/tooling/scripts/memory_summarize.py +9 -2
- package/tooling/scripts/new-doc.py +2 -9
- package/tooling/scripts/personalize_agent.py +1 -12
- package/tooling/scripts/rollback-migration.sh +60 -60
- package/tooling/scripts/run-collab.ps1 +16 -16
- package/tooling/scripts/run-collab.py +88 -53
- package/tooling/scripts/run-collab.sh +4 -4
- package/tooling/scripts/run-story.py +278 -20
- package/tooling/scripts/run-story.sh +3 -3
- package/tooling/scripts/setup-checkpoint-service.py +2 -9
- package/tooling/scripts/tech-debt-tracker.py +1 -12
- package/tooling/scripts/test_adversarial_swarm.py +452 -0
- package/tooling/scripts/update_version.py +48 -2
- package/tooling/scripts/validate-overrides.py +1 -10
- package/tooling/scripts/validate-overrides.sh +40 -40
- package/tooling/scripts/validate_loop.py +162 -0
- package/tooling/scripts/validate_setup.py +2 -30
- package/.claude/skills/init/SKILL.md +0 -496
- package/bin/devflow-init.js +0 -10
- package/tooling/scripts/init-project-workflow.ps1 +0 -651
- package/tooling/scripts/init-project-workflow.py +0 -70
- package/tooling/scripts/init-project-workflow.sh +0 -746
|
@@ -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()}")
|