bugbug 0.0.620__tar.gz → 0.0.622__tar.gz

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 (156) hide show
  1. {bugbug-0.0.620/bugbug.egg-info → bugbug-0.0.622}/PKG-INFO +1 -1
  2. bugbug-0.0.622/VERSION +1 -0
  3. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/test_scheduling.py +1 -1
  4. bugbug-0.0.622/bugbug/tools/build_repair/agent.py +559 -0
  5. bugbug-0.0.622/bugbug/tools/build_repair/config.py +90 -0
  6. bugbug-0.0.622/bugbug/tools/build_repair/prompts.py +77 -0
  7. bugbug-0.0.622/bugbug/tools/build_repair/scorer.py +207 -0
  8. bugbug-0.0.622/bugbug/tools/build_repair/try_server.py +286 -0
  9. bugbug-0.0.622/bugbug/tools/build_repair/worktree.py +81 -0
  10. {bugbug-0.0.620 → bugbug-0.0.622/bugbug.egg-info}/PKG-INFO +1 -1
  11. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug.egg-info/SOURCES.txt +9 -0
  12. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug.egg-info/requires.txt +12 -11
  13. {bugbug-0.0.620 → bugbug-0.0.622}/requirements.txt +12 -11
  14. bugbug-0.0.622/scripts/__init__.py +0 -0
  15. bugbug-0.0.622/scripts/build_repair_eval.py +378 -0
  16. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/comment_level_labeler.py +2 -1
  17. bugbug-0.0.622/scripts/gcp_logs_retriever.py +229 -0
  18. bugbug-0.0.620/VERSION +0 -1
  19. {bugbug-0.0.620 → bugbug-0.0.622}/LICENSE +0 -0
  20. {bugbug-0.0.620 → bugbug-0.0.622}/MANIFEST.in +0 -0
  21. {bugbug-0.0.620 → bugbug-0.0.622}/README.md +0 -0
  22. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/__init__.py +0 -0
  23. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/bug_features.py +0 -0
  24. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/bug_snapshot.py +0 -0
  25. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/bugzilla.py +0 -0
  26. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/code_search/__init__.py +0 -0
  27. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/code_search/function_search.py +0 -0
  28. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/code_search/mozilla.py +0 -0
  29. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/code_search/parser.py +0 -0
  30. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/code_search/searchfox_api.py +0 -0
  31. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/code_search/searchfox_data.py +0 -0
  32. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/code_search/searchfox_download.py +0 -0
  33. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/commit_features.py +0 -0
  34. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/db.py +0 -0
  35. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/feature_cleanup.py +0 -0
  36. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/github.py +0 -0
  37. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/issue_features.py +0 -0
  38. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/issue_snapshot.py +0 -0
  39. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/labels/annotateignore.csv +0 -0
  40. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/labels/bug_nobug.csv +0 -0
  41. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/labels/defect_enhancement_task.csv +0 -0
  42. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/labels/defect_enhancement_task_e.csv +0 -0
  43. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/labels/defect_enhancement_task_h.csv +0 -0
  44. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/labels/defect_enhancement_task_p.csv +0 -0
  45. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/labels/defect_enhancement_task_s.csv +0 -0
  46. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/labels/regression_bug_nobug.csv +0 -0
  47. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/labels/regressionrange.csv +0 -0
  48. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/labels/str.csv +0 -0
  49. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/labels/tracking.csv +0 -0
  50. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/labels.py +0 -0
  51. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/model.py +0 -0
  52. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/model_calibration.py +0 -0
  53. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/__init__.py +0 -0
  54. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/accessibility.py +0 -0
  55. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/annotate_ignore.py +0 -0
  56. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/assignee.py +0 -0
  57. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/backout.py +0 -0
  58. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/browsername.py +0 -0
  59. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/bugtype.py +0 -0
  60. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/component.py +0 -0
  61. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/defect.py +0 -0
  62. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/defect_enhancement_task.py +0 -0
  63. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/devdocneeded.py +0 -0
  64. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/fenixcomponent.py +0 -0
  65. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/fixtime.py +0 -0
  66. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/invalid_compatibility_report.py +0 -0
  67. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/needsdiagnosis.py +0 -0
  68. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/performancebug.py +0 -0
  69. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/qaneeded.py +0 -0
  70. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/rcatype.py +0 -0
  71. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/regression.py +0 -0
  72. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/regressionrange.py +0 -0
  73. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/regressor.py +0 -0
  74. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/spambug.py +0 -0
  75. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/stepstoreproduce.py +0 -0
  76. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/testfailure.py +0 -0
  77. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/testselect.py +0 -0
  78. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/tracking.py +0 -0
  79. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/uplift.py +0 -0
  80. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/models/worksforme.py +0 -0
  81. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/nlp.py +0 -0
  82. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/phabricator.py +0 -0
  83. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/repository.py +0 -0
  84. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/rust_code_analysis_server.py +0 -0
  85. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/swarm.py +0 -0
  86. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/test_scheduling_features.py +0 -0
  87. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/__init__.py +0 -0
  88. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/base.py +0 -0
  89. {bugbug-0.0.620/bugbug/tools/core → bugbug-0.0.622/bugbug/tools/build_repair}/__init__.py +0 -0
  90. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/code_review/__init__.py +0 -0
  91. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/code_review/agent.py +0 -0
  92. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/code_review/data_types.py +0 -0
  93. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/code_review/database.py +0 -0
  94. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/code_review/langchain_tools.py +0 -0
  95. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/code_review/prompts.py +0 -0
  96. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/code_review/protocols.py +0 -0
  97. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/code_review/scorer.py +0 -0
  98. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/code_review/utils.py +0 -0
  99. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/comment_resolution/__init__.py +0 -0
  100. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/comment_resolution/agent.py +0 -0
  101. {bugbug-0.0.620/scripts → bugbug-0.0.622/bugbug/tools/core}/__init__.py +0 -0
  102. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/core/connection.py +0 -0
  103. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/core/data_types.py +0 -0
  104. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/core/exceptions.py +0 -0
  105. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/core/llms.py +0 -0
  106. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/core/platforms/__init__.py +0 -0
  107. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/core/platforms/base.py +0 -0
  108. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/core/platforms/bugzilla.py +0 -0
  109. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/core/platforms/phabricator.py +0 -0
  110. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/patch_summarization/__init__.py +0 -0
  111. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/patch_summarization/agent.py +0 -0
  112. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/patch_summarization/prompts.py +0 -0
  113. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/suggestion_filtering/__init__.py +0 -0
  114. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/suggestion_filtering/agent.py +0 -0
  115. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/tools/suggestion_filtering/prompts.py +0 -0
  116. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/utils.py +0 -0
  117. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug/vectordb.py +0 -0
  118. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug.egg-info/dependency_links.txt +0 -0
  119. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug.egg-info/entry_points.txt +0 -0
  120. {bugbug-0.0.620 → bugbug-0.0.622}/bugbug.egg-info/top_level.txt +0 -0
  121. {bugbug-0.0.620 → bugbug-0.0.622}/extra-nlp-requirements.txt +0 -0
  122. {bugbug-0.0.620 → bugbug-0.0.622}/extra-nn-requirements.txt +0 -0
  123. {bugbug-0.0.620 → bugbug-0.0.622}/pyproject.toml +0 -0
  124. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/analyze_training_metrics.py +0 -0
  125. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/backout_related_test_regressions.py +0 -0
  126. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/bug_classifier.py +0 -0
  127. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/bug_retriever.py +0 -0
  128. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/check.py +0 -0
  129. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/check_all_metrics.py +0 -0
  130. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/comment_resolver_runner.py +0 -0
  131. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/commit_classifier.py +0 -0
  132. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/commit_retriever.py +0 -0
  133. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/compatibility_report_classifier.py +0 -0
  134. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/generate_landings_risk_report.py +0 -0
  135. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/generate_sheet.py +0 -0
  136. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/get_type_labels.py +0 -0
  137. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/get_untriaged.py +0 -0
  138. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/github_issue_classifier.py +0 -0
  139. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/github_issue_retriever.py +0 -0
  140. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/inline_comments_data_collection.py +0 -0
  141. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/maintenance_effectiveness_indicator.py +0 -0
  142. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/microannotate_generator.py +0 -0
  143. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/past_bugs_by_unit.py +0 -0
  144. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/redundant_failures.py +0 -0
  145. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/regressor_finder.py +0 -0
  146. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/retrieve_ci_failures.py +0 -0
  147. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/retrieve_training_metrics.py +0 -0
  148. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/review_comments_retriever.py +0 -0
  149. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/revision_retriever.py +0 -0
  150. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/shadow_scheduler_stats.py +0 -0
  151. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/test_scheduling_history_retriever.py +0 -0
  152. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/testing_policy_stats.py +0 -0
  153. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/trainer.py +0 -0
  154. {bugbug-0.0.620 → bugbug-0.0.622}/scripts/trainer_extract_args.py +0 -0
  155. {bugbug-0.0.620 → bugbug-0.0.622}/setup.cfg +0 -0
  156. {bugbug-0.0.620 → bugbug-0.0.622}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bugbug
