@pjmendonca/devflow 1.19.0 → 1.20.1

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 (55) hide show
  1. package/.claude/hooks/session-startup.sh +20 -0
  2. package/.claude/settings.json +0 -4
  3. package/.claude/skills/dashboard/SKILL.md +118 -0
  4. package/CHANGELOG.md +26 -0
  5. package/README.md +2 -2
  6. package/bin/devflow-dashboard.js +10 -0
  7. package/bin/devflow-swarm.js +11 -0
  8. package/bin/devflow.js +2 -0
  9. package/package.json +3 -1
  10. package/tooling/.automation/memory/knowledge/kg_integration-test.json +212 -1
  11. package/tooling/.automation/memory/knowledge/kg_test-story.json +710 -2
  12. package/tooling/.automation/memory/shared/shared_integration-test.json +61 -1
  13. package/tooling/.automation/memory/shared/shared_test-story.json +181 -1
  14. package/tooling/.automation/memory/shared/shared_test.json +313 -1
  15. package/tooling/.automation/memory/shared/shared_validation-check.json +66 -1
  16. package/tooling/.automation/validation/history/2026-01-16_val_0b81ec2f.json +41 -0
  17. package/tooling/.automation/validation/history/2026-01-16_val_26c18e64.json +32 -0
  18. package/tooling/.automation/validation/history/2026-01-16_val_32af0152.json +32 -0
  19. package/tooling/.automation/validation/history/2026-01-16_val_353d1569.json +32 -0
  20. package/tooling/.automation/validation/history/2026-01-16_val_39e3c143.json +59 -0
  21. package/tooling/.automation/validation/history/2026-01-16_val_77fb42e4.json +32 -0
  22. package/tooling/.automation/validation/history/2026-01-16_val_a0752656.json +41 -0
  23. package/tooling/.automation/validation/history/2026-01-16_val_a29213b0.json +41 -0
  24. package/tooling/.automation/validation/history/2026-01-16_val_a9375d4c.json +32 -0
  25. package/tooling/.automation/validation/history/2026-01-16_val_c147bbdf.json +32 -0
  26. package/tooling/.automation/validation/history/2026-01-16_val_d06ccf8d.json +32 -0
  27. package/tooling/.automation/validation/history/2026-01-16_val_d6a80295.json +59 -0
  28. package/tooling/.automation/validation/history/2026-01-16_val_dce5005d.json +41 -0
  29. package/tooling/.automation/validation/history/2026-01-16_val_e53b3a63.json +32 -0
  30. package/tooling/.automation/validation/history/2026-01-18_val_108c18cf.json +32 -0
  31. package/tooling/.automation/validation/history/2026-01-18_val_35ee606f.json +32 -0
  32. package/tooling/.automation/validation/history/2026-01-18_val_3fc7268b.json +41 -0
  33. package/tooling/.automation/validation/history/2026-01-18_val_49f0bb17.json +32 -0
  34. package/tooling/.automation/validation/history/2026-01-18_val_53c928d2.json +59 -0
  35. package/tooling/.automation/validation/history/2026-01-18_val_55604791.json +32 -0
  36. package/tooling/.automation/validation/history/2026-01-18_val_67e695f0.json +41 -0
  37. package/tooling/.automation/validation/history/2026-01-18_val_82784713.json +41 -0
  38. package/tooling/.automation/validation/history/2026-01-18_val_94a8e584.json +32 -0
  39. package/tooling/.automation/validation/history/2026-01-18_val_95353af0.json +32 -0
  40. package/tooling/.automation/validation/history/2026-01-18_val_9a046f3a.json +32 -0
  41. package/tooling/.automation/validation/history/2026-01-18_val_b3443d2e.json +32 -0
  42. package/tooling/.automation/validation/history/2026-01-18_val_bfd298f4.json +32 -0
  43. package/tooling/.automation/validation/history/2026-01-18_val_cfc2a362.json +32 -0
  44. package/tooling/.automation/validation/history/2026-01-18_val_e581a3d2.json +41 -0
  45. package/tooling/scripts/lib/__init__.py +1 -3
  46. package/tooling/scripts/lib/agent_router.py +0 -4
  47. package/tooling/scripts/lib/cost_display.py +7 -1
  48. package/tooling/scripts/lib/swarm_orchestrator.py +14 -12
  49. package/tooling/scripts/live_dashboard.py +832 -0
  50. package/tooling/scripts/new-doc.py +1 -1
  51. package/tooling/scripts/run-collab.py +3 -47
  52. package/tooling/scripts/run-story.py +21 -9
  53. package/tooling/scripts/setup-checkpoint-service.py +1 -1
  54. package/.claude/commands/pair.md +0 -23
  55. package/tooling/scripts/lib/pair_programming.py +0 -688
