tapps-agents 3.5.38__py3-none-any.whl → 3.5.39__py3-none-any.whl

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.
@@ -1,723 +1,723 @@
1
- """
2
- Fix Orchestrator - Coordinates bug fixing workflow with review loopback and auto-commit.
3
-
4
- Coordinates: Debugger → Implementer → Tester → Reviewer (with loopback) → Security Scan → Git Commit
5
- """
6
-
7
- import logging
8
- import time
9
- from datetime import UTC, datetime
10
- from pathlib import Path
11
- from typing import Any
12
-
13
- from tapps_agents.core.config import ProjectConfig, load_config
14
- from tapps_agents.core.git_operations import (
15
- commit_changes,
16
- create_and_checkout_branch,
17
- create_pull_request,
18
- get_current_branch,
19
- push_changes,
20
- )
21
- from tapps_agents.core.multi_agent_orchestrator import MultiAgentOrchestrator
22
- from tapps_agents.quality.quality_gates import QualityGate, QualityThresholds
23
- from ..intent_parser import Intent
24
- from .base import SimpleModeOrchestrator
25
-
26
- logger = logging.getLogger(__name__)
27
-
28
-
29
- def _write_fix_handoff(
30
- project_root: Path,
31
- workflow_id: str,
32
- success: bool,
33
- iterations: int = 0,
34
- error: str | None = None,
35
- target_file: str | None = None,
36
- ) -> None:
37
- """Write session handoff for fix workflow (plan 2.1)."""
38
- try:
39
- from tapps_agents.workflow.session_handoff import SessionHandoff, write_handoff
40
-
41
- status = "completed" if success else "failed"
42
- summary = f"Fix workflow {status}. Iterations: {iterations}."
43
- if error:
44
- summary += f" Error: {error[:100]}."
45
- if target_file:
46
- summary += f" Target: {target_file}."
47
- handoff = SessionHandoff(
48
- workflow_id=workflow_id,
49
- session_ended_at=datetime.now(UTC).isoformat(),
50
- summary=summary,
51
- done=[f"iteration {i + 1}" for i in range(iterations)] if iterations else ["debugger"],
52
- decisions=[],
53
- next_steps=["Resume with tapps-agents workflow resume", "Run `bd ready`"],
54
- artifact_paths=[],
55
- bd_ready_hint="Run `bd ready`",
56
- )
57
- write_handoff(project_root, handoff)
58
- except Exception as e: # pylint: disable=broad-except
59
- logger.debug("Could not write session handoff: %s", e)
60
-
61
-
62
- class FixOrchestrator(SimpleModeOrchestrator):
63
- """Orchestrator for fixing bugs and errors with review loopback and auto-commit."""
64
-
65
- def get_agent_sequence(self) -> list[str]:
66
- """Get the sequence of agents for fix workflow."""
67
- return ["debugger", "implementer", "tester", "reviewer"]
68
-
69
- async def execute(
70
- self, intent: Intent, parameters: dict[str, Any] | None = None
71
- ) -> dict[str, Any]:
72
- """
73
- Execute fix workflow with review loopback and auto-commit.
74
-
75
- Args:
76
- intent: Parsed user intent
77
- parameters: Additional parameters from user input:
78
- - files: List of file paths
79
- - error_message: Error description
80
- - max_iterations: Maximum iterations for loopback (default: 3)
81
- - auto_commit: Whether to commit on success (default: True)
82
- - commit_message: Optional commit message (auto-generated if not provided)
83
- - quality_thresholds: Optional quality thresholds dict
84
-
85
- Returns:
86
- Dictionary with execution results including commit info
87
- """
88
- parameters = parameters or {}
89
- files = parameters.get("files", [])
90
- error_message = parameters.get("error_message", "")
91
- workflow_id = f"fix-{int(time.time() * 1000)}"
92
-
93
- # Load configuration
94
- config = self.config or load_config()
95
- bug_fix_config = config.bug_fix_agent
96
-
97
- # Beads required: fail early if beads.required and bd unavailable
98
- try:
99
- from ..beads import require_beads
100
-
101
- require_beads(config, self.project_root)
102
- except Exception as e:
103
- from tapps_agents.core.feedback import get_feedback
104
-
105
- get_feedback().error(str(e), context={"beads_required": True})
106
- return {
107
- "type": "fix",
108
- "success": False,
109
- "error": str(e),
110
- "workflow_id": workflow_id,
111
- }
112
-
113
- max_iterations = parameters.get("max_iterations", bug_fix_config.max_iterations)
114
- auto_commit = parameters.get("auto_commit", bug_fix_config.auto_commit)
115
- commit_message = parameters.get("commit_message")
116
- escalation_threshold = bug_fix_config.escalation_threshold
117
- escalation_enabled = bug_fix_config.escalation_enabled
118
- pre_commit_security_scan = bug_fix_config.pre_commit_security_scan
119
- metrics_enabled = bug_fix_config.metrics_enabled
120
- commit_strategy = bug_fix_config.commit_strategy
121
- auto_merge_pr = bug_fix_config.auto_merge_pr
122
- require_pr_review = bug_fix_config.require_pr_review
123
-
124
- # Get quality thresholds from config or parameters
125
- thresholds_dict = parameters.get("quality_thresholds", {})
126
- if thresholds_dict:
127
- thresholds = QualityThresholds.from_dict(thresholds_dict)
128
- else:
129
- # Use thresholds from config
130
- quality_thresholds = bug_fix_config.quality_thresholds
131
- thresholds = QualityThresholds(
132
- overall_min=quality_thresholds.get("overall_min", 7.0),
133
- security_min=quality_thresholds.get("security_min", 6.5),
134
- maintainability_min=quality_thresholds.get("maintainability_min", 7.0),
135
- )
136
-
137
- # Initialize metrics collection
138
- start_time = time.time()
139
- metrics = {
140
- "bug_description": error_message or intent.original_input,
141
- "target_file": files[0] if files else None,
142
- "start_time": start_time,
143
- "iterations": 0,
144
- "success": False,
145
- }
146
-
147
- # Create multi-agent orchestrator
148
- orchestrator = MultiAgentOrchestrator(
149
- project_root=self.project_root,
150
- config=self.config,
151
- max_parallel=1, # Sequential for fix workflow
152
- )
153
-
154
- # Prepare agent tasks
155
- target_file = files[0] if files else None
156
- bug_description = error_message or intent.original_input
157
-
158
- from ..beads_hooks import create_fix_issue, close_issue
159
-
160
- beads_issue_id: str | None = None
161
- if self.config:
162
- beads_issue_id = create_fix_issue(
163
- self.project_root,
164
- self.config,
165
- str(target_file) if target_file else "",
166
- bug_description,
167
- )
168
-
169
- # Step 1: Execute debugger
170
- debug_tasks = [
171
- {
172
- "agent_id": "debugger-1",
173
- "agent": "debugger",
174
- "command": "debug",
175
- "args": {
176
- "error_message": bug_description,
177
- "file": target_file,
178
- },
179
- },
180
- ]
181
-
182
- logger.info(f"Step 1/4+: Analyzing bug: {bug_description}")
183
- # #region agent log
184
- import json
185
- from datetime import datetime
186
- log_path = self.project_root / ".cursor" / "debug.log"
187
- try:
188
- with open(log_path, "a", encoding="utf-8") as f:
189
- f.write(json.dumps({
190
- "sessionId": "debug-session",
191
- "runId": "run1",
192
- "hypothesisId": "E",
193
- "location": "fix_orchestrator.py:execute:before_debugger",
194
- "message": "About to execute debugger",
195
- "data": {"bug_description": bug_description[:200], "target_file": target_file},
196
- "timestamp": int(datetime.now().timestamp() * 1000)
197
- }) + "\n")
198
- except Exception:
199
- pass
200
- # #endregion
201
- debug_result = await orchestrator.execute_parallel(debug_tasks)
202
- # #region agent log
203
- try:
204
- with open(log_path, "a", encoding="utf-8") as f:
205
- f.write(json.dumps({
206
- "sessionId": "debug-session",
207
- "runId": "run1",
208
- "hypothesisId": "E",
209
- "location": "fix_orchestrator.py:execute:after_debugger",
210
- "message": "debugger execute_parallel returned",
211
- "data": {"has_results": "results" in debug_result, "result_keys": list(debug_result.keys())},
212
- "timestamp": int(datetime.now().timestamp() * 1000)
213
- }) + "\n")
214
- except Exception:
215
- pass
216
- # #endregion
217
-
218
- # Check if debugger succeeded
219
- debugger_result = debug_result.get("results", {}).get("debugger-1", {})
220
- # #region agent log
221
- try:
222
- with open(log_path, "a", encoding="utf-8") as f:
223
- f.write(json.dumps({
224
- "sessionId": "debug-session",
225
- "runId": "run1",
226
- "hypothesisId": "F",
227
- "location": "fix_orchestrator.py:execute:debugger_result_check",
228
- "message": "debugger_result structure",
229
- "data": {"success": debugger_result.get("success"), "result_keys": list(debugger_result.keys()), "has_result_key": "result" in debugger_result},
230
- "timestamp": int(datetime.now().timestamp() * 1000)
231
- }) + "\n")
232
- except Exception:
233
- pass
234
- # #endregion
235
- if not debugger_result.get("success"):
236
- # #region agent log
237
- try:
238
- with open(log_path, "a", encoding="utf-8") as f:
239
- f.write(json.dumps({
240
- "sessionId": "debug-session",
241
- "runId": "run1",
242
- "hypothesisId": "F",
243
- "location": "fix_orchestrator.py:execute:debugger_failed",
244
- "message": "Debugger failed",
245
- "data": {"debugger_result": str(debugger_result)[:500]},
246
- "timestamp": int(datetime.now().timestamp() * 1000)
247
- }) + "\n")
248
- except Exception:
249
- pass
250
- # #endregion
251
- close_issue(self.project_root, beads_issue_id)
252
- _write_fix_handoff(
253
- self.project_root, workflow_id,
254
- success=False, iterations=0,
255
- error="Debugger failed to analyze the bug",
256
- target_file=files[0] if files else None,
257
- )
258
- return {
259
- "type": "fix",
260
- "success": False,
261
- "error": "Debugger failed to analyze the bug",
262
- "debugger_result": debugger_result,
263
- "iterations": 0,
264
- "committed": False,
265
- }
266
-
267
- # Extract fix suggestion from debugger analysis
268
- # debug_command returns: {"type": "debug", "analysis": {...}, "suggestions": [...], "fix_examples": [...]}
269
- debugger_analysis = debugger_result.get("result", {}).get("analysis", {})
270
- suggestions = debugger_result.get("result", {}).get("suggestions", [])
271
- fix_examples = debugger_result.get("result", {}).get("fix_examples", [])
272
-
273
- # Build fix suggestion from suggestions and examples
274
- fix_suggestion_parts = []
275
- if suggestions:
276
- fix_suggestion_parts.extend(suggestions[:3]) # Take first 3 suggestions
277
- if fix_examples:
278
- fix_suggestion_parts.append("\n".join(fix_examples[:2])) # Take first 2 examples
279
- fix_suggestion = "\n\n".join(fix_suggestion_parts) if fix_suggestion_parts else ""
280
-
281
- # #region agent log
282
- try:
283
- result_inner = debugger_result.get("result", {})
284
- with open(log_path, "a", encoding="utf-8") as f:
285
- f.write(json.dumps({
286
- "sessionId": "debug-session",
287
- "runId": "run1",
288
- "hypothesisId": "G",
289
- "location": "fix_orchestrator.py:execute:fix_suggestion_check",
290
- "message": "Checking fix_suggestion",
291
- "data": {"has_result_key": "result" in debugger_result, "result_keys": list(result_inner.keys()) if isinstance(result_inner, dict) else "not_dict", "has_analysis": "analysis" in result_inner, "has_suggestions": "suggestions" in result_inner, "suggestions_count": len(suggestions), "fix_examples_count": len(fix_examples), "fix_suggestion_length": len(fix_suggestion)},
292
- "timestamp": int(datetime.now().timestamp() * 1000)
293
- }) + "\n")
294
- except Exception:
295
- pass
296
- # #endregion
297
- if not fix_suggestion:
298
- close_issue(self.project_root, beads_issue_id)
299
- _write_fix_handoff(
300
- self.project_root, workflow_id,
301
- success=False, iterations=0,
302
- error="Debugger did not provide a fix suggestion",
303
- target_file=files[0] if files else None,
304
- )
305
- return {
306
- "type": "fix",
307
- "success": False,
308
- "error": "Debugger did not provide a fix suggestion",
309
- "debugger_result": debugger_result,
310
- "iterations": 0,
311
- "committed": False,
312
- }
313
-
314
- # Step 2-4: Loop: Fix → Test → Review (until quality passes or max iterations)
315
- quality_passed = False
316
- iteration = 0
317
- review_results = []
318
-
319
- while iteration < max_iterations and not quality_passed:
320
- iteration += 1
321
- logger.info(f"Iteration {iteration}/{max_iterations}: Fix → Test → Review")
322
-
323
- # Step 2: Implement fix
324
- implement_tasks = [
325
- {
326
- "agent_id": f"implementer-{iteration}",
327
- "agent": "implementer",
328
- "command": "refactor",
329
- "args": {
330
- "file": target_file,
331
- "instructions": fix_suggestion,
332
- },
333
- },
334
- ]
335
-
336
- implement_result = await orchestrator.execute_parallel(implement_tasks)
337
- implementer_result = implement_result.get("results", {}).get(
338
- f"implementer-{iteration}", {}
339
- )
340
-
341
- if not implementer_result.get("success"):
342
- close_issue(self.project_root, beads_issue_id)
343
- _write_fix_handoff(
344
- self.project_root, workflow_id,
345
- success=False, iterations=iteration,
346
- error=f"Implementer failed on iteration {iteration}",
347
- target_file=target_file,
348
- )
349
- return {
350
- "type": "fix",
351
- "success": False,
352
- "error": f"Implementer failed on iteration {iteration}",
353
- "implementer_result": implementer_result,
354
- "iterations": iteration,
355
- "committed": False,
356
- }
357
-
358
- # Step 3: Test the fix
359
- test_tasks = [
360
- {
361
- "agent_id": f"tester-{iteration}",
362
- "agent": "tester",
363
- "command": "test",
364
- "args": {"file": target_file},
365
- },
366
- ]
367
-
368
- test_result = await orchestrator.execute_parallel(test_tasks)
369
- tester_result = test_result.get("results", {}).get(
370
- f"tester-{iteration}", {}
371
- )
372
-
373
- if not tester_result.get("success"):
374
- logger.warning(
375
- f"Tester reported issues on iteration {iteration}, continuing to review"
376
- )
377
-
378
- # Step 4: Review quality
379
- review_tasks = [
380
- {
381
- "agent_id": f"reviewer-{iteration}",
382
- "agent": "reviewer",
383
- "command": "review",
384
- "args": {"file": target_file},
385
- },
386
- ]
387
-
388
- review_result_exec = await orchestrator.execute_parallel(review_tasks)
389
- reviewer_result = review_result_exec.get("results", {}).get(
390
- f"reviewer-{iteration}", {}
391
- )
392
-
393
- if not reviewer_result.get("success"):
394
- logger.warning(
395
- f"Reviewer failed on iteration {iteration}, assuming quality passed"
396
- )
397
- quality_passed = True
398
- review_results.append({
399
- "iteration": iteration,
400
- "result": reviewer_result,
401
- "quality_passed": True,
402
- })
403
- break
404
-
405
- # Extract review result
406
- review_result_data = reviewer_result.get("result", {})
407
- review_results.append({
408
- "iteration": iteration,
409
- "result": review_result_data,
410
- })
411
-
412
- # Evaluate quality gate
413
- quality_gate = QualityGate(thresholds=thresholds)
414
- gate_result = quality_gate.evaluate_from_review_result(
415
- review_result_data, thresholds
416
- )
417
-
418
- quality_passed = gate_result.passed
419
-
420
- logger.info(
421
- f"Iteration {iteration}: Quality gate {'PASSED' if quality_passed else 'FAILED'}"
422
- )
423
- logger.info(
424
- f" Overall: {gate_result.scores.get('overall_score', 0):.2f}/10 "
425
- f"(threshold: {thresholds.overall_min})"
426
- )
427
- logger.info(
428
- f" Security: {gate_result.scores.get('security_score', 0):.2f}/10 "
429
- f"(threshold: {thresholds.security_min})"
430
- )
431
-
432
- if not quality_passed:
433
- failed_iterations = iteration
434
- # Human-in-the-Loop Escalation (2025 Enhancement)
435
- if escalation_enabled and failed_iterations >= escalation_threshold:
436
- logger.warning(
437
- f"🔔 ESCALATION: {failed_iterations} failed iterations reached escalation threshold ({escalation_threshold})"
438
- )
439
- logger.warning(
440
- f"Human intervention recommended. Bug fix agent has attempted {failed_iterations} fixes without meeting quality thresholds."
441
- )
442
- logger.warning(
443
- f"Current scores: Overall={gate_result.scores.get('overall_score', 0):.2f}/10, "
444
- f"Security={gate_result.scores.get('security_score', 0):.2f}/10, "
445
- f"Maintainability={gate_result.scores.get('maintainability_score', 0):.2f}/10"
446
- )
447
- # Continue to max_iterations but log escalation
448
-
449
- if iteration < max_iterations:
450
- # Get improvement suggestions from review
451
- improvements = review_result_data.get("improvements", [])
452
- if improvements:
453
- # Combine with previous fix suggestion
454
- fix_suggestion = (
455
- f"{fix_suggestion}\n\nAdditional improvements needed:\n"
456
- + "\n".join(f"- {imp}" for imp in improvements[:5])
457
- )
458
- logger.info(
459
- f"Quality threshold not met, attempting improvement (iteration {iteration + 1})"
460
- )
461
- else:
462
- logger.error(
463
- f"Maximum iterations ({max_iterations}) reached, quality threshold not met"
464
- )
465
- # Record escalation in metrics
466
- if metrics_enabled and failed_iterations >= escalation_threshold:
467
- metrics["escalated"] = True
468
- metrics["escalation_iteration"] = escalation_threshold
469
-
470
- # Check if quality passed
471
- if not quality_passed:
472
- execution_time = time.time() - start_time
473
- if metrics_enabled:
474
- metrics.update({
475
- "iterations": iteration,
476
- "success": False,
477
- "execution_time": execution_time,
478
- "final_quality_scores": review_results[-1]["result"].get("scores", {}) if review_results else {},
479
- })
480
- logger.info(f"Metrics: {metrics}")
481
-
482
- close_issue(self.project_root, beads_issue_id)
483
- return {
484
- "type": "fix",
485
- "success": False,
486
- "error": f"Quality threshold not met after {max_iterations} iterations",
487
- "review_results": review_results,
488
- "iterations": iteration,
489
- "committed": False,
490
- "escalated": escalation_enabled and iteration >= escalation_threshold,
491
- "metrics": metrics if metrics_enabled else None,
492
- }
493
-
494
- # Step 5: Pre-Commit Security Scan (2025 Enhancement)
495
- security_scan_passed = True
496
- security_scan_result = None
497
- if pre_commit_security_scan and auto_commit:
498
- logger.info("Running pre-commit security scan...")
499
- try:
500
- security_scan_tasks = [
501
- {
502
- "agent_id": "ops-security-1",
503
- "agent": "ops",
504
- "command": "security-scan",
505
- "args": {
506
- "target": target_file,
507
- "scan_type": "code",
508
- },
509
- },
510
- ]
511
- security_result = await orchestrator.execute_parallel(security_scan_tasks)
512
- security_scan_result = security_result.get("results", {}).get("ops-security-1", {})
513
-
514
- if security_scan_result.get("success"):
515
- security_score = security_scan_result.get("result", {}).get("security_score", 10.0)
516
- vulnerabilities = security_scan_result.get("result", {}).get("vulnerabilities", [])
517
- critical_vulns = [v for v in vulnerabilities if v.get("severity") in ["CRITICAL", "HIGH"]]
518
-
519
- if critical_vulns:
520
- logger.error(f"Pre-commit security scan FAILED: {len(critical_vulns)} CRITICAL/HIGH vulnerabilities found")
521
- security_scan_passed = False
522
- logger.error("Blocking commit due to security vulnerabilities")
523
- elif security_score < thresholds.security_min:
524
- logger.warning(f"Pre-commit security scan: score {security_score:.2f} below threshold {thresholds.security_min}")
525
- # Warn but don't block for non-critical issues
526
- else:
527
- logger.info(f"Pre-commit security scan PASSED: score {security_score:.2f}/10")
528
- else:
529
- logger.warning("Security scan failed, but continuing with commit")
530
- except Exception as e:
531
- logger.warning(f"Error during security scan: {e}, continuing with commit")
532
-
533
- if not security_scan_passed:
534
- execution_time = time.time() - start_time
535
- if metrics_enabled:
536
- metrics.update({
537
- "iterations": iteration,
538
- "success": False,
539
- "execution_time": execution_time,
540
- "security_scan_blocked": True,
541
- })
542
- close_issue(self.project_root, beads_issue_id)
543
- _write_fix_handoff(
544
- self.project_root, workflow_id,
545
- success=False, iterations=iteration,
546
- error="Pre-commit security scan failed: CRITICAL/HIGH vulnerabilities detected",
547
- target_file=target_file,
548
- )
549
- return {
550
- "type": "fix",
551
- "success": False,
552
- "error": "Pre-commit security scan failed: CRITICAL/HIGH vulnerabilities detected",
553
- "review_results": review_results,
554
- "iterations": iteration,
555
- "committed": False,
556
- "security_scan_result": security_scan_result,
557
- "metrics": metrics if metrics_enabled else None,
558
- }
559
-
560
- # Step 6: Commit changes (direct or PR workflow)
561
- commit_info = None
562
- pr_info = None
563
- if auto_commit:
564
- try:
565
- # Generate commit message if not provided
566
- if not commit_message:
567
- final_scores = review_results[-1]["result"].get("scores", {})
568
- overall_score = final_scores.get("overall_score", 0)
569
- commit_message = (
570
- f"Fix: {bug_description}\n\n"
571
- f"Quality scores: Overall {overall_score:.1f}/10\n"
572
- f"Iterations: {iteration}\n"
573
- f"Auto-fixed by TappsCodingAgents Bug Fix Agent"
574
- )
575
-
576
- # Branch Protection & PR Workflow (2025 Enhancement)
577
- if commit_strategy == "pull_request":
578
- # Create feature branch for PR
579
- timestamp = datetime.now(UTC).strftime("%Y%m%d%H%M%S")
580
- file_stem = Path(target_file).stem if target_file else "fix"
581
- feature_branch = f"bugfix/{file_stem}-{timestamp}"
582
-
583
- logger.info(f"Creating feature branch: {feature_branch}")
584
- branch_result = create_and_checkout_branch(feature_branch, self.project_root)
585
-
586
- if not branch_result["success"]:
587
- raise RuntimeError(f"Failed to create branch: {branch_result.get('error')}")
588
-
589
- # Commit to feature branch
590
- commit_result = commit_changes(
591
- message=commit_message,
592
- files=[target_file] if target_file else None,
593
- branch=feature_branch,
594
- path=self.project_root,
595
- )
596
-
597
- if commit_result["success"]:
598
- # Push feature branch
599
- push_result = push_changes(feature_branch, self.project_root)
600
-
601
- if push_result["success"]:
602
- # Create PR
603
- pr_title = f"Fix: {bug_description[:100]}"
604
- pr_body = (
605
- f"## Automated Bug Fix\n\n"
606
- f"**Bug Description:** {bug_description}\n\n"
607
- f"**Target File:** {target_file}\n\n"
608
- f"**Quality Scores:**\n"
609
- f"- Overall: {final_scores.get('overall_score', 0):.1f}/10\n"
610
- f"- Security: {final_scores.get('security_score', 0):.1f}/10\n"
611
- f"- Maintainability: {final_scores.get('maintainability_score', 0):.1f}/10\n\n"
612
- f"**Iterations:** {iteration}\n\n"
613
- f"**Auto-fixed by:** TappsCodingAgents Bug Fix Agent\n\n"
614
- f"---\n\n"
615
- f"*This PR was created automatically after passing quality gates.*"
616
- )
617
-
618
- pr_result = create_pull_request(
619
- title=pr_title,
620
- body=pr_body,
621
- head_branch=feature_branch,
622
- base_branch="main",
623
- path=self.project_root,
624
- )
625
-
626
- if pr_result["success"]:
627
- pr_info = {
628
- "pr_url": pr_result.get("pr_url"),
629
- "pr_number": pr_result.get("pr_number"),
630
- "branch": feature_branch,
631
- }
632
- logger.info(f"Created pull request: {pr_result.get('pr_url', 'N/A')}")
633
-
634
- if auto_merge_pr and not require_pr_review:
635
- logger.info("Auto-merge enabled, but manual merge required (GitHub CLI limitation)")
636
- else:
637
- logger.warning(f"Failed to create PR: {pr_result.get('error')}")
638
- # Fall back to commit info even if PR creation failed
639
- commit_info = {
640
- "commit_hash": commit_result["commit_hash"],
641
- "branch": feature_branch,
642
- "message": commit_message,
643
- "pr_error": pr_result.get("error"),
644
- }
645
- else:
646
- logger.warning(f"Failed to push branch: {push_result.get('error')}")
647
- commit_info = {"error": f"Push failed: {push_result.get('error')}"}
648
- else:
649
- logger.warning(f"Failed to commit to branch: {commit_result.get('error')}")
650
- commit_info = {"error": commit_result.get("error")}
651
-
652
- else:
653
- # Direct commit to main (original behavior)
654
- commit_result = commit_changes(
655
- message=commit_message,
656
- files=[target_file] if target_file else None,
657
- branch="main",
658
- path=self.project_root,
659
- )
660
-
661
- if commit_result["success"]:
662
- commit_info = {
663
- "commit_hash": commit_result["commit_hash"],
664
- "branch": commit_result["branch"],
665
- "message": commit_message,
666
- }
667
- logger.info(
668
- f"Committed changes to {commit_result['branch']}: "
669
- f"{commit_result['commit_hash'][:8]}"
670
- )
671
- else:
672
- logger.warning(
673
- f"Failed to commit changes: {commit_result.get('error')}"
674
- )
675
- commit_info = {
676
- "error": commit_result.get("error"),
677
- }
678
-
679
- except Exception as e:
680
- logger.error(f"Error during commit: {e}", exc_info=True)
681
- commit_info = {"error": str(e)}
682
-
683
- # Update metrics for successful execution
684
- execution_time = time.time() - start_time
685
- if metrics_enabled:
686
- final_scores = review_results[-1]["result"].get("scores", {}) if review_results else {}
687
- metrics.update({
688
- "iterations": iteration,
689
- "success": True,
690
- "execution_time": execution_time,
691
- "final_quality_scores": final_scores,
692
- "committed": commit_info is not None and "error" not in commit_info,
693
- "security_scan_performed": pre_commit_security_scan and auto_commit,
694
- })
695
- logger.info(f"Execution metrics: {metrics}")
696
-
697
- close_issue(self.project_root, beads_issue_id)
698
- _write_fix_handoff(
699
- self.project_root, workflow_id,
700
- success=True, iterations=iteration,
701
- target_file=target_file,
702
- )
703
- return {
704
- "type": "fix",
705
- "success": True,
706
- "agents_executed": ["debugger", "implementer", "tester", "reviewer"] + (["ops"] if pre_commit_security_scan and auto_commit else []),
707
- "iterations": iteration,
708
- "review_results": review_results,
709
- "quality_passed": True,
710
- "committed": commit_info is not None and "error" not in commit_info,
711
- "commit_info": commit_info,
712
- "pr_info": pr_info,
713
- "commit_strategy": commit_strategy,
714
- "security_scan_result": security_scan_result if pre_commit_security_scan and auto_commit else None,
715
- "metrics": metrics if metrics_enabled else None,
716
- "summary": {
717
- "bug_description": bug_description,
718
- "target_file": target_file,
719
- "iterations": iteration,
720
- "final_quality": review_results[-1]["result"].get("scores", {}) if review_results else {},
721
- "execution_time": execution_time,
722
- },
723
- }
1
+ """
2
+ Fix Orchestrator - Coordinates bug fixing workflow with review loopback and auto-commit.
3
+
4
+ Coordinates: Debugger → Implementer → Tester → Reviewer (with loopback) → Security Scan → Git Commit
5
+ """
6
+
7
+ import logging
8
+ import time
9
+ from datetime import UTC, datetime
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from tapps_agents.core.config import ProjectConfig, load_config
14
+ from tapps_agents.core.git_operations import (
15
+ commit_changes,
16
+ create_and_checkout_branch,
17
+ create_pull_request,
18
+ get_current_branch,
19
+ push_changes,
20
+ )
21
+ from tapps_agents.core.multi_agent_orchestrator import MultiAgentOrchestrator
22
+ from tapps_agents.quality.quality_gates import QualityGate, QualityThresholds
23
+ from ..intent_parser import Intent
24
+ from .base import SimpleModeOrchestrator
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ def _write_fix_handoff(
30
+ project_root: Path,
31
+ workflow_id: str,
32
+ success: bool,
33
+ iterations: int = 0,
34
+ error: str | None = None,
35
+ target_file: str | None = None,
36
+ ) -> None:
37
+ """Write session handoff for fix workflow (plan 2.1)."""
38
+ try:
39
+ from tapps_agents.workflow.session_handoff import SessionHandoff, write_handoff
40
+
41
+ status = "completed" if success else "failed"
42
+ summary = f"Fix workflow {status}. Iterations: {iterations}."
43
+ if error:
44
+ summary += f" Error: {error[:100]}."
45
+ if target_file:
46
+ summary += f" Target: {target_file}."
47
+ handoff = SessionHandoff(
48
+ workflow_id=workflow_id,
49
+ session_ended_at=datetime.now(UTC).isoformat(),
50
+ summary=summary,
51
+ done=[f"iteration {i + 1}" for i in range(iterations)] if iterations else ["debugger"],
52
+ decisions=[],
53
+ next_steps=["Resume with tapps-agents workflow resume", "Run `bd ready`"],
54
+ artifact_paths=[],
55
+ bd_ready_hint="Run `bd ready`",
56
+ )
57
+ write_handoff(project_root, handoff)
58
+ except Exception as e: # pylint: disable=broad-except
59
+ logger.debug("Could not write session handoff: %s", e)
60
+
61
+
62
+ class FixOrchestrator(SimpleModeOrchestrator):
63
+ """Orchestrator for fixing bugs and errors with review loopback and auto-commit."""
64
+
65
+ def get_agent_sequence(self) -> list[str]:
66
+ """Get the sequence of agents for fix workflow."""
67
+ return ["debugger", "implementer", "tester", "reviewer"]
68
+
69
+ async def execute(
70
+ self, intent: Intent, parameters: dict[str, Any] | None = None
71
+ ) -> dict[str, Any]:
72
+ """
73
+ Execute fix workflow with review loopback and auto-commit.
74
+
75
+ Args:
76
+ intent: Parsed user intent
77
+ parameters: Additional parameters from user input:
78
+ - files: List of file paths
79
+ - error_message: Error description
80
+ - max_iterations: Maximum iterations for loopback (default: 3)
81
+ - auto_commit: Whether to commit on success (default: True)
82
+ - commit_message: Optional commit message (auto-generated if not provided)
83
+ - quality_thresholds: Optional quality thresholds dict
84
+
85
+ Returns:
86
+ Dictionary with execution results including commit info
87
+ """
88
+ parameters = parameters or {}
89
+ files = parameters.get("files", [])
90
+ error_message = parameters.get("error_message", "")
91
+ workflow_id = f"fix-{int(time.time() * 1000)}"
92
+
93
+ # Load configuration
94
+ config = self.config or load_config()
95
+ bug_fix_config = config.bug_fix_agent
96
+
97
+ # Beads required: fail early if beads.required and bd unavailable
98
+ try:
99
+ from ..beads import require_beads
100
+
101
+ require_beads(config, self.project_root)
102
+ except Exception as e:
103
+ from tapps_agents.core.feedback import get_feedback
104
+
105
+ get_feedback().error(str(e), context={"beads_required": True})
106
+ return {
107
+ "type": "fix",
108
+ "success": False,
109
+ "error": str(e),
110
+ "workflow_id": workflow_id,
111
+ }
112
+
113
+ max_iterations = parameters.get("max_iterations", bug_fix_config.max_iterations)
114
+ auto_commit = parameters.get("auto_commit", bug_fix_config.auto_commit)
115
+ commit_message = parameters.get("commit_message")
116
+ escalation_threshold = bug_fix_config.escalation_threshold
117
+ escalation_enabled = bug_fix_config.escalation_enabled
118
+ pre_commit_security_scan = bug_fix_config.pre_commit_security_scan
119
+ metrics_enabled = bug_fix_config.metrics_enabled
120
+ commit_strategy = bug_fix_config.commit_strategy
121
+ auto_merge_pr = bug_fix_config.auto_merge_pr
122
+ require_pr_review = bug_fix_config.require_pr_review
123
+
124
+ # Get quality thresholds from config or parameters
125
+ thresholds_dict = parameters.get("quality_thresholds", {})
126
+ if thresholds_dict:
127
+ thresholds = QualityThresholds.from_dict(thresholds_dict)
128
+ else:
129
+ # Use thresholds from config
130
+ quality_thresholds = bug_fix_config.quality_thresholds
131
+ thresholds = QualityThresholds(
132
+ overall_min=quality_thresholds.get("overall_min", 7.0),
133
+ security_min=quality_thresholds.get("security_min", 6.5),
134
+ maintainability_min=quality_thresholds.get("maintainability_min", 7.0),
135
+ )
136
+
137
+ # Initialize metrics collection
138
+ start_time = time.time()
139
+ metrics = {
140
+ "bug_description": error_message or intent.original_input,
141
+ "target_file": files[0] if files else None,
142
+ "start_time": start_time,
143
+ "iterations": 0,
144
+ "success": False,
145
+ }
146
+
147
+ # Create multi-agent orchestrator
148
+ orchestrator = MultiAgentOrchestrator(
149
+ project_root=self.project_root,
150
+ config=self.config,
151
+ max_parallel=1, # Sequential for fix workflow
152
+ )
153
+
154
+ # Prepare agent tasks
155
+ target_file = files[0] if files else None
156
+ bug_description = error_message or intent.original_input
157
+
158
+ from ..beads_hooks import create_fix_issue, close_issue
159
+
160
+ beads_issue_id: str | None = None
161
+ if self.config:
162
+ beads_issue_id = create_fix_issue(
163
+ self.project_root,
164
+ self.config,
165
+ str(target_file) if target_file else "",
166
+ bug_description,
167
+ )
168
+
169
+ # Step 1: Execute debugger
170
+ debug_tasks = [
171
+ {
172
+ "agent_id": "debugger-1",
173
+ "agent": "debugger",
174
+ "command": "debug",
175
+ "args": {
176
+ "error_message": bug_description,
177
+ "file": target_file,
178
+ },
179
+ },
180
+ ]
181
+
182
+ logger.info(f"Step 1/4+: Analyzing bug: {bug_description}")
183
+ # #region agent log
184
+ import json
185
+ from datetime import datetime
186
+ log_path = self.project_root / ".cursor" / "debug.log"
187
+ try:
188
+ with open(log_path, "a", encoding="utf-8") as f:
189
+ f.write(json.dumps({
190
+ "sessionId": "debug-session",
191
+ "runId": "run1",
192
+ "hypothesisId": "E",
193
+ "location": "fix_orchestrator.py:execute:before_debugger",
194
+ "message": "About to execute debugger",
195
+ "data": {"bug_description": bug_description[:200], "target_file": target_file},
196
+ "timestamp": int(datetime.now().timestamp() * 1000)
197
+ }) + "\n")
198
+ except Exception:
199
+ pass
200
+ # #endregion
201
+ debug_result = await orchestrator.execute_parallel(debug_tasks)
202
+ # #region agent log
203
+ try:
204
+ with open(log_path, "a", encoding="utf-8") as f:
205
+ f.write(json.dumps({
206
+ "sessionId": "debug-session",
207
+ "runId": "run1",
208
+ "hypothesisId": "E",
209
+ "location": "fix_orchestrator.py:execute:after_debugger",
210
+ "message": "debugger execute_parallel returned",
211
+ "data": {"has_results": "results" in debug_result, "result_keys": list(debug_result.keys())},
212
+ "timestamp": int(datetime.now().timestamp() * 1000)
213
+ }) + "\n")
214
+ except Exception:
215
+ pass
216
+ # #endregion
217
+
218
+ # Check if debugger succeeded
219
+ debugger_result = debug_result.get("results", {}).get("debugger-1", {})
220
+ # #region agent log
221
+ try:
222
+ with open(log_path, "a", encoding="utf-8") as f:
223
+ f.write(json.dumps({
224
+ "sessionId": "debug-session",
225
+ "runId": "run1",
226
+ "hypothesisId": "F",
227
+ "location": "fix_orchestrator.py:execute:debugger_result_check",
228
+ "message": "debugger_result structure",
229
+ "data": {"success": debugger_result.get("success"), "result_keys": list(debugger_result.keys()), "has_result_key": "result" in debugger_result},
230
+ "timestamp": int(datetime.now().timestamp() * 1000)
231
+ }) + "\n")
232
+ except Exception:
233
+ pass
234
+ # #endregion
235
+ if not debugger_result.get("success"):
236
+ # #region agent log
237
+ try:
238
+ with open(log_path, "a", encoding="utf-8") as f:
239
+ f.write(json.dumps({
240
+ "sessionId": "debug-session",
241
+ "runId": "run1",
242
+ "hypothesisId": "F",
243
+ "location": "fix_orchestrator.py:execute:debugger_failed",
244
+ "message": "Debugger failed",
245
+ "data": {"debugger_result": str(debugger_result)[:500]},
246
+ "timestamp": int(datetime.now().timestamp() * 1000)
247
+ }) + "\n")
248
+ except Exception:
249
+ pass
250
+ # #endregion
251
+ close_issue(self.project_root, beads_issue_id)
252
+ _write_fix_handoff(
253
+ self.project_root, workflow_id,
254
+ success=False, iterations=0,
255
+ error="Debugger failed to analyze the bug",
256
+ target_file=files[0] if files else None,
257
+ )
258
+ return {
259
+ "type": "fix",
260
+ "success": False,
261
+ "error": "Debugger failed to analyze the bug",
262
+ "debugger_result": debugger_result,
263
+ "iterations": 0,
264
+ "committed": False,
265
+ }
266
+
267
+ # Extract fix suggestion from debugger analysis
268
+ # debug_command returns: {"type": "debug", "analysis": {...}, "suggestions": [...], "fix_examples": [...]}
269
+ debugger_analysis = debugger_result.get("result", {}).get("analysis", {})
270
+ suggestions = debugger_result.get("result", {}).get("suggestions", [])
271
+ fix_examples = debugger_result.get("result", {}).get("fix_examples", [])
272
+
273
+ # Build fix suggestion from suggestions and examples
274
+ fix_suggestion_parts = []
275
+ if suggestions:
276
+ fix_suggestion_parts.extend(suggestions[:3]) # Take first 3 suggestions
277
+ if fix_examples:
278
+ fix_suggestion_parts.append("\n".join(fix_examples[:2])) # Take first 2 examples
279
+ fix_suggestion = "\n\n".join(fix_suggestion_parts) if fix_suggestion_parts else ""
280
+
281
+ # #region agent log
282
+ try:
283
+ result_inner = debugger_result.get("result", {})
284
+ with open(log_path, "a", encoding="utf-8") as f:
285
+ f.write(json.dumps({
286
+ "sessionId": "debug-session",
287
+ "runId": "run1",
288
+ "hypothesisId": "G",
289
+ "location": "fix_orchestrator.py:execute:fix_suggestion_check",
290
+ "message": "Checking fix_suggestion",
291
+ "data": {"has_result_key": "result" in debugger_result, "result_keys": list(result_inner.keys()) if isinstance(result_inner, dict) else "not_dict", "has_analysis": "analysis" in result_inner, "has_suggestions": "suggestions" in result_inner, "suggestions_count": len(suggestions), "fix_examples_count": len(fix_examples), "fix_suggestion_length": len(fix_suggestion)},
292
+ "timestamp": int(datetime.now().timestamp() * 1000)
293
+ }) + "\n")
294
+ except Exception:
295
+ pass
296
+ # #endregion
297
+ if not fix_suggestion:
298
+ close_issue(self.project_root, beads_issue_id)
299
+ _write_fix_handoff(
300
+ self.project_root, workflow_id,
301
+ success=False, iterations=0,
302
+ error="Debugger did not provide a fix suggestion",
303
+ target_file=files[0] if files else None,
304
+ )
305
+ return {
306
+ "type": "fix",
307
+ "success": False,
308
+ "error": "Debugger did not provide a fix suggestion",
309
+ "debugger_result": debugger_result,
310
+ "iterations": 0,
311
+ "committed": False,
312
+ }
313
+
314
+ # Step 2-4: Loop: Fix → Test → Review (until quality passes or max iterations)
315
+ quality_passed = False
316
+ iteration = 0
317
+ review_results = []
318
+
319
+ while iteration < max_iterations and not quality_passed:
320
+ iteration += 1
321
+ logger.info(f"Iteration {iteration}/{max_iterations}: Fix → Test → Review")
322
+
323
+ # Step 2: Implement fix
324
+ implement_tasks = [
325
+ {
326
+ "agent_id": f"implementer-{iteration}",
327
+ "agent": "implementer",
328
+ "command": "refactor",
329
+ "args": {
330
+ "file": target_file,
331
+ "instructions": fix_suggestion,
332
+ },
333
+ },
334
+ ]
335
+
336
+ implement_result = await orchestrator.execute_parallel(implement_tasks)
337
+ implementer_result = implement_result.get("results", {}).get(
338
+ f"implementer-{iteration}", {}
339
+ )
340
+
341
+ if not implementer_result.get("success"):
342
+ close_issue(self.project_root, beads_issue_id)
343
+ _write_fix_handoff(
344
+ self.project_root, workflow_id,
345
+ success=False, iterations=iteration,
346
+ error=f"Implementer failed on iteration {iteration}",
347
+ target_file=target_file,
348
+ )
349
+ return {
350
+ "type": "fix",
351
+ "success": False,
352
+ "error": f"Implementer failed on iteration {iteration}",
353
+ "implementer_result": implementer_result,
354
+ "iterations": iteration,
355
+ "committed": False,
356
+ }
357
+
358
+ # Step 3: Test the fix
359
+ test_tasks = [
360
+ {
361
+ "agent_id": f"tester-{iteration}",
362
+ "agent": "tester",
363
+ "command": "test",
364
+ "args": {"file": target_file},
365
+ },
366
+ ]
367
+
368
+ test_result = await orchestrator.execute_parallel(test_tasks)
369
+ tester_result = test_result.get("results", {}).get(
370
+ f"tester-{iteration}", {}
371
+ )
372
+
373
+ if not tester_result.get("success"):
374
+ logger.warning(
375
+ f"Tester reported issues on iteration {iteration}, continuing to review"
376
+ )
377
+
378
+ # Step 4: Review quality
379
+ review_tasks = [
380
+ {
381
+ "agent_id": f"reviewer-{iteration}",
382
+ "agent": "reviewer",
383
+ "command": "review",
384
+ "args": {"file": target_file},
385
+ },
386
+ ]
387
+
388
+ review_result_exec = await orchestrator.execute_parallel(review_tasks)
389
+ reviewer_result = review_result_exec.get("results", {}).get(
390
+ f"reviewer-{iteration}", {}
391
+ )
392
+
393
+ if not reviewer_result.get("success"):
394
+ logger.warning(
395
+ f"Reviewer failed on iteration {iteration}, assuming quality passed"
396
+ )
397
+ quality_passed = True
398
+ review_results.append({
399
+ "iteration": iteration,
400
+ "result": reviewer_result,
401
+ "quality_passed": True,
402
+ })
403
+ break
404
+
405
+ # Extract review result
406
+ review_result_data = reviewer_result.get("result", {})
407
+ review_results.append({
408
+ "iteration": iteration,
409
+ "result": review_result_data,
410
+ })
411
+
412
+ # Evaluate quality gate
413
+ quality_gate = QualityGate(thresholds=thresholds)
414
+ gate_result = quality_gate.evaluate_from_review_result(
415
+ review_result_data, thresholds
416
+ )
417
+
418
+ quality_passed = gate_result.passed
419
+
420
+ logger.info(
421
+ f"Iteration {iteration}: Quality gate {'PASSED' if quality_passed else 'FAILED'}"
422
+ )
423
+ logger.info(
424
+ f" Overall: {gate_result.scores.get('overall_score', 0):.2f}/10 "
425
+ f"(threshold: {thresholds.overall_min})"
426
+ )
427
+ logger.info(
428
+ f" Security: {gate_result.scores.get('security_score', 0):.2f}/10 "
429
+ f"(threshold: {thresholds.security_min})"
430
+ )
431
+
432
+ if not quality_passed:
433
+ failed_iterations = iteration
434
+ # Human-in-the-Loop Escalation (2025 Enhancement)
435
+ if escalation_enabled and failed_iterations >= escalation_threshold:
436
+ logger.warning(
437
+ f"🔔 ESCALATION: {failed_iterations} failed iterations reached escalation threshold ({escalation_threshold})"
438
+ )
439
+ logger.warning(
440
+ f"Human intervention recommended. Bug fix agent has attempted {failed_iterations} fixes without meeting quality thresholds."
441
+ )
442
+ logger.warning(
443
+ f"Current scores: Overall={gate_result.scores.get('overall_score', 0):.2f}/10, "
444
+ f"Security={gate_result.scores.get('security_score', 0):.2f}/10, "
445
+ f"Maintainability={gate_result.scores.get('maintainability_score', 0):.2f}/10"
446
+ )
447
+ # Continue to max_iterations but log escalation
448
+
449
+ if iteration < max_iterations:
450
+ # Get improvement suggestions from review
451
+ improvements = review_result_data.get("improvements", [])
452
+ if improvements:
453
+ # Combine with previous fix suggestion
454
+ fix_suggestion = (
455
+ f"{fix_suggestion}\n\nAdditional improvements needed:\n"
456
+ + "\n".join(f"- {imp}" for imp in improvements[:5])
457
+ )
458
+ logger.info(
459
+ f"Quality threshold not met, attempting improvement (iteration {iteration + 1})"
460
+ )
461
+ else:
462
+ logger.error(
463
+ f"Maximum iterations ({max_iterations}) reached, quality threshold not met"
464
+ )
465
+ # Record escalation in metrics
466
+ if metrics_enabled and failed_iterations >= escalation_threshold:
467
+ metrics["escalated"] = True
468
+ metrics["escalation_iteration"] = escalation_threshold
469
+
470
+ # Check if quality passed
471
+ if not quality_passed:
472
+ execution_time = time.time() - start_time
473
+ if metrics_enabled:
474
+ metrics.update({
475
+ "iterations": iteration,
476
+ "success": False,
477
+ "execution_time": execution_time,
478
+ "final_quality_scores": review_results[-1]["result"].get("scores", {}) if review_results else {},
479
+ })
480
+ logger.info(f"Metrics: {metrics}")
481
+
482
+ close_issue(self.project_root, beads_issue_id)
483
+ return {
484
+ "type": "fix",
485
+ "success": False,
486
+ "error": f"Quality threshold not met after {max_iterations} iterations",
487
+ "review_results": review_results,
488
+ "iterations": iteration,
489
+ "committed": False,
490
+ "escalated": escalation_enabled and iteration >= escalation_threshold,
491
+ "metrics": metrics if metrics_enabled else None,
492
+ }
493
+
494
+ # Step 5: Pre-Commit Security Scan (2025 Enhancement)
495
+ security_scan_passed = True
496
+ security_scan_result = None
497
+ if pre_commit_security_scan and auto_commit:
498
+ logger.info("Running pre-commit security scan...")
499
+ try:
500
+ security_scan_tasks = [
501
+ {
502
+ "agent_id": "ops-security-1",
503
+ "agent": "ops",
504
+ "command": "security-scan",
505
+ "args": {
506
+ "target": target_file,
507
+ "scan_type": "code",
508
+ },
509
+ },
510
+ ]
511
+ security_result = await orchestrator.execute_parallel(security_scan_tasks)
512
+ security_scan_result = security_result.get("results", {}).get("ops-security-1", {})
513
+
514
+ if security_scan_result.get("success"):
515
+ security_score = security_scan_result.get("result", {}).get("security_score", 10.0)
516
+ vulnerabilities = security_scan_result.get("result", {}).get("vulnerabilities", [])
517
+ critical_vulns = [v for v in vulnerabilities if v.get("severity") in ["CRITICAL", "HIGH"]]
518
+
519
+ if critical_vulns:
520
+ logger.error(f"Pre-commit security scan FAILED: {len(critical_vulns)} CRITICAL/HIGH vulnerabilities found")
521
+ security_scan_passed = False
522
+ logger.error("Blocking commit due to security vulnerabilities")
523
+ elif security_score < thresholds.security_min:
524
+ logger.warning(f"Pre-commit security scan: score {security_score:.2f} below threshold {thresholds.security_min}")
525
+ # Warn but don't block for non-critical issues
526
+ else:
527
+ logger.info(f"Pre-commit security scan PASSED: score {security_score:.2f}/10")
528
+ else:
529
+ logger.warning("Security scan failed, but continuing with commit")
530
+ except Exception as e:
531
+ logger.warning(f"Error during security scan: {e}, continuing with commit")
532
+
533
+ if not security_scan_passed:
534
+ execution_time = time.time() - start_time
535
+ if metrics_enabled:
536
+ metrics.update({
537
+ "iterations": iteration,
538
+ "success": False,
539
+ "execution_time": execution_time,
540
+ "security_scan_blocked": True,
541
+ })
542
+ close_issue(self.project_root, beads_issue_id)
543
+ _write_fix_handoff(
544
+ self.project_root, workflow_id,
545
+ success=False, iterations=iteration,
546
+ error="Pre-commit security scan failed: CRITICAL/HIGH vulnerabilities detected",
547
+ target_file=target_file,
548
+ )
549
+ return {
550
+ "type": "fix",
551
+ "success": False,
552
+ "error": "Pre-commit security scan failed: CRITICAL/HIGH vulnerabilities detected",
553
+ "review_results": review_results,
554
+ "iterations": iteration,
555
+ "committed": False,
556
+ "security_scan_result": security_scan_result,
557
+ "metrics": metrics if metrics_enabled else None,
558
+ }
559
+
560
+ # Step 6: Commit changes (direct or PR workflow)
561
+ commit_info = None
562
+ pr_info = None
563
+ if auto_commit:
564
+ try:
565
+ # Generate commit message if not provided
566
+ if not commit_message:
567
+ final_scores = review_results[-1]["result"].get("scores", {}) if review_results else {}
568
+ overall_score = final_scores.get("overall_score", 0)
569
+ commit_message = (
570
+ f"Fix: {bug_description}\n\n"
571
+ f"Quality scores: Overall {overall_score:.1f}/10\n"
572
+ f"Iterations: {iteration}\n"
573
+ f"Auto-fixed by TappsCodingAgents Bug Fix Agent"
574
+ )
575
+
576
+ # Branch Protection & PR Workflow (2025 Enhancement)
577
+ if commit_strategy == "pull_request":
578
+ # Create feature branch for PR
579
+ timestamp = datetime.now(UTC).strftime("%Y%m%d%H%M%S")
580
+ file_stem = Path(target_file).stem if target_file else "fix"
581
+ feature_branch = f"bugfix/{file_stem}-{timestamp}"
582
+
583
+ logger.info(f"Creating feature branch: {feature_branch}")
584
+ branch_result = create_and_checkout_branch(feature_branch, self.project_root)
585
+
586
+ if not branch_result["success"]:
587
+ raise RuntimeError(f"Failed to create branch: {branch_result.get('error')}")
588
+
589
+ # Commit to feature branch
590
+ commit_result = commit_changes(
591
+ message=commit_message,
592
+ files=[target_file] if target_file else None,
593
+ branch=feature_branch,
594
+ path=self.project_root,
595
+ )
596
+
597
+ if commit_result["success"]:
598
+ # Push feature branch
599
+ push_result = push_changes(feature_branch, self.project_root)
600
+
601
+ if push_result["success"]:
602
+ # Create PR
603
+ pr_title = f"Fix: {bug_description[:100]}"
604
+ pr_body = (
605
+ f"## Automated Bug Fix\n\n"
606
+ f"**Bug Description:** {bug_description}\n\n"
607
+ f"**Target File:** {target_file}\n\n"
608
+ f"**Quality Scores:**\n"
609
+ f"- Overall: {final_scores.get('overall_score', 0):.1f}/10\n"
610
+ f"- Security: {final_scores.get('security_score', 0):.1f}/10\n"
611
+ f"- Maintainability: {final_scores.get('maintainability_score', 0):.1f}/10\n\n"
612
+ f"**Iterations:** {iteration}\n\n"
613
+ f"**Auto-fixed by:** TappsCodingAgents Bug Fix Agent\n\n"
614
+ f"---\n\n"
615
+ f"*This PR was created automatically after passing quality gates.*"
616
+ )
617
+
618
+ pr_result = create_pull_request(
619
+ title=pr_title,
620
+ body=pr_body,
621
+ head_branch=feature_branch,
622
+ base_branch="main",
623
+ path=self.project_root,
624
+ )
625
+
626
+ if pr_result["success"]:
627
+ pr_info = {
628
+ "pr_url": pr_result.get("pr_url"),
629
+ "pr_number": pr_result.get("pr_number"),
630
+ "branch": feature_branch,
631
+ }
632
+ logger.info(f"Created pull request: {pr_result.get('pr_url', 'N/A')}")
633
+
634
+ if auto_merge_pr and not require_pr_review:
635
+ logger.info("Auto-merge enabled, but manual merge required (GitHub CLI limitation)")
636
+ else:
637
+ logger.warning(f"Failed to create PR: {pr_result.get('error')}")
638
+ # Fall back to commit info even if PR creation failed
639
+ commit_info = {
640
+ "commit_hash": commit_result["commit_hash"],
641
+ "branch": feature_branch,
642
+ "message": commit_message,
643
+ "pr_error": pr_result.get("error"),
644
+ }
645
+ else:
646
+ logger.warning(f"Failed to push branch: {push_result.get('error')}")
647
+ commit_info = {"error": f"Push failed: {push_result.get('error')}"}
648
+ else:
649
+ logger.warning(f"Failed to commit to branch: {commit_result.get('error')}")
650
+ commit_info = {"error": commit_result.get("error")}
651
+
652
+ else:
653
+ # Direct commit to main (original behavior)
654
+ commit_result = commit_changes(
655
+ message=commit_message,
656
+ files=[target_file] if target_file else None,
657
+ branch="main",
658
+ path=self.project_root,
659
+ )
660
+
661
+ if commit_result["success"]:
662
+ commit_info = {
663
+ "commit_hash": commit_result["commit_hash"],
664
+ "branch": commit_result["branch"],
665
+ "message": commit_message,
666
+ }
667
+ logger.info(
668
+ f"Committed changes to {commit_result['branch']}: "
669
+ f"{commit_result['commit_hash'][:8]}"
670
+ )
671
+ else:
672
+ logger.warning(
673
+ f"Failed to commit changes: {commit_result.get('error')}"
674
+ )
675
+ commit_info = {
676
+ "error": commit_result.get("error"),
677
+ }
678
+
679
+ except Exception as e:
680
+ logger.error(f"Error during commit: {e}", exc_info=True)
681
+ commit_info = {"error": str(e)}
682
+
683
+ # Update metrics for successful execution
684
+ execution_time = time.time() - start_time
685
+ if metrics_enabled:
686
+ final_scores = review_results[-1]["result"].get("scores", {}) if review_results else {}
687
+ metrics.update({
688
+ "iterations": iteration,
689
+ "success": True,
690
+ "execution_time": execution_time,
691
+ "final_quality_scores": final_scores,
692
+ "committed": commit_info is not None and "error" not in commit_info,
693
+ "security_scan_performed": pre_commit_security_scan and auto_commit,
694
+ })
695
+ logger.info(f"Execution metrics: {metrics}")
696
+
697
+ close_issue(self.project_root, beads_issue_id)
698
+ _write_fix_handoff(
699
+ self.project_root, workflow_id,
700
+ success=True, iterations=iteration,
701
+ target_file=target_file,
702
+ )
703
+ return {
704
+ "type": "fix",
705
+ "success": True,
706
+ "agents_executed": ["debugger", "implementer", "tester", "reviewer"] + (["ops"] if pre_commit_security_scan and auto_commit else []),
707
+ "iterations": iteration,
708
+ "review_results": review_results,
709
+ "quality_passed": True,
710
+ "committed": commit_info is not None and "error" not in commit_info,
711
+ "commit_info": commit_info,
712
+ "pr_info": pr_info,
713
+ "commit_strategy": commit_strategy,
714
+ "security_scan_result": security_scan_result if pre_commit_security_scan and auto_commit else None,
715
+ "metrics": metrics if metrics_enabled else None,
716
+ "summary": {
717
+ "bug_description": bug_description,
718
+ "target_file": target_file,
719
+ "iterations": iteration,
720
+ "final_quality": review_results[-1]["result"].get("scores", {}) if review_results else {},
721
+ "execution_time": execution_time,
722
+ },
723
+ }