@pjmendonca/devflow 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +526 -0
- package/LICENSE +21 -0
- package/README.md +620 -0
- package/bin/devflow-checkpoint.js +10 -0
- package/bin/devflow-collab.js +10 -0
- package/bin/devflow-cost.js +10 -0
- package/bin/devflow-create-persona.js +10 -0
- package/bin/devflow-init.js +10 -0
- package/bin/devflow-memory.js +10 -0
- package/bin/devflow-new-doc.js +10 -0
- package/bin/devflow-personalize.js +10 -0
- package/bin/devflow-setup-checkpoint.js +10 -0
- package/bin/devflow-story.js +10 -0
- package/bin/devflow-tech-debt.js +10 -0
- package/bin/devflow-validate-overrides.js +10 -0
- package/bin/devflow-validate.js +10 -0
- package/bin/devflow-version.js +10 -0
- package/lib/constants.js +30 -0
- package/lib/exec-python.js +78 -0
- package/lib/python-check.js +178 -0
- package/package.json +64 -0
- package/tooling/.automation/agents/architect.md +135 -0
- package/tooling/.automation/agents/ba.md +70 -0
- package/tooling/.automation/agents/dev.md +79 -0
- package/tooling/.automation/agents/maintainer.md +97 -0
- package/tooling/.automation/agents/pm.md +116 -0
- package/tooling/.automation/agents/reviewer.md +141 -0
- package/tooling/.automation/agents/sm.md +61 -0
- package/tooling/.automation/agents/writer.md +193 -0
- package/tooling/.automation/config.ps1.template +61 -0
- package/tooling/.automation/config.sh.template +48 -0
- package/tooling/.automation/memory/.gitkeep +6 -0
- package/tooling/.automation/memory/knowledge/kg_integration-test.json +94 -0
- package/tooling/.automation/memory/knowledge/kg_test-story.json +300 -0
- package/tooling/.automation/memory/shared/shared_integration-test.json +30 -0
- package/tooling/.automation/memory/shared/shared_test-story.json +78 -0
- package/tooling/.automation/overrides/templates/README.md +113 -0
- package/tooling/.automation/overrides/templates/architect/README.md +27 -0
- package/tooling/.automation/overrides/templates/architect/cloud-native.yaml +92 -0
- package/tooling/.automation/overrides/templates/architect/enterprise-architect.yaml +85 -0
- package/tooling/.automation/overrides/templates/architect/pragmatic-minimalist.yaml +88 -0
- package/tooling/.automation/overrides/templates/ba/README.md +27 -0
- package/tooling/.automation/overrides/templates/ba/agile-storyteller.yaml +86 -0
- package/tooling/.automation/overrides/templates/ba/domain-expert.yaml +91 -0
- package/tooling/.automation/overrides/templates/ba/requirements-engineer.yaml +89 -0
- package/tooling/.automation/overrides/templates/dev/README.md +32 -0
- package/tooling/.automation/overrides/templates/dev/junior-mentored.yaml +39 -0
- package/tooling/.automation/overrides/templates/dev/performance-engineer.yaml +43 -0
- package/tooling/.automation/overrides/templates/dev/rapid-prototyper.yaml +52 -0
- package/tooling/.automation/overrides/templates/dev/security-focused.yaml +43 -0
- package/tooling/.automation/overrides/templates/dev/senior-fullstack.yaml +39 -0
- package/tooling/.automation/overrides/templates/maintainer/README.md +27 -0
- package/tooling/.automation/overrides/templates/maintainer/devops-maintainer.yaml +113 -0
- package/tooling/.automation/overrides/templates/maintainer/legacy-steward.yaml +94 -0
- package/tooling/.automation/overrides/templates/maintainer/oss-maintainer.yaml +94 -0
- package/tooling/.automation/overrides/templates/pm/README.md +27 -0
- package/tooling/.automation/overrides/templates/pm/agile-pm.yaml +91 -0
- package/tooling/.automation/overrides/templates/pm/hybrid-delivery.yaml +87 -0
- package/tooling/.automation/overrides/templates/pm/traditional-pm.yaml +91 -0
- package/tooling/.automation/overrides/templates/reviewer/README.md +11 -0
- package/tooling/.automation/overrides/templates/reviewer/mentoring-reviewer.yaml +45 -0
- package/tooling/.automation/overrides/templates/reviewer/quick-sanity.yaml +50 -0
- package/tooling/.automation/overrides/templates/reviewer/thorough-critic.yaml +48 -0
- package/tooling/.automation/overrides/templates/sm/README.md +11 -0
- package/tooling/.automation/overrides/templates/sm/agile-coach.yaml +52 -0
- package/tooling/.automation/overrides/templates/sm/startup-pm.yaml +50 -0
- package/tooling/.automation/overrides/templates/sm/technical-lead.yaml +47 -0
- package/tooling/.automation/overrides/templates/user-profile.template.yaml +62 -0
- package/tooling/.automation/overrides/templates/writer/README.md +27 -0
- package/tooling/.automation/overrides/templates/writer/api-documentarian.yaml +99 -0
- package/tooling/.automation/overrides/templates/writer/docs-as-code.yaml +108 -0
- package/tooling/.automation/overrides/templates/writer/user-guide-author.yaml +100 -0
- package/tooling/completions/DevflowCompletion.ps1 +213 -0
- package/tooling/completions/_run-story +116 -0
- package/tooling/completions/run-story-completion.bash +136 -0
- package/tooling/docs/DOC-STANDARD.md +717 -0
- package/tooling/docs/sprint-status.yaml.template +24 -0
- package/tooling/docs/templates/bug-report.md +234 -0
- package/tooling/docs/templates/migration-spec.md +274 -0
- package/tooling/docs/templates/refactor-spec.md +86 -0
- package/tooling/docs/templates/tech-debt.md +86 -0
- package/tooling/scripts/context_checkpoint.py +556 -0
- package/tooling/scripts/cost_dashboard.py +617 -0
- package/tooling/scripts/create-persona.py +690 -0
- package/tooling/scripts/create-persona.sh +435 -0
- package/tooling/scripts/init-project-workflow.ps1 +651 -0
- package/tooling/scripts/init-project-workflow.py +70 -0
- package/tooling/scripts/init-project-workflow.sh +746 -0
- package/tooling/scripts/lib/__init__.py +35 -0
- package/tooling/scripts/lib/agent_handoff.py +526 -0
- package/tooling/scripts/lib/agent_router.py +698 -0
- package/tooling/scripts/lib/checkpoint-integration.ps1 +245 -0
- package/tooling/scripts/lib/checkpoint-integration.sh +191 -0
- package/tooling/scripts/lib/claude-cli.ps1 +952 -0
- package/tooling/scripts/lib/claude-cli.sh +1293 -0
- package/tooling/scripts/lib/cost_config.py +222 -0
- package/tooling/scripts/lib/cost_display.py +443 -0
- package/tooling/scripts/lib/cost_tracker.py +710 -0
- package/tooling/scripts/lib/currency_converter.py +328 -0
- package/tooling/scripts/lib/errors.py +438 -0
- package/tooling/scripts/lib/override-loader.sh +286 -0
- package/tooling/scripts/lib/pair_programming.py +589 -0
- package/tooling/scripts/lib/shared_memory.py +637 -0
- package/tooling/scripts/lib/swarm_orchestrator.py +689 -0
- package/tooling/scripts/memory_summarize.py +324 -0
- package/tooling/scripts/new-doc.ps1 +405 -0
- package/tooling/scripts/new-doc.py +93 -0
- package/tooling/scripts/new-doc.sh +534 -0
- package/tooling/scripts/personalize_agent.py +385 -0
- package/tooling/scripts/rollback-migration.sh +540 -0
- package/tooling/scripts/run-collab.ps1 +251 -0
- package/tooling/scripts/run-collab.py +605 -0
- package/tooling/scripts/run-collab.sh +110 -0
- package/tooling/scripts/run-story.ps1 +490 -0
- package/tooling/scripts/run-story.py +387 -0
- package/tooling/scripts/run-story.sh +467 -0
- package/tooling/scripts/setup-checkpoint-service.ps1 +219 -0
- package/tooling/scripts/setup-checkpoint-service.py +87 -0
- package/tooling/scripts/setup-checkpoint-service.sh +236 -0
- package/tooling/scripts/tech-debt-tracker.py +608 -0
- package/tooling/scripts/update_version.py +244 -0
- package/tooling/scripts/validate-overrides.py +511 -0
- package/tooling/scripts/validate-overrides.sh +432 -0
- package/tooling/scripts/validate_setup.py +539 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Pair Programming Mode - DEV + REVIEWER Interleaved Collaboration
|
|
4
|
+
|
|
5
|
+
Real-time collaboration between DEV and REVIEWER where:
|
|
6
|
+
- DEV implements in small chunks
|
|
7
|
+
- REVIEWER provides immediate feedback
|
|
8
|
+
- DEV addresses feedback before continuing
|
|
9
|
+
- Results in higher quality, fewer iterations
|
|
10
|
+
|
|
11
|
+
Features:
|
|
12
|
+
- Chunk-based development
|
|
13
|
+
- Real-time feedback loops
|
|
14
|
+
- Incremental refinement
|
|
15
|
+
- Shared context maintenance
|
|
16
|
+
- Automatic issue tracking
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
from lib.pair_programming import PairSession, start_pair_session
|
|
20
|
+
|
|
21
|
+
session = start_pair_session(story_key="3-5", task="Implement user login")
|
|
22
|
+
result = session.run()
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import re
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from datetime import datetime
|
|
30
|
+
from enum import Enum
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Optional
|
|
33
|
+
|
|
34
|
+
# Import dependencies
|
|
35
|
+
try:
|
|
36
|
+
from shared_memory import get_knowledge_graph, get_shared_memory
|
|
37
|
+
except ImportError:
|
|
38
|
+
from lib.shared_memory import get_knowledge_graph, get_shared_memory
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
PROJECT_ROOT = Path(__file__).parent.parent.parent.parent
|
|
42
|
+
CLAUDE_CLI = "claude.cmd" if sys.platform == "win32" else "claude"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ChunkType(Enum):
|
|
46
|
+
"""Types of code chunks."""
|
|
47
|
+
|
|
48
|
+
DESIGN = "design" # Architecture/design decision
|
|
49
|
+
IMPLEMENTATION = "implementation" # Core code
|
|
50
|
+
TEST = "test" # Test code
|
|
51
|
+
REFACTOR = "refactor" # Refactoring
|
|
52
|
+
FIX = "fix" # Bug fix
|
|
53
|
+
DOCUMENTATION = "documentation" # Docs/comments
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FeedbackType(Enum):
|
|
57
|
+
"""Types of reviewer feedback."""
|
|
58
|
+
|
|
59
|
+
APPROVE = "approve" # Good to proceed
|
|
60
|
+
MINOR = "minor" # Minor issues, can proceed
|
|
61
|
+
MAJOR = "major" # Major issues, must fix
|
|
62
|
+
BLOCKING = "blocking" # Cannot proceed until fixed
|
|
63
|
+
QUESTION = "question" # Needs clarification
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class CodeChunk:
|
|
68
|
+
"""A chunk of code being developed."""
|
|
69
|
+
|
|
70
|
+
chunk_id: str
|
|
71
|
+
chunk_type: ChunkType
|
|
72
|
+
description: str
|
|
73
|
+
content: str
|
|
74
|
+
file_path: Optional[str] = None
|
|
75
|
+
line_range: Optional[tuple[int, int]] = None
|
|
76
|
+
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
77
|
+
|
|
78
|
+
def to_dict(self) -> dict:
|
|
79
|
+
return {
|
|
80
|
+
"chunk_id": self.chunk_id,
|
|
81
|
+
"chunk_type": self.chunk_type.value,
|
|
82
|
+
"description": self.description,
|
|
83
|
+
"content": self.content[:500] + "..." if len(self.content) > 500 else self.content,
|
|
84
|
+
"file_path": self.file_path,
|
|
85
|
+
"line_range": self.line_range,
|
|
86
|
+
"timestamp": self.timestamp,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class ReviewFeedback:
|
|
92
|
+
"""Feedback from reviewer on a chunk."""
|
|
93
|
+
|
|
94
|
+
chunk_id: str
|
|
95
|
+
feedback_type: FeedbackType
|
|
96
|
+
comments: list[str]
|
|
97
|
+
suggestions: list[str] = field(default_factory=list)
|
|
98
|
+
must_fix: list[str] = field(default_factory=list)
|
|
99
|
+
nice_to_have: list[str] = field(default_factory=list)
|
|
100
|
+
approved: bool = False
|
|
101
|
+
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
102
|
+
|
|
103
|
+
def to_dict(self) -> dict:
|
|
104
|
+
return {
|
|
105
|
+
"chunk_id": self.chunk_id,
|
|
106
|
+
"feedback_type": self.feedback_type.value,
|
|
107
|
+
"comments": self.comments,
|
|
108
|
+
"suggestions": self.suggestions,
|
|
109
|
+
"must_fix": self.must_fix,
|
|
110
|
+
"nice_to_have": self.nice_to_have,
|
|
111
|
+
"approved": self.approved,
|
|
112
|
+
"timestamp": self.timestamp,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
def has_blocking_issues(self) -> bool:
|
|
116
|
+
return self.feedback_type in [FeedbackType.MAJOR, FeedbackType.BLOCKING]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class PairExchange:
|
|
121
|
+
"""A single exchange in the pair programming session."""
|
|
122
|
+
|
|
123
|
+
exchange_id: int
|
|
124
|
+
chunk: CodeChunk
|
|
125
|
+
feedback: Optional[ReviewFeedback] = None
|
|
126
|
+
revision: Optional[CodeChunk] = None
|
|
127
|
+
resolved: bool = False
|
|
128
|
+
|
|
129
|
+
def to_dict(self) -> dict:
|
|
130
|
+
return {
|
|
131
|
+
"exchange_id": self.exchange_id,
|
|
132
|
+
"chunk": self.chunk.to_dict(),
|
|
133
|
+
"feedback": self.feedback.to_dict() if self.feedback else None,
|
|
134
|
+
"revision": self.revision.to_dict() if self.revision else None,
|
|
135
|
+
"resolved": self.resolved,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass
|
|
140
|
+
class PairSessionResult:
|
|
141
|
+
"""Result of a pair programming session."""
|
|
142
|
+
|
|
143
|
+
story_key: str
|
|
144
|
+
task: str
|
|
145
|
+
exchanges: list[PairExchange]
|
|
146
|
+
final_code: str
|
|
147
|
+
files_created: list[str]
|
|
148
|
+
files_modified: list[str]
|
|
149
|
+
total_chunks: int
|
|
150
|
+
total_revisions: int
|
|
151
|
+
approval_rate: float
|
|
152
|
+
start_time: str
|
|
153
|
+
end_time: str
|
|
154
|
+
|
|
155
|
+
def to_dict(self) -> dict:
|
|
156
|
+
return {
|
|
157
|
+
"story_key": self.story_key,
|
|
158
|
+
"task": self.task,
|
|
159
|
+
"exchanges": [e.to_dict() for e in self.exchanges],
|
|
160
|
+
"final_code": self.final_code[:1000]
|
|
161
|
+
if len(self.final_code) > 1000
|
|
162
|
+
else self.final_code,
|
|
163
|
+
"files_created": self.files_created,
|
|
164
|
+
"files_modified": self.files_modified,
|
|
165
|
+
"total_chunks": self.total_chunks,
|
|
166
|
+
"total_revisions": self.total_revisions,
|
|
167
|
+
"approval_rate": self.approval_rate,
|
|
168
|
+
"start_time": self.start_time,
|
|
169
|
+
"end_time": self.end_time,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
def to_summary(self) -> str:
|
|
173
|
+
"""Generate human-readable summary."""
|
|
174
|
+
lines = [
|
|
175
|
+
f"## Pair Programming Result: {self.story_key}",
|
|
176
|
+
"",
|
|
177
|
+
f"**Task**: {self.task}",
|
|
178
|
+
f"**Exchanges**: {len(self.exchanges)}",
|
|
179
|
+
f"**Chunks**: {self.total_chunks}",
|
|
180
|
+
f"**Revisions**: {self.total_revisions}",
|
|
181
|
+
f"**Approval Rate**: {self.approval_rate:.0%}",
|
|
182
|
+
"",
|
|
183
|
+
"### Files Created",
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
for f in self.files_created:
|
|
187
|
+
lines.append(f"- `{f}`")
|
|
188
|
+
|
|
189
|
+
if self.files_modified:
|
|
190
|
+
lines.append("")
|
|
191
|
+
lines.append("### Files Modified")
|
|
192
|
+
for f in self.files_modified:
|
|
193
|
+
lines.append(f"- `{f}`")
|
|
194
|
+
|
|
195
|
+
return "\n".join(lines)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@dataclass
|
|
199
|
+
class PairConfig:
|
|
200
|
+
"""Configuration for pair programming session."""
|
|
201
|
+
|
|
202
|
+
max_revisions_per_chunk: int = 3
|
|
203
|
+
timeout_seconds: int = 180
|
|
204
|
+
verbose: bool = True
|
|
205
|
+
auto_apply_fixes: bool = True
|
|
206
|
+
chunk_size_hint: str = "medium" # small, medium, large
|
|
207
|
+
reviewer_model: str = "opus"
|
|
208
|
+
dev_model: str = "opus"
|
|
209
|
+
|
|
210
|
+
def to_dict(self) -> dict:
|
|
211
|
+
return {
|
|
212
|
+
"max_revisions_per_chunk": self.max_revisions_per_chunk,
|
|
213
|
+
"timeout_seconds": self.timeout_seconds,
|
|
214
|
+
"auto_apply_fixes": self.auto_apply_fixes,
|
|
215
|
+
"chunk_size_hint": self.chunk_size_hint,
|
|
216
|
+
"reviewer_model": self.reviewer_model,
|
|
217
|
+
"dev_model": self.dev_model,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class PairSession:
|
|
222
|
+
"""A pair programming session between DEV and REVIEWER."""
|
|
223
|
+
|
|
224
|
+
def __init__(self, story_key: str, task: str, config: Optional[PairConfig] = None):
|
|
225
|
+
self.story_key = story_key
|
|
226
|
+
self.task = task
|
|
227
|
+
self.config = config or PairConfig()
|
|
228
|
+
self.project_root = PROJECT_ROOT
|
|
229
|
+
self.shared_memory = get_shared_memory(story_key)
|
|
230
|
+
self.knowledge_graph = get_knowledge_graph(story_key)
|
|
231
|
+
|
|
232
|
+
self.exchanges: list[PairExchange] = []
|
|
233
|
+
self.files_created: list[str] = []
|
|
234
|
+
self.files_modified: list[str] = []
|
|
235
|
+
self.chunk_counter = 0
|
|
236
|
+
self.exchange_counter = 0
|
|
237
|
+
|
|
238
|
+
def _log(self, message: str, agent: str = "SYSTEM"):
|
|
239
|
+
"""Log a message."""
|
|
240
|
+
if self.config.verbose:
|
|
241
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
242
|
+
emoji = {"DEV": "💻", "REVIEWER": "👀", "SYSTEM": "⚙️"}.get(agent, "•")
|
|
243
|
+
print(f"[{timestamp}] {emoji} [{agent}] {message}")
|
|
244
|
+
|
|
245
|
+
def _invoke_agent(self, agent: str, prompt: str) -> str:
|
|
246
|
+
"""Invoke an agent with Claude CLI."""
|
|
247
|
+
model = self.config.dev_model if agent == "DEV" else self.config.reviewer_model
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
result = subprocess.run(
|
|
251
|
+
[CLAUDE_CLI, "--print", "--model", model, "-p", prompt],
|
|
252
|
+
capture_output=True,
|
|
253
|
+
text=True,
|
|
254
|
+
timeout=self.config.timeout_seconds,
|
|
255
|
+
cwd=str(self.project_root),
|
|
256
|
+
)
|
|
257
|
+
return result.stdout + result.stderr
|
|
258
|
+
except subprocess.TimeoutExpired:
|
|
259
|
+
return "[TIMEOUT: Agent did not respond in time]"
|
|
260
|
+
except Exception as e:
|
|
261
|
+
return f"[ERROR: {str(e)}]"
|
|
262
|
+
|
|
263
|
+
def _generate_chunk_id(self) -> str:
|
|
264
|
+
"""Generate unique chunk ID."""
|
|
265
|
+
self.chunk_counter += 1
|
|
266
|
+
return f"chunk_{self.chunk_counter:03d}"
|
|
267
|
+
|
|
268
|
+
def _parse_dev_output(self, output: str) -> CodeChunk:
|
|
269
|
+
"""Parse DEV output into a CodeChunk."""
|
|
270
|
+
# Try to extract file path
|
|
271
|
+
file_match = re.search(r'(?:file|path):\s*[`"]?([^\s`"]+)[`"]?', output, re.IGNORECASE)
|
|
272
|
+
file_path = file_match.group(1) if file_match else None
|
|
273
|
+
|
|
274
|
+
# Try to extract code blocks
|
|
275
|
+
code_blocks = re.findall(r"```[\w]*\n(.*?)```", output, re.DOTALL)
|
|
276
|
+
content = "\n\n".join(code_blocks) if code_blocks else output
|
|
277
|
+
|
|
278
|
+
# Determine chunk type
|
|
279
|
+
chunk_type = ChunkType.IMPLEMENTATION
|
|
280
|
+
output_lower = output.lower()
|
|
281
|
+
if "test" in output_lower:
|
|
282
|
+
chunk_type = ChunkType.TEST
|
|
283
|
+
elif "refactor" in output_lower:
|
|
284
|
+
chunk_type = ChunkType.REFACTOR
|
|
285
|
+
elif "design" in output_lower or "architecture" in output_lower:
|
|
286
|
+
chunk_type = ChunkType.DESIGN
|
|
287
|
+
elif "fix" in output_lower or "bug" in output_lower:
|
|
288
|
+
chunk_type = ChunkType.FIX
|
|
289
|
+
|
|
290
|
+
# Extract description
|
|
291
|
+
desc_match = re.search(r"^#+\s*(.+)$", output, re.MULTILINE)
|
|
292
|
+
description = desc_match.group(1) if desc_match else "Code implementation"
|
|
293
|
+
|
|
294
|
+
return CodeChunk(
|
|
295
|
+
chunk_id=self._generate_chunk_id(),
|
|
296
|
+
chunk_type=chunk_type,
|
|
297
|
+
description=description[:100],
|
|
298
|
+
content=content,
|
|
299
|
+
file_path=file_path,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
def _parse_reviewer_output(self, output: str, chunk_id: str) -> ReviewFeedback:
|
|
303
|
+
"""Parse REVIEWER output into ReviewFeedback."""
|
|
304
|
+
output_lower = output.lower()
|
|
305
|
+
|
|
306
|
+
# Determine feedback type
|
|
307
|
+
if any(word in output_lower for word in ["blocking", "cannot proceed", "critical"]):
|
|
308
|
+
feedback_type = FeedbackType.BLOCKING
|
|
309
|
+
elif any(word in output_lower for word in ["major", "significant", "must fix"]):
|
|
310
|
+
feedback_type = FeedbackType.MAJOR
|
|
311
|
+
elif any(word in output_lower for word in ["minor", "small", "nitpick"]):
|
|
312
|
+
feedback_type = FeedbackType.MINOR
|
|
313
|
+
elif any(word in output_lower for word in ["question", "clarify", "unclear"]):
|
|
314
|
+
feedback_type = FeedbackType.QUESTION
|
|
315
|
+
else:
|
|
316
|
+
feedback_type = FeedbackType.APPROVE
|
|
317
|
+
|
|
318
|
+
# Extract comments
|
|
319
|
+
comments = []
|
|
320
|
+
comment_patterns = [
|
|
321
|
+
r"[-•]\s*(.+)",
|
|
322
|
+
r"\d+\.\s*(.+)",
|
|
323
|
+
]
|
|
324
|
+
for pattern in comment_patterns:
|
|
325
|
+
matches = re.findall(pattern, output)
|
|
326
|
+
comments.extend(matches[:10])
|
|
327
|
+
|
|
328
|
+
# Extract must-fix issues
|
|
329
|
+
must_fix = []
|
|
330
|
+
must_fix_section = re.search(
|
|
331
|
+
r"(?:must fix|blocking|required).*?:(.*?)(?:\n\n|\Z)", output, re.IGNORECASE | re.DOTALL
|
|
332
|
+
)
|
|
333
|
+
if must_fix_section:
|
|
334
|
+
issues = re.findall(r"[-•]\s*(.+)", must_fix_section.group(1))
|
|
335
|
+
must_fix.extend(issues)
|
|
336
|
+
|
|
337
|
+
# Extract suggestions
|
|
338
|
+
suggestions = []
|
|
339
|
+
suggestion_section = re.search(
|
|
340
|
+
r"(?:suggest|consider|recommend).*?:(.*?)(?:\n\n|\Z)", output, re.IGNORECASE | re.DOTALL
|
|
341
|
+
)
|
|
342
|
+
if suggestion_section:
|
|
343
|
+
sugs = re.findall(r"[-•]\s*(.+)", suggestion_section.group(1))
|
|
344
|
+
suggestions.extend(sugs)
|
|
345
|
+
|
|
346
|
+
# Check for approval
|
|
347
|
+
approved = feedback_type == FeedbackType.APPROVE or any(
|
|
348
|
+
word in output_lower for word in ["lgtm", "approved", "looks good", "ship it"]
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
return ReviewFeedback(
|
|
352
|
+
chunk_id=chunk_id,
|
|
353
|
+
feedback_type=feedback_type,
|
|
354
|
+
comments=comments[:10],
|
|
355
|
+
suggestions=suggestions[:5],
|
|
356
|
+
must_fix=must_fix[:5],
|
|
357
|
+
approved=approved,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
def _build_dev_prompt(
|
|
361
|
+
self, task_part: str, context: str, previous_feedback: Optional[ReviewFeedback] = None
|
|
362
|
+
) -> str:
|
|
363
|
+
"""Build prompt for DEV agent."""
|
|
364
|
+
|
|
365
|
+
base_prompt = f"""You are in a PAIR PROGRAMMING session with a REVIEWER.
|
|
366
|
+
Work in small, focused chunks. After each chunk, wait for reviewer feedback.
|
|
367
|
+
|
|
368
|
+
## Task
|
|
369
|
+
{task_part}
|
|
370
|
+
|
|
371
|
+
## Context
|
|
372
|
+
{context}
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
if previous_feedback:
|
|
376
|
+
feedback_text = "\n".join(
|
|
377
|
+
[
|
|
378
|
+
"## Reviewer Feedback (address these)",
|
|
379
|
+
"",
|
|
380
|
+
"**Issues to Fix:**",
|
|
381
|
+
*[f"- ❌ {issue}" for issue in previous_feedback.must_fix],
|
|
382
|
+
"",
|
|
383
|
+
"**Suggestions:**",
|
|
384
|
+
*[f"- 💡 {sug}" for sug in previous_feedback.suggestions],
|
|
385
|
+
]
|
|
386
|
+
)
|
|
387
|
+
base_prompt += f"\n\n{feedback_text}\n"
|
|
388
|
+
|
|
389
|
+
base_prompt += """
|
|
390
|
+
## Instructions
|
|
391
|
+
1. Implement ONE focused chunk of code
|
|
392
|
+
2. Show the file path and code clearly
|
|
393
|
+
3. Explain your approach briefly
|
|
394
|
+
4. Keep the chunk small enough for quick review
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
return base_prompt
|
|
398
|
+
|
|
399
|
+
def _build_reviewer_prompt(self, chunk: CodeChunk, accumulated_code: str) -> str:
|
|
400
|
+
"""Build prompt for REVIEWER agent."""
|
|
401
|
+
|
|
402
|
+
return f"""You are in a PAIR PROGRAMMING session reviewing DEV's work in real-time.
|
|
403
|
+
|
|
404
|
+
## Current Chunk to Review
|
|
405
|
+
**Type**: {chunk.chunk_type.value}
|
|
406
|
+
**Description**: {chunk.description}
|
|
407
|
+
**File**: {chunk.file_path or "Not specified"}
|
|
408
|
+
|
|
409
|
+
```
|
|
410
|
+
{chunk.content}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## Accumulated Code So Far
|
|
414
|
+
```
|
|
415
|
+
{accumulated_code[-2000:] if len(accumulated_code) > 2000 else accumulated_code}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
## Instructions
|
|
419
|
+
1. Review this chunk for:
|
|
420
|
+
- Correctness
|
|
421
|
+
- Code quality
|
|
422
|
+
- Best practices
|
|
423
|
+
- Potential bugs
|
|
424
|
+
- Test coverage needs
|
|
425
|
+
|
|
426
|
+
2. Categorize issues as:
|
|
427
|
+
- **BLOCKING**: Cannot proceed
|
|
428
|
+
- **MUST FIX**: Required before merge
|
|
429
|
+
- **SUGGESTION**: Nice to have
|
|
430
|
+
|
|
431
|
+
3. If the code is good, say "LGTM" or "Approved"
|
|
432
|
+
|
|
433
|
+
4. Be constructive and specific
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
def run(self) -> PairSessionResult:
|
|
437
|
+
"""Run the pair programming session."""
|
|
438
|
+
start_time = datetime.now().isoformat()
|
|
439
|
+
|
|
440
|
+
self._log(f"Starting pair session for: {self.task[:50]}...")
|
|
441
|
+
|
|
442
|
+
# Break task into parts (for now, treat as single task)
|
|
443
|
+
task_parts = [self.task]
|
|
444
|
+
|
|
445
|
+
accumulated_code = ""
|
|
446
|
+
total_revisions = 0
|
|
447
|
+
approved_chunks = 0
|
|
448
|
+
|
|
449
|
+
for i, task_part in enumerate(task_parts):
|
|
450
|
+
self._log(f"Working on part {i + 1}/{len(task_parts)}")
|
|
451
|
+
|
|
452
|
+
# Get initial context
|
|
453
|
+
context = f"Story: {self.story_key}\n"
|
|
454
|
+
context += self.shared_memory.to_context_string(5)
|
|
455
|
+
|
|
456
|
+
# DEV creates initial chunk
|
|
457
|
+
self._log("Creating initial implementation...", "DEV")
|
|
458
|
+
dev_prompt = self._build_dev_prompt(task_part, context)
|
|
459
|
+
dev_output = self._invoke_agent("DEV", dev_prompt)
|
|
460
|
+
|
|
461
|
+
chunk = self._parse_dev_output(dev_output)
|
|
462
|
+
self._log(f"Created chunk: {chunk.description}", "DEV")
|
|
463
|
+
|
|
464
|
+
# Track file
|
|
465
|
+
if chunk.file_path:
|
|
466
|
+
if (
|
|
467
|
+
chunk.file_path not in self.files_created
|
|
468
|
+
and chunk.file_path not in self.files_modified
|
|
469
|
+
):
|
|
470
|
+
self.files_created.append(chunk.file_path)
|
|
471
|
+
|
|
472
|
+
accumulated_code += f"\n\n// {chunk.description}\n{chunk.content}"
|
|
473
|
+
|
|
474
|
+
# REVIEWER reviews
|
|
475
|
+
self._log("Reviewing chunk...", "REVIEWER")
|
|
476
|
+
reviewer_prompt = self._build_reviewer_prompt(chunk, accumulated_code)
|
|
477
|
+
reviewer_output = self._invoke_agent("REVIEWER", reviewer_prompt)
|
|
478
|
+
|
|
479
|
+
feedback = self._parse_reviewer_output(reviewer_output, chunk.chunk_id)
|
|
480
|
+
self._log(f"Feedback: {feedback.feedback_type.value}", "REVIEWER")
|
|
481
|
+
|
|
482
|
+
exchange = PairExchange(
|
|
483
|
+
exchange_id=self.exchange_counter,
|
|
484
|
+
chunk=chunk,
|
|
485
|
+
feedback=feedback,
|
|
486
|
+
resolved=feedback.approved,
|
|
487
|
+
)
|
|
488
|
+
self.exchange_counter += 1
|
|
489
|
+
|
|
490
|
+
# Revision loop if needed
|
|
491
|
+
revision_count = 0
|
|
492
|
+
while (
|
|
493
|
+
feedback.has_blocking_issues()
|
|
494
|
+
and revision_count < self.config.max_revisions_per_chunk
|
|
495
|
+
):
|
|
496
|
+
revision_count += 1
|
|
497
|
+
total_revisions += 1
|
|
498
|
+
self._log(f"Revision {revision_count} needed", "SYSTEM")
|
|
499
|
+
|
|
500
|
+
# DEV revises
|
|
501
|
+
self._log("Addressing feedback...", "DEV")
|
|
502
|
+
dev_prompt = self._build_dev_prompt(task_part, context, feedback)
|
|
503
|
+
dev_output = self._invoke_agent("DEV", dev_prompt)
|
|
504
|
+
|
|
505
|
+
revised_chunk = self._parse_dev_output(dev_output)
|
|
506
|
+
exchange.revision = revised_chunk
|
|
507
|
+
|
|
508
|
+
# Update accumulated code
|
|
509
|
+
accumulated_code += (
|
|
510
|
+
f"\n\n// Revision: {revised_chunk.description}\n{revised_chunk.content}"
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
# REVIEWER re-reviews
|
|
514
|
+
self._log("Re-reviewing...", "REVIEWER")
|
|
515
|
+
reviewer_prompt = self._build_reviewer_prompt(revised_chunk, accumulated_code)
|
|
516
|
+
reviewer_output = self._invoke_agent("REVIEWER", reviewer_prompt)
|
|
517
|
+
|
|
518
|
+
feedback = self._parse_reviewer_output(reviewer_output, revised_chunk.chunk_id)
|
|
519
|
+
exchange.feedback = feedback
|
|
520
|
+
exchange.resolved = feedback.approved
|
|
521
|
+
|
|
522
|
+
self._log(f"Feedback: {feedback.feedback_type.value}", "REVIEWER")
|
|
523
|
+
|
|
524
|
+
if exchange.resolved or feedback.approved:
|
|
525
|
+
approved_chunks += 1
|
|
526
|
+
self._log("✅ Chunk approved!", "SYSTEM")
|
|
527
|
+
else:
|
|
528
|
+
self._log("⚠️ Moving on with unresolved issues", "SYSTEM")
|
|
529
|
+
|
|
530
|
+
self.exchanges.append(exchange)
|
|
531
|
+
|
|
532
|
+
# Record in shared memory
|
|
533
|
+
self.shared_memory.add(
|
|
534
|
+
agent="PAIR",
|
|
535
|
+
content=f"Completed chunk: {chunk.description} ({feedback.feedback_type.value})",
|
|
536
|
+
tags=["pair-programming", "chunk"],
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
# Calculate approval rate
|
|
540
|
+
total_chunks = len(self.exchanges)
|
|
541
|
+
approval_rate = approved_chunks / total_chunks if total_chunks > 0 else 0
|
|
542
|
+
|
|
543
|
+
self._log(f"Session complete. Approval rate: {approval_rate:.0%}", "SYSTEM")
|
|
544
|
+
|
|
545
|
+
return PairSessionResult(
|
|
546
|
+
story_key=self.story_key,
|
|
547
|
+
task=self.task,
|
|
548
|
+
exchanges=self.exchanges,
|
|
549
|
+
final_code=accumulated_code,
|
|
550
|
+
files_created=self.files_created,
|
|
551
|
+
files_modified=self.files_modified,
|
|
552
|
+
total_chunks=total_chunks,
|
|
553
|
+
total_revisions=total_revisions,
|
|
554
|
+
approval_rate=approval_rate,
|
|
555
|
+
start_time=start_time,
|
|
556
|
+
end_time=datetime.now().isoformat(),
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
# Convenience functions
|
|
561
|
+
def start_pair_session(story_key: str, task: str, **config_kwargs) -> PairSession:
|
|
562
|
+
"""Start a new pair programming session."""
|
|
563
|
+
config = PairConfig(**config_kwargs)
|
|
564
|
+
return PairSession(story_key, task, config)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def run_pair_session(story_key: str, task: str, **config_kwargs) -> PairSessionResult:
|
|
568
|
+
"""Run a complete pair programming session."""
|
|
569
|
+
session = start_pair_session(story_key, task, **config_kwargs)
|
|
570
|
+
return session.run()
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
if __name__ == "__main__":
|
|
574
|
+
print("=== Pair Programming Mode Demo ===\n")
|
|
575
|
+
print("This module enables real-time DEV + REVIEWER collaboration.")
|
|
576
|
+
print("\nExample usage:")
|
|
577
|
+
print("""
|
|
578
|
+
from lib.pair_programming import run_pair_session
|
|
579
|
+
|
|
580
|
+
result = run_pair_session(
|
|
581
|
+
story_key="3-5",
|
|
582
|
+
task="Implement user authentication endpoint",
|
|
583
|
+
max_revisions_per_chunk=2,
|
|
584
|
+
verbose=True
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
print(result.to_summary())
|
|
588
|
+
print(f"Approval rate: {result.approval_rate:.0%}")
|
|
589
|
+
""")
|