@@ -1,688 +0,0 @@
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
- from dataclasses import dataclass, field
28
- from datetime import datetime
29
- from enum import Enum
30
- from pathlib import Path
31
- from typing import Optional
32
-
33
- # Import dependencies
34
- try:
35
- from platform import IS_WINDOWS
36
-
37
- from shared_memory import get_knowledge_graph, get_shared_memory
38
- except ImportError:
39
- from lib.platform import IS_WINDOWS
40
- from lib.shared_memory import get_knowledge_graph, get_shared_memory
41
-
42
- # Try to import validation loop
43
- try:
44
- from validation_loop import (
45
- INTER_PHASE_GATES,
46
- LoopContext,
47
- ValidationLoop,
48
- )
49
-
50
- HAS_VALIDATION = True
51
- except ImportError:
52
- try:
53
- from lib.validation_loop import (
54
- INTER_PHASE_GATES,
55
- LoopContext,
56
- ValidationLoop,
57
- )
58
-
59
- HAS_VALIDATION = True
60
- except ImportError:
61
- HAS_VALIDATION = False
62
-
63
-
64
- PROJECT_ROOT = Path(__file__).parent.parent.parent.parent
65
- CLAUDE_CLI = "claude.cmd" if IS_WINDOWS else "claude"
66
-
67
- # Security: Maximum prompt length to prevent resource exhaustion
68
- MAX_PROMPT_LENGTH = 500_000 # ~500KB
69
-
70
-
71
- def _sanitize_prompt(prompt: str) -> str:
72
- """Sanitize prompt for safe subprocess execution.
73
-
74
- - Removes null bytes and control characters (except newlines/tabs)
75
- - Truncates to maximum length
76
- - Ensures valid UTF-8
77
-
78
- Args:
79
- prompt: Raw prompt string
80
-
81
- Returns:
82
- Sanitized prompt safe for subprocess
83
- """
84
- if not prompt:
85
- return ""
86
-
87
- # Remove null bytes and control characters (keep newlines, tabs, and printable chars)
88
- # Keep: \n (10), \t (9), \r (13), and all chars >= 32 (printable ASCII + UTF-8)
89
- sanitized = "".join(char for char in prompt if char in "\n\t\r" or ord(char) >= 32)
90
-
91
- # Truncate if too long
92
- if len(sanitized) > MAX_PROMPT_LENGTH:
93
- sanitized = sanitized[:MAX_PROMPT_LENGTH] + "\n[TRUNCATED]"
94
-
95
- return sanitized
96
-
97
-
98
- class ChunkType(Enum):
99
- """Types of code chunks."""
100
-
101
- DESIGN = "design" # Architecture/design decision
102
- IMPLEMENTATION = "implementation" # Core code
103
- TEST = "test" # Test code
104
- REFACTOR = "refactor" # Refactoring
105
- FIX = "fix" # Bug fix
106
- DOCUMENTATION = "documentation" # Docs/comments
107
-
108
-
109
- class FeedbackType(Enum):
110
- """Types of reviewer feedback."""
111
-
112
- APPROVE = "approve" # Good to proceed
113
- MINOR = "minor" # Minor issues, can proceed
114
- MAJOR = "major" # Major issues, must fix
115
- BLOCKING = "blocking" # Cannot proceed until fixed
116
- QUESTION = "question" # Needs clarification
117
-
118
-
119
- @dataclass
120
- class CodeChunk:
121
- """A chunk of code being developed."""
122
-
123
- chunk_id: str
124
- chunk_type: ChunkType
125
- description: str
126
- content: str
127
- file_path: Optional[str] = None
128
- line_range: Optional[tuple[int, int]] = None
129
- timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
130
-
131
- def to_dict(self) -> dict:
132
- return {
133
- "chunk_id": self.chunk_id,
134
- "chunk_type": self.chunk_type.value,
135
- "description": self.description,
136
- "content": self.content[:500] + "..." if len(self.content) > 500 else self.content,
137
- "file_path": self.file_path,
138
- "line_range": self.line_range,
139
- "timestamp": self.timestamp,
140
- }
141
-
142
-
143
- @dataclass
144
- class ReviewFeedback:
145
- """Feedback from reviewer on a chunk."""
146
-
147
- chunk_id: str
148
- feedback_type: FeedbackType
149
- comments: list[str]
150
- suggestions: list[str] = field(default_factory=list)
151
- must_fix: list[str] = field(default_factory=list)
152
- nice_to_have: list[str] = field(default_factory=list)
153
- approved: bool = False
154
- timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
155
-
156
- def to_dict(self) -> dict:
157
- return {
158
- "chunk_id": self.chunk_id,
159
- "feedback_type": self.feedback_type.value,
160
- "comments": self.comments,
161
- "suggestions": self.suggestions,
162
- "must_fix": self.must_fix,
163
- "nice_to_have": self.nice_to_have,
164
- "approved": self.approved,
165
- "timestamp": self.timestamp,
166
- }
167
-
168
- def has_blocking_issues(self) -> bool:
169
- return self.feedback_type in [FeedbackType.MAJOR, FeedbackType.BLOCKING]
170
-
171
-
172
- @dataclass
173
- class PairExchange:
174
- """A single exchange in the pair programming session."""
175
-
176
- exchange_id: int
177
- chunk: CodeChunk
178
- feedback: Optional[ReviewFeedback] = None
179
- revision: Optional[CodeChunk] = None
180
- resolved: bool = False
181
-
182
- def to_dict(self) -> dict:
183
- return {
184
- "exchange_id": self.exchange_id,
185
- "chunk": self.chunk.to_dict(),
186
- "feedback": self.feedback.to_dict() if self.feedback else None,
187
- "revision": self.revision.to_dict() if self.revision else None,
188
- "resolved": self.resolved,
189
- }
190
-
191
-
192
- @dataclass
193
- class PairSessionResult:
194
- """Result of a pair programming session."""
195
-
196
- story_key: str
197
- task: str
198
- exchanges: list[PairExchange]
199
- final_code: str
200
- files_created: list[str]
201
- files_modified: list[str]
202
- total_chunks: int
203
- total_revisions: int
204
- approval_rate: float
205
- start_time: str
206
- end_time: str
207
-
208
- def to_dict(self) -> dict:
209
- return {
210
- "story_key": self.story_key,
211
- "task": self.task,
212
- "exchanges": [e.to_dict() for e in self.exchanges],
213
- "final_code": self.final_code[:1000]
214
- if len(self.final_code) > 1000
215
- else self.final_code,
216
- "files_created": self.files_created,
217
- "files_modified": self.files_modified,
218
- "total_chunks": self.total_chunks,
219
- "total_revisions": self.total_revisions,
220
- "approval_rate": self.approval_rate,
221
- "start_time": self.start_time,
222
- "end_time": self.end_time,
223
- }
224
-
225
- def to_summary(self) -> str:
226
- """Generate human-readable summary."""
227
- lines = [
228
- f"## Pair Programming Result: {self.story_key}",
229
- "",
230
- f"**Task**: {self.task}",
231
- f"**Exchanges**: {len(self.exchanges)}",
232
- f"**Chunks**: {self.total_chunks}",
233
- f"**Revisions**: {self.total_revisions}",
234
- f"**Approval Rate**: {self.approval_rate:.0%}",
235
- "",
236
- "### Files Created",
237
- ]
238
-
239
- for f in self.files_created:
240
- lines.append(f"- `{f}`")
241
-
242
- if self.files_modified:
243
- lines.append("")
244
- lines.append("### Files Modified")
245
- for f in self.files_modified:
246
- lines.append(f"- `{f}`")
247
-
248
- return "\n".join(lines)
249
-
250
-
251
- @dataclass
252
- class PairConfig:
253
- """Configuration for pair programming session."""
254
-
255
- max_revisions_per_chunk: int = 3
256
- timeout_seconds: int = 180
257
- verbose: bool = True
258
- auto_apply_fixes: bool = True
259
- chunk_size_hint: str = "medium" # small, medium, large
260
- reviewer_model: str = "opus"
261
- dev_model: str = "opus"
262
- validation_enabled: bool = True # Enable validation between revisions
263
-
264
- def to_dict(self) -> dict:
265
- return {
266
- "max_revisions_per_chunk": self.max_revisions_per_chunk,
267
- "timeout_seconds": self.timeout_seconds,
268
- "auto_apply_fixes": self.auto_apply_fixes,
269
- "validation_enabled": self.validation_enabled,
270
- "chunk_size_hint": self.chunk_size_hint,
271
- "reviewer_model": self.reviewer_model,
272
- "dev_model": self.dev_model,
273
- }
274
-
275
-
276
- class PairSession:
277
- """A pair programming session between DEV and REVIEWER."""
278
-
279
- def __init__(self, story_key: str, task: str, config: Optional[PairConfig] = None):
280
- self.story_key = story_key
281
- self.task = task
282
- self.config = config or PairConfig()
283
- self.project_root = PROJECT_ROOT
284
- self.shared_memory = get_shared_memory(story_key)
285
- self.knowledge_graph = get_knowledge_graph(story_key)
286
-
287
- self.exchanges: list[PairExchange] = []
288
- self.files_created: list[str] = []
289
- self.files_modified: list[str] = []
290
- self.chunk_counter = 0
291
- self.exchange_counter = 0
292
-
293
- # Initialize validation loop if available and enabled
294
- self.validation_loop = None
295
- self.validation_context = None
296
- if HAS_VALIDATION and self.config.validation_enabled:
297
- self.validation_loop = ValidationLoop(
298
- gates=INTER_PHASE_GATES,
299
- config={"auto_fix_enabled": self.config.auto_apply_fixes},
300
- story_key=story_key,
301
- )
302
- self.validation_context = LoopContext(
303
- story_key=story_key,
304
- max_iterations=self.config.max_revisions_per_chunk,
305
- )
306
-
307
- def _log(self, message: str, agent: str = "SYSTEM"):
308
- """Log a message."""
309
- if self.config.verbose:
310
- timestamp = datetime.now().strftime("%H:%M:%S")
311
- emoji = {"DEV": "", "REVIEWER": "", "SYSTEM": ""}.get(agent, "•")
312
- print(f"[{timestamp}] {emoji} [{agent}] {message}")
313
-
314
- def _run_revision_validation(self, chunk_id: str, revision_num: int) -> bool:
315
- """Run validation between DEV revisions.
316
-
317
- Args:
318
- chunk_id: ID of the current chunk
319
- revision_num: Current revision number
320
-
321
- Returns:
322
- True if validation passed
323
- """
324
- if not self.validation_loop or not self.validation_context:
325
- return True
326
-
327
- self.validation_context.iteration = revision_num
328
- self.validation_context.phase = f"chunk_{chunk_id}_revision_{revision_num}"
329
-
330
- report = self.validation_loop.run_gates(self.validation_context, tier=2)
331
-
332
- if report.passed:
333
- self._log(f"[VALIDATION] Revision {revision_num} passed validation")
334
- return True
335
- else:
336
- for failure in report.failures:
337
- self._log(f"[VALIDATION] {failure.gate_name}: {failure.message}", "SYSTEM")
338
- return True # Don't block, just inform
339
-
340
- def _invoke_agent(self, agent: str, prompt: str) -> str:
341
- """Invoke an agent with Claude CLI."""
342
- model = self.config.dev_model if agent == "DEV" else self.config.reviewer_model
343
- sanitized_prompt = _sanitize_prompt(prompt)
344
-
345
- try:
346
- result = subprocess.run(
347
- [CLAUDE_CLI, "--print", "--model", model, "-p", sanitized_prompt],
348
- capture_output=True,
349
- text=True,
350
- timeout=self.config.timeout_seconds,
351
- cwd=str(self.project_root),
352
- )
353
- return result.stdout + result.stderr
354
- except subprocess.TimeoutExpired:
355
- return "[TIMEOUT: Agent did not respond in time]"
356
- except Exception as e:
357
- return f"[ERROR: {str(e)}]"
358
-
359
- def _generate_chunk_id(self) -> str:
360
- """Generate unique chunk ID."""
361
- self.chunk_counter += 1
362
- return f"chunk_{self.chunk_counter:03d}"
363
-
364
- def _parse_dev_output(self, output: str) -> CodeChunk:
365
- """Parse DEV output into a CodeChunk."""
366
- # Try to extract file path
367
- file_match = re.search(r'(?:file|path):\s*[`"]?([^\s`"]+)[`"]?', output, re.IGNORECASE)
368
- file_path = file_match.group(1) if file_match else None
369
-
370
- # Try to extract code blocks
371
- code_blocks = re.findall(r"```[\w]*\n(.*?)```", output, re.DOTALL)
372
- content = "\n\n".join(code_blocks) if code_blocks else output
373
-
374
- # Determine chunk type
375
- chunk_type = ChunkType.IMPLEMENTATION
376
- output_lower = output.lower()
377
- if "test" in output_lower:
378
- chunk_type = ChunkType.TEST
379
- elif "refactor" in output_lower:
380
- chunk_type = ChunkType.REFACTOR
381
- elif "design" in output_lower or "architecture" in output_lower:
382
- chunk_type = ChunkType.DESIGN
383
- elif "fix" in output_lower or "bug" in output_lower:
384
- chunk_type = ChunkType.FIX
385
-
386
- # Extract description
387
- desc_match = re.search(r"^#+\s*(.+)$", output, re.MULTILINE)
388
- description = desc_match.group(1) if desc_match else "Code implementation"
389
-
390
- return CodeChunk(
391
- chunk_id=self._generate_chunk_id(),
392
- chunk_type=chunk_type,
393
- description=description[:100],
394
- content=content,
395
- file_path=file_path,
396
- )
397
-
398
- def _parse_reviewer_output(self, output: str, chunk_id: str) -> ReviewFeedback:
399
- """Parse REVIEWER output into ReviewFeedback."""
400
- output_lower = output.lower()
401
-
402
- # Determine feedback type
403
- if any(word in output_lower for word in ["blocking", "cannot proceed", "critical"]):
404
- feedback_type = FeedbackType.BLOCKING
405
- elif any(word in output_lower for word in ["major", "significant", "must fix"]):
406
- feedback_type = FeedbackType.MAJOR
407
- elif any(word in output_lower for word in ["minor", "small", "nitpick"]):
408
- feedback_type = FeedbackType.MINOR
409
- elif any(word in output_lower for word in ["question", "clarify", "unclear"]):
410
- feedback_type = FeedbackType.QUESTION
411
- else:
412
- feedback_type = FeedbackType.APPROVE
413
-
414
- # Extract comments
415
- comments = []
416
- comment_patterns = [
417
- r"[-•]\s*(.+)",
418
- r"\d+\.\s*(.+)",
419
- ]
420
- for pattern in comment_patterns:
421
- matches = re.findall(pattern, output)
422
- comments.extend(matches[:10])
423
-
424
- # Extract must-fix issues
425
- must_fix = []
426
- must_fix_section = re.search(
427
- r"(?:must fix|blocking|required).*?:(.*?)(?:\n\n|\Z)", output, re.IGNORECASE | re.DOTALL
428
- )
429
- if must_fix_section:
430
- issues = re.findall(r"[-•]\s*(.+)", must_fix_section.group(1))
431
- must_fix.extend(issues)
432
-
433
- # Extract suggestions
434
- suggestions = []
435
- suggestion_section = re.search(
436
- r"(?:suggest|consider|recommend).*?:(.*?)(?:\n\n|\Z)", output, re.IGNORECASE | re.DOTALL
437
- )
438
- if suggestion_section:
439
- sugs = re.findall(r"[-•]\s*(.+)", suggestion_section.group(1))
440
- suggestions.extend(sugs)
441
-
442
- # Check for approval
443
- approved = feedback_type == FeedbackType.APPROVE or any(
444
- word in output_lower for word in ["lgtm", "approved", "looks good", "ship it"]
445
- )
446
-
447
- return ReviewFeedback(
448
- chunk_id=chunk_id,
449
- feedback_type=feedback_type,
450
- comments=comments[:10],
451
- suggestions=suggestions[:5],
452
- must_fix=must_fix[:5],
453
- approved=approved,
454
- )
455
-
456
- def _build_dev_prompt(
457
- self, task_part: str, context: str, previous_feedback: Optional[ReviewFeedback] = None
458
- ) -> str:
459
- """Build prompt for DEV agent."""
460
-
461
- base_prompt = f"""You are in a PAIR PROGRAMMING session with a REVIEWER.
462
- Work in small, focused chunks. After each chunk, wait for reviewer feedback.
463
-
464
- ## Task
465
- {task_part}
466
-
467
- ## Context
468
- {context}
469
- """
470
-
471
- if previous_feedback:
472
- feedback_text = "\n".join(
473
- [
474
- "## Reviewer Feedback (address these)",
475
- "",
476
- "**Issues to Fix:**",
477
- *[f"- {issue}" for issue in previous_feedback.must_fix],
478
- "",
479
- "**Suggestions:**",
480
- *[f"- {sug}" for sug in previous_feedback.suggestions],
481
- ]
482
- )
483
- base_prompt += f"\n\n{feedback_text}\n"
484
-
485
- base_prompt += """
486
- ## Instructions
487
- 1. Implement ONE focused chunk of code
488
- 2. Show the file path and code clearly
489
- 3. Explain your approach briefly
490
- 4. Keep the chunk small enough for quick review
491
- """
492
-
493
- return base_prompt
494
-
495
- def _build_reviewer_prompt(self, chunk: CodeChunk, accumulated_code: str) -> str:
496
- """Build prompt for REVIEWER agent."""
497
-
498
- return f"""You are in a PAIR PROGRAMMING session reviewing DEV's work in real-time.
499
-
500
- ## Current Chunk to Review
501
- **Type**: {chunk.chunk_type.value}
502
- **Description**: {chunk.description}
503
- **File**: {chunk.file_path or "Not specified"}
504
-
505
- ```
506
- {chunk.content}
507
- ```
508
-
509
- ## Accumulated Code So Far
510
- ```
511
- {accumulated_code[-2000:] if len(accumulated_code) > 2000 else accumulated_code}
512
- ```
513
-
514
- ## Instructions
515
- 1. Review this chunk for:
516
- - Correctness
517
- - Code quality
518
- - Best practices
519
- - Potential bugs
520
- - Test coverage needs
521
-
522
- 2. Categorize issues as:
523
- - **BLOCKING**: Cannot proceed
524
- - **MUST FIX**: Required before merge
525
- - **SUGGESTION**: Nice to have
526
-
527
- 3. If the code is good, say "LGTM" or "Approved"
528
-
529
- 4. Be constructive and specific
530
- """
531
-
532
- def run(self) -> PairSessionResult:
533
- """Run the pair programming session."""
534
- start_time = datetime.now().isoformat()
535
-
536
- self._log(f"Starting pair session for: {self.task[:50]}...")
537
-
538
- # Break task into parts (for now, treat as single task)
539
- task_parts = [self.task]
540
-
541
- accumulated_code = ""
542
- total_revisions = 0
543
- approved_chunks = 0
544
-
545
- for i, task_part in enumerate(task_parts):
546
- self._log(f"Working on part {i + 1}/{len(task_parts)}")
547
-
548
- # Get initial context
549
- context = f"Story: {self.story_key}\n"
550
- context += self.shared_memory.to_context_string(5)
551
-
552
- # DEV creates initial chunk
553
- self._log("Creating initial implementation...", "DEV")
554
- dev_prompt = self._build_dev_prompt(task_part, context)
555
- dev_output = self._invoke_agent("DEV", dev_prompt)
556
-
557
- chunk = self._parse_dev_output(dev_output)
558
- self._log(f"Created chunk: {chunk.description}", "DEV")
559
-
560
- # Track file
561
- if chunk.file_path:
562
- if (
563
- chunk.file_path not in self.files_created
564
- and chunk.file_path not in self.files_modified
565
- ):
566
- self.files_created.append(chunk.file_path)
567
-
568
- accumulated_code += f"\n\n// {chunk.description}\n{chunk.content}"
569
-
570
- # REVIEWER reviews
571
- self._log("Reviewing chunk...", "REVIEWER")
572
- reviewer_prompt = self._build_reviewer_prompt(chunk, accumulated_code)
573
- reviewer_output = self._invoke_agent("REVIEWER", reviewer_prompt)
574
-
575
- feedback = self._parse_reviewer_output(reviewer_output, chunk.chunk_id)
576
- self._log(f"Feedback: {feedback.feedback_type.value}", "REVIEWER")
577
-
578
- exchange = PairExchange(
579
- exchange_id=self.exchange_counter,
580
- chunk=chunk,
581
- feedback=feedback,
582
- resolved=feedback.approved,
583
- )
584
- self.exchange_counter += 1
585
-
586
- # Revision loop if needed
587
- revision_count = 0
588
- while (
589
- feedback.has_blocking_issues()
590
- and revision_count < self.config.max_revisions_per_chunk
591
- ):
592
- revision_count += 1
593
- total_revisions += 1
594
- self._log(f"Revision {revision_count} needed", "SYSTEM")
595
-
596
- # DEV revises
597
- self._log("Addressing feedback...", "DEV")
598
- dev_prompt = self._build_dev_prompt(task_part, context, feedback)
599
- dev_output = self._invoke_agent("DEV", dev_prompt)
600
-
601
- revised_chunk = self._parse_dev_output(dev_output)
602
- exchange.revision = revised_chunk
603
-
604
- # Update accumulated code
605
- accumulated_code += (
606
- f"\n\n// Revision: {revised_chunk.description}\n{revised_chunk.content}"
607
- )
608
-
609
- # Run validation between revisions
610
- self._run_revision_validation(chunk.chunk_id, revision_count)
611
-
612
- # REVIEWER re-reviews
613
- self._log("Re-reviewing...", "REVIEWER")
614
- reviewer_prompt = self._build_reviewer_prompt(revised_chunk, accumulated_code)
615
- reviewer_output = self._invoke_agent("REVIEWER", reviewer_prompt)
616
-
617
- feedback = self._parse_reviewer_output(reviewer_output, revised_chunk.chunk_id)
618
- exchange.feedback = feedback
619
- exchange.resolved = feedback.approved
620
-
621
- self._log(f"Feedback: {feedback.feedback_type.value}", "REVIEWER")
622
-
623
- if exchange.resolved or feedback.approved:
624
- approved_chunks += 1
625
- self._log(" Chunk approved!", "SYSTEM")
626
- else:
627
- self._log(" Moving on with unresolved issues", "SYSTEM")
628
-
629
- self.exchanges.append(exchange)
630
-
631
- # Record in shared memory
632
- self.shared_memory.add(
633
- agent="PAIR",
634
- content=f"Completed chunk: {chunk.description} ({feedback.feedback_type.value})",
635
- tags=["pair-programming", "chunk"],
636
- )
637
-
638
- # Calculate approval rate
639
- total_chunks = len(self.exchanges)
640
- approval_rate = approved_chunks / total_chunks if total_chunks > 0 else 0
641
-
642
- self._log(f"Session complete. Approval rate: {approval_rate:.0%}", "SYSTEM")
643
-
644
- return PairSessionResult(
645
- story_key=self.story_key,
646
- task=self.task,
647
- exchanges=self.exchanges,
648
- final_code=accumulated_code,
649
- files_created=self.files_created,
650
- files_modified=self.files_modified,
651
- total_chunks=total_chunks,
652
- total_revisions=total_revisions,
653
- approval_rate=approval_rate,
654
- start_time=start_time,
655
- end_time=datetime.now().isoformat(),
656
- )
657
-
658
-
659
- # Convenience functions
660
- def start_pair_session(story_key: str, task: str, **config_kwargs) -> PairSession:
661
- """Start a new pair programming session."""
662
- config = PairConfig(**config_kwargs)
663
- return PairSession(story_key, task, config)
664
-
665
-
666
- def run_pair_session(story_key: str, task: str, **config_kwargs) -> PairSessionResult:
667
- """Run a complete pair programming session."""
668
- session = start_pair_session(story_key, task, **config_kwargs)
669
- return session.run()
670
-
671
-
672
- if __name__ == "__main__":
673
- print("=== Pair Programming Mode Demo ===\n")
674
- print("This module enables real-time DEV + REVIEWER collaboration.")
675
- print("\nExample usage:")
676
- print("""
677
- from lib.pair_programming import run_pair_session
678
-
679
- result = run_pair_session(
680
- story_key="3-5",
681
- task="Implement user authentication endpoint",
682
- max_revisions_per_chunk=2,
683
- verbose=True
684
- )
685
-
686
- print(result.to_summary())
687
- print(f"Approval rate: {result.approval_rate:.0%}")
688
- """)