3
- Version: 0.0.620
3
+ Version: 0.0.622
4
4
  Summary: ML tools for Mozilla projects
5
5
  Author: Marco Castelluccio
6
6
  Author-email: mcastelluccio@mozilla.com
bugbug-0.0.622/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.622
@@ -1027,7 +1027,7 @@ def find_manifests_for_paths(repo_dir_str: str, paths: list[str]) -> set[str]:
1027
1027
  )
1028
1028
 
1029
1029
  def has_wpt_files(d: Path) -> bool:
1030
- return any(is_wpt_file(p) for p in d.iterdir())
1030
+ return d.is_dir() and any(is_wpt_file(p) for p in d.iterdir())
1031
1031
 
1032
1032
  while cur_dir != test_root and not has_wpt_files(cur_dir):
1033
1033
  cur_dir = cur_dir.parent
@@ -0,0 +1,559 @@
1
+ # -*- coding: utf-8 -*-
2
+ # This Source Code Form is subject to the terms of the Mozilla Public
3
+ # License, v. 2.0. If a copy of the MPL was not distributed with this file,
4
+ # You can obtain one at http://mozilla.org/MPL/2.0/.
5
+
6
+ import subprocess
7
+ import traceback
8
+ from collections.abc import Callable
9
+ from logging import getLogger
10
+ from pathlib import Path
11
+
12
+ from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
13
+ from pydantic import BaseModel, Field
14
+ from tenacity import (
15
+ retry,
16
+ retry_if_exception,
17
+ retry_if_exception_message,
18
+ stop_after_attempt,
19
+ wait_exponential_jitter,
20
+ )
21
+
22
+ from bugbug.tools.base import GenerativeModelTool
23
+ from bugbug.tools.build_repair.config import (
24
+ ADDITIONAL_DIRS,
25
+ ALLOWED_TOOLS,
26
+ ANALYSIS_MODEL,
27
+ FIREFOX_MCP_URL,
28
+ FIX_MODEL,
29
+ SANDBOX_CONFIG,
30
+ VERIFY_ALLOWED_TOOLS,
31
+ VERIFY_MODEL,
32
+ )
33
+ from bugbug.tools.build_repair.prompts import (
34
+ ANALYSIS_TEMPLATE,
35
+ EVAL_PROMPT,
36
+ FIX_TEMPLATE,
37
+ VERIFY_TEMPLATE,
38
+ )
39
+
40
+ logger = getLogger(__name__)
41
+
42
+
43
+ class BuildFailure(BaseModel):
44
+ """Input describing a build failure from the dataset."""
45
+
46
+ bug_id: int = Field(description="The ID of the bug in Bugzilla.")
47
+ bug_title: str | None = Field(default=None, description="Optional bug title.")
48
+ bug_comments: list[str] | None = Field(
49
+ default=None, description="Optional bug comments."
50
+ )
51
+ git_commit: str = Field(description="Git revision to checkout.")
52
+ failure_tasks: list[dict] = Field(
53
+ description="List of {task_name, task_id, retry_id, failure_lines}."
54
+ )
55
+
56
+
57
+ class UsageStats(BaseModel):
58
+ cost_usd: float = Field(default=0.0)
59
+ num_turns: int = Field(default=0)
60
+ input_tokens: int = Field(default=0)
61
+ output_tokens: int = Field(default=0)
62
+ cache_read_input_tokens: int = Field(default=0)
63
+ cache_creation_input_tokens: int = Field(default=0)
64
+
65
+
66
+ class AgentResponse(UsageStats):
67
+ """Output from a build repair run, including analysis, diff, cost, and build results."""
68
+
69
+ summary: str = Field(default="")
70
+ analysis: str = Field(default="")
71
+ diff: str = Field(default="")
72
+ error: str | None = Field(default=None)
73
+ error_traceback: str | None = Field(default=None)
74
+ failure_stage: str | None = Field(default=None)
75
+ cost_usd: float = Field(default=0.0)
76
+ num_turns: int = Field(default=0)
77
+ input_tokens: int = Field(default=0)
78
+ output_tokens: int = Field(default=0)
79
+ cache_read_input_tokens: int = Field(default=0)
80
+ cache_creation_input_tokens: int = Field(default=0)
81
+ local_build_passed: bool | None = Field(default=None)
82
+ try_build_passed: bool | None = Field(default=None)
83
+ lando_job_id: str | None = Field(default=None)
84
+ treeherder_url: str | None = Field(default=None)
85
+ stage1_transcript: list[dict] = Field(default_factory=list)
86
+ stage2_transcript: list[dict] = Field(default_factory=list)
87
+
88
+
89
+ class GroundTruth(BaseModel):
90
+ gh_fix_commits: list[str] = Field(
91
+ description="Git commit hashes of the ground truth fix."
92
+ )
93
+
94
+
95
+ class Judgment(BaseModel):
96
+ analysis_correct: bool
97
+ analysis_quality: float
98
+ analysis_explanation: str
99
+ fix_matches_ground_truth: bool
100
+ fix_quality: float
101
+ fix_explanation: str
102
+ fix_acceptance_probability: float
103
+ fix_acceptance_explanation: str
104
+
105
+
106
+ class VerifyResponse(UsageStats):
107
+ judgment: Judgment | None = Field(default=None)
108
+ verification_transcript: list[dict] = Field(default_factory=list)
109
+
110
+
111
+ class BuildRepairTool(GenerativeModelTool):
112
+ """Two-stage build repair agent using Claude Agent SDK.
113
+
114
+ Stage 1: Analyzes the failure and produces analysis/planning/summary docs.
115
+ Stage 2: Reads the analysis and implements a fix. Skipped in analysis-only mode.
116
+ After Stage 2, commits the fix, runs ./mach build, and optionally submits to try.
117
+ """
118
+
119
+ def __init__(
120
+ self,
121
+ target_software: str = "Mozilla Firefox",
122
+ analysis_only: bool = False,
123
+ eval_mode: bool = False,
124
+ analysis_model: str = ANALYSIS_MODEL,
125
+ fix_model: str = FIX_MODEL,
126
+ verify_model: str = VERIFY_MODEL,
127
+ ) -> None:
128
+ self.eval_mode = eval_mode
129
+ self.target_software = target_software
130
+ self.analysis_only = analysis_only
131
+ self.analysis_model = analysis_model
132
+ self.fix_model = fix_model
133
+ self.verify_model = verify_model
134
+
135
+ @classmethod
136
+ def create(cls, **kwargs):
137
+ return cls(**kwargs)
138
+
139
+ @staticmethod
140
+ def _usage_fields(usage: dict) -> dict:
141
+ return {
142
+ "input_tokens": usage.get("input_tokens", 0),
143
+ "output_tokens": usage.get("output_tokens", 0),
144
+ "cache_read_input_tokens": usage.get("cache_read_input_tokens", 0),
145
+ "cache_creation_input_tokens": usage.get("cache_creation_input_tokens", 0),
146
+ }
147
+
148
+ @staticmethod
149
+ def _serialize_message(message) -> dict:
150
+ data = {"type": type(message).__name__}
151
+ if hasattr(message, "model_dump"):
152
+ data.update(message.model_dump())
153
+ elif hasattr(message, "__dict__"):
154
+ data.update(vars(message))
155
+ else:
156
+ data["raw"] = str(message)
157
+ return data
158
+
159
+ async def _run_stage(
160
+ self,
161
+ stage_name: str,
162
+ prompt: str,
163
+ model: str,
164
+ options: ClaudeAgentOptions,
165
+ bug_id: int,
166
+ on_message: Callable[[str, dict], None] | None = None,
167
+ ) -> tuple[list[dict], float, int, dict]:
168
+ transcript: list[dict] = []
169
+ cost = 0.0
170
+ turns = 0
171
+ result_data: dict = {}
172
+ usage: dict = {}
173
+
174
+ @retry(
175
+ retry=(
176
+ retry_if_exception_message(match="Control request timeout")
177
+ | retry_if_exception_message(match="overloaded")
178
+ | retry_if_exception_message(match="529")
179
+ | retry_if_exception_message(match="exit code")
180
+ | retry_if_exception(
181
+ lambda e: isinstance(e, (TimeoutError, ConnectionError, OSError))
182
+ )
183
+ ),
184
+ stop=stop_after_attempt(5),
185
+ wait=wait_exponential_jitter(initial=2, max=60, jitter=5),
186
+ before_sleep=lambda rs: logger.warning(
187
+ f"Bug {bug_id}: {stage_name} transient error "
188
+ f"(attempt {rs.attempt_number}/5), retrying: {rs.outcome.exception()}"
189
+ ),
190
+ reraise=True,
191
+ )
192
+ async def _query():
193
+ nonlocal cost, turns, usage, result_data
194
+ async for message in query(prompt=prompt, options=options):
195
+ serialized = self._serialize_message(message)
196
+ transcript.append(serialized)
197
+ logger.debug(f"Bug {bug_id}: {stage_name} [{serialized['type']}]")
198
+ if on_message:
199
+ on_message(stage_name, serialized)
200
+ if isinstance(message, ResultMessage):
201
+ cost += message.total_cost_usd or 0
202
+ turns += message.num_turns or 0
203
+ usage = getattr(message, "usage", {}) or {}
204
+ result_data = serialized
205
+
206
+ if on_message:
207
+ on_message(
208
+ stage_name,
209
+ {
210
+ "type": "stage_start",
211
+ "prompt": prompt,
212
+ "model": model,
213
+ },
214
+ )
215
+ try:
216
+ await _query()
217
+ finally:
218
+ if on_message:
219
+ on_message(
220
+ stage_name,
221
+ {
222
+ "type": "stage_end",
223
+ "cost_usd": cost,
224
+ "num_turns": turns,
225
+ "result_data": result_data,
226
+ },
227
+ )
228
+
229
+ return transcript, cost, turns, usage
230
+
231
+ def _prepare_input_files(self, failure: BuildFailure, worktree_path: Path) -> None:
232
+ in_dir = worktree_path / "repair_agent" / "in" / str(failure.bug_id)
233
+ in_dir.mkdir(parents=True, exist_ok=True)
234
+
235
+ (in_dir / "bug_description.md").write_text(
236
+ f"# Bug {failure.bug_id}: {failure.bug_title}\n\n"
237
+ + "\n\n---\n\n".join(failure.bug_comments or [])
238
+ )
239
+
240
+ logs_content = ""
241
+ for task in failure.failure_tasks:
242
+ logs_content += f"## {task['task_name']} (task_id: {task['task_id']})\n\n"
243
+ logs_content += "\n".join(task["failure_lines"]) + "\n\n"
244
+ (in_dir / "build_failure_logs.md").write_text(logs_content)
245
+
246
+ out_dir = worktree_path / "repair_agent" / "out" / str(failure.bug_id)
247
+ out_dir.mkdir(parents=True, exist_ok=True)
248
+
249
+ logger.info(
250
+ f"Prepared input files for bug {failure.bug_id} at {in_dir} "
251
+ f"({len(failure.failure_tasks)} failure tasks)"
252
+ )
253
+
254
+ def _read_output(self, failure: BuildFailure, worktree_path: Path, key: str) -> str:
255
+ path = (
256
+ worktree_path / "repair_agent" / "out" / str(failure.bug_id) / f"{key}.md"
257
+ )
258
+ if path.exists():
259
+ return path.read_text()
260
+ return ""
261
+
262
+ async def run(
263
+ self,
264
+ failure: BuildFailure,
265
+ worktree_path: Path,
266
+ skip_try_push: bool = False,
267
+ on_message: Callable[[str, dict], None] | None = None,
268
+ ) -> AgentResponse:
269
+ logger.info(
270
+ f"Starting build repair for bug {failure.bug_id} "
271
+ f"(commit={failure.git_commit}, worktree={worktree_path}, "
272
+ f"analysis_only={self.analysis_only}, skip_try_push={skip_try_push})"
273
+ )
274
+ self._prepare_input_files(failure, worktree_path)
275
+
276
+ mcp_servers = {"firefox": {"type": "http", "url": FIREFOX_MCP_URL}}
277
+ disallowed = ["AskUserQuestion", "Task"]
278
+ total_cost = 0.0
279
+ total_turns = 0
280
+ total_usage: dict = {}
281
+
282
+ logger.info(
283
+ f"Bug {failure.bug_id}: starting Stage 1 (analysis) "
284
+ f"with model={self.analysis_model}"
285
+ )
286
+ stage1_options = ClaudeAgentOptions(
287
+ model=self.analysis_model,
288
+ cwd=str(worktree_path),
289
+ allowed_tools=ALLOWED_TOOLS,
290
+ disallowed_tools=disallowed,
291
+ add_dirs=ADDITIONAL_DIRS,
292
+ sandbox=SANDBOX_CONFIG,
293
+ permission_mode="acceptEdits",
294
+ effort="high",
295
+ mcp_servers=mcp_servers,
296
+ )
297
+ analysis_prompt = ANALYSIS_TEMPLATE.format(
298
+ bug_id=failure.bug_id,
299
+ target_software=self.target_software,
300
+ worktree_path=worktree_path,
301
+ eval=EVAL_PROMPT if self.eval_mode else "",
302
+ )
303
+ try:
304
+ (
305
+ stage1_transcript,
306
+ stage1_cost,
307
+ stage1_turns,
308
+ stage1_usage,
309
+ ) = await self._run_stage(
310
+ "analysis",
311
+ analysis_prompt,
312
+ self.analysis_model,
313
+ stage1_options,
314
+ failure.bug_id,
315
+ on_message,
316
+ )
317
+ total_cost += stage1_cost
318
+ total_turns += stage1_turns
319
+ for k, v in stage1_usage.items():
320
+ if isinstance(v, (int, float)):
321
+ total_usage[k] = total_usage.get(k, 0) + v
322
+ except Exception as e:
323
+ logger.error(
324
+ f"Bug {failure.bug_id}: Stage 1 (analysis) failed: {e}", exc_info=True
325
+ )
326
+ return AgentResponse(
327
+ error=str(e),
328
+ error_traceback=traceback.format_exc(),
329
+ failure_stage="analysis",
330
+ cost_usd=total_cost,
331
+ num_turns=total_turns,
332
+ **self._usage_fields(total_usage),
333
+ )
334
+
335
+ logger.info(
336
+ f"Bug {failure.bug_id}: Stage 1 complete "
337
+ f"(cost=${total_cost:.4f}, turns={total_turns})"
338
+ )
339
+
340
+ summary = self._read_output(failure, worktree_path, "summary")
341
+ analysis = self._read_output(failure, worktree_path, "analysis")
342
+ logger.info(
343
+ f"Bug {failure.bug_id}: read output files "
344
+ f"(summary={len(summary)} chars, analysis={len(analysis)} chars)"
345
+ )
346
+
347
+ if self.analysis_only:
348
+ logger.info(f"Bug {failure.bug_id}: analysis-only mode, skipping Stage 2")
349
+ return AgentResponse(
350
+ summary=summary,
351
+ analysis=analysis,
352
+ cost_usd=total_cost,
353
+ num_turns=total_turns,
354
+ **self._usage_fields(total_usage),
355
+ stage1_transcript=stage1_transcript,
356
+ )
357
+
358
+ logger.info(
359
+ f"Bug {failure.bug_id}: starting Stage 2 (fix) with model={self.fix_model}"
360
+ )
361
+ stage2_options = ClaudeAgentOptions(
362
+ model=self.fix_model,
363
+ cwd=str(worktree_path),
364
+ allowed_tools=ALLOWED_TOOLS,
365
+ disallowed_tools=disallowed,
366
+ add_dirs=ADDITIONAL_DIRS,
367
+ sandbox=SANDBOX_CONFIG,
368
+ permission_mode="acceptEdits",
369
+ effort="low",
370
+ mcp_servers=mcp_servers,
371
+ )
372
+ fix_prompt = FIX_TEMPLATE.format(
373
+ target_software=self.target_software,
374
+ bug_id=failure.bug_id,
375
+ worktree_path=worktree_path,
376
+ eval=EVAL_PROMPT if self.eval_mode else "",
377
+ )
378
+ try:
379
+ (
380
+ stage2_transcript,
381
+ stage2_cost,
382
+ stage2_turns,
383
+ stage2_usage,
384
+ ) = await self._run_stage(
385
+ "fix",
386
+ fix_prompt,
387
+ self.fix_model,
388
+ stage2_options,
389
+ failure.bug_id,
390
+ on_message,
391
+ )
392
+ total_cost += stage2_cost
393
+ total_turns += stage2_turns
394
+ for k, v in stage2_usage.items():
395
+ if isinstance(v, (int, float)):
396
+ total_usage[k] = total_usage.get(k, 0) + v
397
+ except Exception as e:
398
+ logger.error(
399
+ f"Bug {failure.bug_id}: Stage 2 (fix) failed: {e}", exc_info=True
400
+ )
401
+ return AgentResponse(
402
+ summary=summary,
403
+ analysis=analysis,
404
+ error=str(e),
405
+ error_traceback=traceback.format_exc(),
406
+ failure_stage="fix",
407
+ cost_usd=total_cost,
408
+ num_turns=total_turns,
409
+ **self._usage_fields(total_usage),
410
+ )
411
+
412
+ logger.info(
413
+ f"Bug {failure.bug_id}: Stage 2 complete "
414
+ f"(cost=${total_cost:.4f}, turns={total_turns})"
415
+ )
416
+
417
+ subprocess.run(
418
+ ["git", "add", "-A"],
419
+ cwd=worktree_path,
420
+ capture_output=True,
421
+ )
422
+ diff_result = subprocess.run(
423
+ ["git", "diff", "--staged", "HEAD"],
424
+ cwd=worktree_path,
425
+ capture_output=True,
426
+ text=True,
427
+ )
428
+ diff = diff_result.stdout
429
+ logger.info(f"Bug {failure.bug_id}: git diff produced {len(diff)} chars")
430
+
431
+ if not diff.strip():
432
+ logger.warning(f"Bug {failure.bug_id}: no diff produced, returning early")
433
+ return AgentResponse(
434
+ summary=summary,
435
+ analysis=analysis,
436
+ diff=diff,
437
+ cost_usd=total_cost,
438
+ num_turns=total_turns,
439
+ **self._usage_fields(total_usage),
440
+ stage1_transcript=stage1_transcript,
441
+ stage2_transcript=stage2_transcript,
442
+ )
443
+
444
+ from bugbug.tools.build_repair.try_server import run_try_verification
445
+
446
+ task_name = (
447
+ failure.failure_tasks[0]["task_name"] if failure.failure_tasks else ""
448
+ )
449
+ logger.info(
450
+ f"Bug {failure.bug_id}: starting try verification "
451
+ f"(task={task_name}, skip_try_push={skip_try_push})"
452
+ )
453
+ try_result = run_try_verification(
454
+ worktree_path=worktree_path,
455
+ bug_id=failure.bug_id,
456
+ task_name=task_name,
457
+ skip_try_push=skip_try_push,
458
+ )
459
+
460
+ logger.info(
461
+ f"Bug {failure.bug_id}: try verification done "
462
+ f"(local_build={try_result.local_build_passed}, "
463
+ f"try_build={try_result.try_build_passed}, "
464
+ f"lando_job={try_result.lando_job_id}, "
465
+ f"total_cost=${total_cost:.4f}, total_turns={total_turns})"
466
+ )
467
+ return AgentResponse(
468
+ summary=summary,
469
+ analysis=analysis,
470
+ diff=diff,
471
+ cost_usd=total_cost,
472
+ num_turns=total_turns,
473
+ **self._usage_fields(total_usage),
474
+ local_build_passed=try_result.local_build_passed,
475
+ try_build_passed=try_result.try_build_passed,
476
+ lando_job_id=try_result.lando_job_id,
477
+ treeherder_url=try_result.treeherder_url,
478
+ stage1_transcript=stage1_transcript,
479
+ stage2_transcript=stage2_transcript,
480
+ )
481
+
482
+ @retry(
483
+ stop=stop_after_attempt(3),
484
+ wait=wait_exponential_jitter(initial=2, max=30, jitter=5),
485
+ before_sleep=lambda rs: logger.warning(
486
+ f"Verification failed (attempt {rs.attempt_number}/3), "
487
+ f"retrying: {rs.outcome.exception()}"
488
+ ),
489
+ reraise=True,
490
+ )
491
+ async def verify(
492
+ self,
493
+ failure: BuildFailure,
494
+ agent_diff: str,
495
+ ground_truth: GroundTruth,
496
+ worktree_path: Path,
497
+ on_message: Callable[[str, dict], None] | None = None,
498
+ ) -> VerifyResponse:
499
+ out_dir = worktree_path / "repair_agent" / "out" / str(failure.bug_id)
500
+ out_dir.mkdir(parents=True, exist_ok=True)
501
+ (out_dir / "agent_fix.diff").write_text(agent_diff, encoding="utf-8")
502
+
503
+ gt_commits = " ".join(ground_truth.gh_fix_commits)
504
+ prompt = VERIFY_TEMPLATE.format(
505
+ target_software=self.target_software,
506
+ bug_id=failure.bug_id,
507
+ failure_commit=failure.git_commit,
508
+ ground_truth_commits=gt_commits,
509
+ worktree_path=worktree_path,
510
+ )
511
+
512
+ options = ClaudeAgentOptions(
513
+ model=self.verify_model,
514
+ cwd=str(worktree_path),
515
+ allowed_tools=VERIFY_ALLOWED_TOOLS,
516
+ disallowed_tools=["AskUserQuestion", "Task"],
517
+ sandbox=SANDBOX_CONFIG,
518
+ permission_mode="acceptEdits",
519
+ effort="high",
520
+ output_format={
521
+ "type": "json_schema",
522
+ "schema": Judgment.model_json_schema(),
523
+ },
524
+ )
525
+
526
+ logger.info(
527
+ f"Bug {failure.bug_id}: starting verification stage "
528
+ f"(model={self.verify_model}, ground_truth={gt_commits})"
529
+ )
530
+
531
+ transcript, cost, turns, usage = await self._run_stage(
532
+ "verification",
533
+ prompt,
534
+ self.verify_model,
535
+ options,
536
+ failure.bug_id,
537
+ on_message,
538
+ )
539
+
540
+ judgment: Judgment | None = None
541
+ for msg in reversed(transcript):
542
+ if msg.get("structured_output"):
543
+ judgment = Judgment.model_validate(msg["structured_output"])
544
+ break
545
+
546
+ if judgment is None:
547
+ result_msgs = [m for m in transcript if m.get("type") == "ResultMessage"]
548
+ raise RuntimeError(
549
+ f"Bug {failure.bug_id}: verification produced no structured output. "
550
+ f"Result messages: {result_msgs}"
551
+ )
552
+
553
+ return VerifyResponse(
554
+ judgment=judgment,
555
+ cost_usd=cost,
556
+ num_turns=turns,
557
+ verification_transcript=transcript,
558
+ **self._usage_fields(usage),
559
+ )
@@ -0,0 +1,90 @@
1
+ # -*- coding: utf-8 -*-
2
+ # This Source Code Form is subject to the terms of the Mozilla Public
3
+ # License, v. 2.0. If a copy of the MPL was not distributed with this file,
4
+ # You can obtain one at http://mozilla.org/MPL/2.0/.
5
+
6
+ from datetime import date
7
+
8
+ from claude_agent_sdk import SandboxNetworkConfig, SandboxSettings
9
+
10
+ ANALYSIS_MODEL = "claude-opus-4-6"
11
+ FIX_MODEL = "claude-opus-4-6"
12
+ VERIFY_MODEL = "claude-opus-4-6"
13
+ DEFAULT_MAX_TURNS = 80
14
+ WORKTREE_BASE_DIR = "/tmp/build_repair_worktrees"
15
+ TRY_PUSH_TIMEOUT_SECONDS = 7200
16
+ TRY_PUSH_POLL_INTERVAL_SECONDS = 60
17
+ TREEHERDER_BASE_URL = "https://treeherder.mozilla.org"
18
+
19
+ FIREFOX_MCP_URL = "https://mcp-dev.moz.tools/mcp"
20
+
21
+ # Training data cutoff dates per model, for data contamination filtering.
22
+ # Examples with fix_commit_date before the cutoff may have been in training data.
23
+ # Source: https://platform.claude.com/docs/en/about-claude/models/overview
24
+ MODEL_CUTOFF_DATES = {
25
+ "claude-opus-4-6": date(2025, 8, 1),
26
+ "claude-sonnet-4-6": date(2026, 1, 1),
27
+ "claude-haiku-4-5-20251001": date(2025, 7, 1),
28
+ "claude-sonnet-4-5-20250929": date(2025, 7, 1),
29
+ "claude-opus-4-5-20251101": date(2025, 8, 1),
30
+ "claude-opus-4-1-20250805": date(2025, 3, 1),
31
+ "claude-sonnet-4-20250514": date(2025, 3, 1),
32
+ "claude-3-7-sonnet-20250219": date(2024, 11, 1),
33
+ "claude-opus-4-20250514": date(2025, 3, 1),
34
+ }
35
+
36
+ VERIFY_ALLOWED_TOOLS = [
37
+ "Read",
38
+ "Bash(git show:*)",
39
+ "Bash(git log:*)",
40
+ "Bash(git diff:*)",
41
+ "Bash(find:*)",
42
+ "Bash(grep:*)",
43
+ "WebFetch(domain:firefox-source-docs.mozilla.org)",
44
+ "WebFetch(domain:searchfox.org)",
45
+ ]
46
+
47
+ ALLOWED_TOOLS = [
48
+ "Edit(~/.mozbuild)",
49
+ "Edit(~/.cache/uv)",
50
+ "Bash(./mach build:*)",
51
+ "Bash(./mach clobber:*)",
52
+ "Bash(./mach configure:*)",
53
+ "Bash(./mach run:*)",
54
+ "Bash(./mach test:*)",
55
+ "Bash(./mach wpt:*)",
56
+ "Bash(./mach lint:*)",
57
+ "Bash(./mach format:*)",
58
+ "Bash(./mach clang-format:*)",
59
+ "Bash(./mach try:*)",
60
+ "Bash(./mach help:*)",
61
+ "Bash(./mach vendor:*)",
62
+ "Bash(./mach bootstrap:*)",
63
+ "Bash(./mach artifact:*)",
64
+ "Bash(clang++:*)",
65
+ "Bash(rm:*)",
66
+ "Bash(timeout:*)",
67
+ "Bash(find:*)",
68
+ "Bash(grep:*)",
69
+ "Bash(tee:*)",
70
+ "Bash(kill:*)",
71
+ "Bash(searchfox-cli:*)",
72
+ "Bash(treeherder-cli:*)",
73
+ "Bash(jj:*)",
74
+ "WebFetch(domain:firefox-source-docs.mozilla.org)",
75
+ "WebFetch(domain:treeherder.mozilla.org)",
76
+ "WebFetch(domain:searchfox.org)",
77
+ "WebFetch(o1069899.ingest.sentry.io)",
78
+ ]
79
+
80
+ ADDITIONAL_DIRS = [
81
+ "~/.mozbuild",
82
+ "~/.cache/uv/",
83
+ ]
84
+
85
+ SANDBOX_CONFIG = SandboxSettings(
86
+ enabled=True,
87
+ autoAllowBashIfSandboxed=True,
88
+ allowUnsandboxedCommands=False,
89
+ network=SandboxNetworkConfig(allowLocalBinding=True),
90
+ )