bugpilot 0.1.0__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.
- bugpilot/__init__.py +5 -0
- bugpilot/__main__.py +5 -0
- bugpilot/cli.py +2615 -0
- bugpilot/cli_json.py +173 -0
- bugpilot/core/__init__.py +1 -0
- bugpilot/core/agent_runner.py +138 -0
- bugpilot/core/artifact_io.py +40 -0
- bugpilot/core/artifacts.py +47 -0
- bugpilot/core/attachments.py +247 -0
- bugpilot/core/branch_policy.py +223 -0
- bugpilot/core/cleanup.py +58 -0
- bugpilot/core/code_files.py +99 -0
- bugpilot/core/config.py +242 -0
- bugpilot/core/context.py +436 -0
- bugpilot/core/copilot.py +47 -0
- bugpilot/core/delivery_instructions.py +106 -0
- bugpilot/core/doctor.py +66 -0
- bugpilot/core/email_notify.py +316 -0
- bugpilot/core/errors.py +105 -0
- bugpilot/core/executables.py +87 -0
- bugpilot/core/fix_mode_state.py +212 -0
- bugpilot/core/fix_mode_store.py +600 -0
- bugpilot/core/fix_modes.py +412 -0
- bugpilot/core/fix_report.py +124 -0
- bugpilot/core/git_history.py +1579 -0
- bugpilot/core/git_ops.py +254 -0
- bugpilot/core/handoff.py +127 -0
- bugpilot/core/identity.py +112 -0
- bugpilot/core/input_adapters.py +97 -0
- bugpilot/core/instructions.py +337 -0
- bugpilot/core/issue.py +487 -0
- bugpilot/core/jira.py +886 -0
- bugpilot/core/jira_adf.py +167 -0
- bugpilot/core/jira_parse.py +442 -0
- bugpilot/core/keywords.py +386 -0
- bugpilot/core/logging_utils.py +28 -0
- bugpilot/core/memory.py +151 -0
- bugpilot/core/models.py +322 -0
- bugpilot/core/project_settings.py +283 -0
- bugpilot/core/prompts.py +567 -0
- bugpilot/core/repository_profile.py +739 -0
- bugpilot/core/retrieval.py +485 -0
- bugpilot/core/review_changes.py +155 -0
- bugpilot/core/review_report.py +171 -0
- bugpilot/core/run.py +171 -0
- bugpilot/core/safe_paths.py +173 -0
- bugpilot/core/search.py +765 -0
- bugpilot/core/search_terms.py +310 -0
- bugpilot/core/setup.py +212 -0
- bugpilot/core/user_config.py +211 -0
- bugpilot/core/verification_report.py +313 -0
- bugpilot/core/workflow.py +2385 -0
- bugpilot/mcp_server.py +651 -0
- bugpilot-0.1.0.dist-info/METADATA +270 -0
- bugpilot-0.1.0.dist-info/RECORD +59 -0
- bugpilot-0.1.0.dist-info/WHEEL +5 -0
- bugpilot-0.1.0.dist-info/entry_points.txt +3 -0
- bugpilot-0.1.0.dist-info/licenses/LICENSE +122 -0
- bugpilot-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""AI Fix Mode definitions and registry.
|
|
2
|
+
|
|
3
|
+
Fix Modes describe *how* an AI coding agent should approach a prepared bug.
|
|
4
|
+
They are deliberately separate from bug evidence and from BugPilot-owned safety
|
|
5
|
+
rules. A mode may tune investigation, implementation and verification, but it
|
|
6
|
+
must never be able to weaken Git/Jira/destructive-action safeguards enforced by
|
|
7
|
+
BugPilot elsewhere.
|
|
8
|
+
|
|
9
|
+
One structured field, `execution_kind`, exists because prose alone cannot be
|
|
10
|
+
acted on: an investigation-only mode has to suppress the commit/push offer in
|
|
11
|
+
the generated task package, and a renderer cannot infer that from instruction
|
|
12
|
+
text. It is deliberately two values rather than the beginning of a workflow
|
|
13
|
+
DSL; anything more expressive belongs in the instruction sections, which are
|
|
14
|
+
prose for the agent and not logic for BugPilot.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
import re
|
|
21
|
+
from typing import Iterable, Literal
|
|
22
|
+
|
|
23
|
+
FixModeSource = Literal["builtin", "user", "project"]
|
|
24
|
+
ExecutionKind = Literal["fix", "investigate"]
|
|
25
|
+
|
|
26
|
+
DEFAULT_FIX_MODE_ID = "standard"
|
|
27
|
+
|
|
28
|
+
# Editable instruction text is interpolated into task.md as Markdown, and
|
|
29
|
+
# BugPilot owns that document's structure: the safety headings an agent looks for
|
|
30
|
+
# ("## Forbidden Actions", "## BugPilot Rule Precedence") must mean what BugPilot
|
|
31
|
+
# put there. A mode section carrying its own headings could forge a second copy
|
|
32
|
+
# of one, above the real one, and textual precedence cannot defend against a
|
|
33
|
+
# forged copy of the precedence rule. So sections are prose, not documents.
|
|
34
|
+
MAX_FIX_MODE_TEXT_LENGTH = 12_000
|
|
35
|
+
_MODE_ID_RE = re.compile(r"^[a-z][a-z0-9-]{0,63}$")
|
|
36
|
+
_MARKDOWN_HEADING_RE = re.compile(r"^#{1,6}\s", re.MULTILINE)
|
|
37
|
+
_SECTION_NAMES: tuple[str, ...] = (
|
|
38
|
+
"objective",
|
|
39
|
+
"investigation",
|
|
40
|
+
"implementation",
|
|
41
|
+
"verification",
|
|
42
|
+
"constraints",
|
|
43
|
+
"completion",
|
|
44
|
+
)
|
|
45
|
+
_VALID_SOURCES: frozenset[str] = frozenset({"builtin", "user", "project"})
|
|
46
|
+
_VALID_EXECUTION_KINDS: frozenset[str] = frozenset({"fix", "investigate"})
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_valid_fix_mode_id(value: object) -> bool:
|
|
50
|
+
"""Whether a value is a usable Fix Mode id.
|
|
51
|
+
|
|
52
|
+
Public because a custom mode's id becomes a file name, and the code that
|
|
53
|
+
builds that path has to ask the question before it builds anything — with
|
|
54
|
+
the model's own rule rather than a second copy of the pattern.
|
|
55
|
+
"""
|
|
56
|
+
return isinstance(value, str) and bool(_MODE_ID_RE.fullmatch(value))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class FixModeError(ValueError):
|
|
60
|
+
"""Base error for invalid Fix Mode definitions or lookup."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class FixModeNotFoundError(FixModeError):
|
|
64
|
+
"""Raised when a requested Fix Mode ID is not registered."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _require_text(value: object, field: str) -> str:
|
|
68
|
+
"""A required text field, checked for type before content.
|
|
69
|
+
|
|
70
|
+
Custom modes will arrive from a file in a later phase, so `validate()` is
|
|
71
|
+
the boundary where a hand-edited definition becomes trusted. Reaching
|
|
72
|
+
`value.strip()` on a number or a list there would raise AttributeError or
|
|
73
|
+
TypeError — the wrong error, from the wrong layer, naming nothing the
|
|
74
|
+
developer can fix — so every field is type-checked first and every failure
|
|
75
|
+
leaves here as a FixModeError.
|
|
76
|
+
"""
|
|
77
|
+
if not isinstance(value, str):
|
|
78
|
+
raise FixModeError(
|
|
79
|
+
f"Fix Mode field {field!r} must be a string, got {type(value).__name__}."
|
|
80
|
+
)
|
|
81
|
+
if not value.strip():
|
|
82
|
+
raise FixModeError(f"Fix Mode field {field!r} must not be empty.")
|
|
83
|
+
return value
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _require_document_safe_text(value: object, field: str) -> str:
|
|
87
|
+
"""Instruction text that cannot restructure the document it is rendered into.
|
|
88
|
+
|
|
89
|
+
Only a heading at the start of a line is rejected, so `#` inside prose
|
|
90
|
+
("issue #4102", "C#") is fine. An indented `#` is fine too, which keeps
|
|
91
|
+
comment lines in an example readable.
|
|
92
|
+
"""
|
|
93
|
+
text = _require_text(value, field)
|
|
94
|
+
if _MARKDOWN_HEADING_RE.search(text):
|
|
95
|
+
raise FixModeError(
|
|
96
|
+
f"Fix Mode field {field!r} must not contain Markdown headings. BugPilot "
|
|
97
|
+
"owns the task document's structure, including its safety headings."
|
|
98
|
+
)
|
|
99
|
+
if len(text) > MAX_FIX_MODE_TEXT_LENGTH:
|
|
100
|
+
raise FixModeError(
|
|
101
|
+
f"Fix Mode field {field!r} exceeds the maximum length of "
|
|
102
|
+
f"{MAX_FIX_MODE_TEXT_LENGTH} characters."
|
|
103
|
+
)
|
|
104
|
+
return text
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _require_positive_int(value: object, field: str) -> int:
|
|
108
|
+
"""A required integer >= 1. `True` is not 1 here, however Python feels."""
|
|
109
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
110
|
+
raise FixModeError(
|
|
111
|
+
f"Fix Mode field {field!r} must be an integer, got {type(value).__name__}."
|
|
112
|
+
)
|
|
113
|
+
if value < 1:
|
|
114
|
+
raise FixModeError(f"Fix Mode field {field!r} must be >= 1.")
|
|
115
|
+
return value
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _require_choice(value: object, field: str, allowed: frozenset[str]) -> str:
|
|
119
|
+
if not isinstance(value, str):
|
|
120
|
+
raise FixModeError(
|
|
121
|
+
f"Fix Mode field {field!r} must be a string, got {type(value).__name__}."
|
|
122
|
+
)
|
|
123
|
+
if value not in allowed:
|
|
124
|
+
raise FixModeError(
|
|
125
|
+
f"Unsupported Fix Mode {field}: {value!r}. "
|
|
126
|
+
f"Expected one of: {', '.join(sorted(allowed))}."
|
|
127
|
+
)
|
|
128
|
+
return value
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass(frozen=True)
|
|
132
|
+
class FixMode:
|
|
133
|
+
"""Provider-neutral instructions that control an AI bug-fixing workflow."""
|
|
134
|
+
|
|
135
|
+
id: str
|
|
136
|
+
name: str
|
|
137
|
+
description: str
|
|
138
|
+
objective: str
|
|
139
|
+
investigation: str
|
|
140
|
+
implementation: str
|
|
141
|
+
verification: str
|
|
142
|
+
constraints: str
|
|
143
|
+
completion: str
|
|
144
|
+
source: FixModeSource = "builtin"
|
|
145
|
+
based_on: str | None = None
|
|
146
|
+
based_on_version: int | None = None
|
|
147
|
+
version: int = 1
|
|
148
|
+
execution_kind: ExecutionKind = "fix"
|
|
149
|
+
|
|
150
|
+
def validate(self) -> None:
|
|
151
|
+
"""Validate a mode without depending on CLI, VS Code, MCP, or Jira."""
|
|
152
|
+
if not _MODE_ID_RE.fullmatch(_require_text(self.id, "id")):
|
|
153
|
+
raise FixModeError(
|
|
154
|
+
"Fix Mode id must start with a lowercase letter and contain only "
|
|
155
|
+
"lowercase letters, digits, or hyphens (max 64 characters)."
|
|
156
|
+
)
|
|
157
|
+
_require_text(self.name, "name")
|
|
158
|
+
_require_document_safe_text(self.description, "description")
|
|
159
|
+
_require_choice(self.source, "source", _VALID_SOURCES)
|
|
160
|
+
_require_choice(self.execution_kind, "execution_kind", _VALID_EXECUTION_KINDS)
|
|
161
|
+
_require_positive_int(self.version, "version")
|
|
162
|
+
if self.based_on is not None:
|
|
163
|
+
if not _MODE_ID_RE.fullmatch(_require_text(self.based_on, "based_on")):
|
|
164
|
+
raise FixModeError(f"Invalid based_on Fix Mode id: {self.based_on!r}.")
|
|
165
|
+
if self.based_on == self.id:
|
|
166
|
+
# Whether based_on names a mode that exists is the registry's
|
|
167
|
+
# question; it needs the whole mode set, which this does not have.
|
|
168
|
+
# Descent from itself needs no such context to be wrong.
|
|
169
|
+
raise FixModeError(
|
|
170
|
+
f"Fix Mode based_on must not equal its own id ({self.id!r})."
|
|
171
|
+
)
|
|
172
|
+
if self.based_on_version is not None:
|
|
173
|
+
_require_positive_int(self.based_on_version, "based_on_version")
|
|
174
|
+
if self.based_on is None:
|
|
175
|
+
# A version with no origin cannot be checked against anything,
|
|
176
|
+
# which is the one thing the field exists to allow.
|
|
177
|
+
raise FixModeError(
|
|
178
|
+
"Fix Mode based_on_version requires based_on to be set."
|
|
179
|
+
)
|
|
180
|
+
for section_name in _SECTION_NAMES:
|
|
181
|
+
_require_document_safe_text(getattr(self, section_name), section_name)
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def is_investigation(self) -> bool:
|
|
185
|
+
"""True when no source change is expected in this mode's initial pass."""
|
|
186
|
+
return self.execution_kind == "investigate"
|
|
187
|
+
|
|
188
|
+
def instruction_sections(self) -> tuple[tuple[str, str], ...]:
|
|
189
|
+
"""Return editable instruction sections in stable rendering order."""
|
|
190
|
+
return tuple((name, getattr(self, name)) for name in _SECTION_NAMES)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
STANDARD_FIX = FixMode(
|
|
194
|
+
id="standard",
|
|
195
|
+
name="Standard Fix",
|
|
196
|
+
description="Default workflow for most bugs: analyze, fix minimally, verify, summarize.",
|
|
197
|
+
objective="Resolve the reported issue with the smallest correct change.",
|
|
198
|
+
investigation=(
|
|
199
|
+
"Read the supplied BugPilot context and relevant code before editing. "
|
|
200
|
+
"Identify the most likely root cause and verify it against the available evidence. "
|
|
201
|
+
"If important evidence is missing, state the assumption or request follow-up information."
|
|
202
|
+
),
|
|
203
|
+
implementation=(
|
|
204
|
+
"Implement the smallest safe fix that addresses the root cause. Follow nearby project "
|
|
205
|
+
"patterns and avoid unrelated refactoring, formatting-only changes, or scope expansion."
|
|
206
|
+
),
|
|
207
|
+
verification=(
|
|
208
|
+
"Run the most relevant focused tests or checks that are available. Review the final diff "
|
|
209
|
+
"for unintended changes. If a useful test cannot be run, explain why."
|
|
210
|
+
),
|
|
211
|
+
constraints=(
|
|
212
|
+
"Do not make speculative changes. Avoid public API changes or new dependencies unless "
|
|
213
|
+
"they are required for the fix."
|
|
214
|
+
),
|
|
215
|
+
completion=(
|
|
216
|
+
"Summarize the root cause, files changed, fix, verification performed, remaining risks, "
|
|
217
|
+
"and any follow-up information still needed."
|
|
218
|
+
),
|
|
219
|
+
execution_kind="fix",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
CONSERVATIVE_FIX = FixMode(
|
|
223
|
+
id="conservative",
|
|
224
|
+
name="Conservative Fix",
|
|
225
|
+
description="Minimal, low-risk changes for legacy or sensitive code.",
|
|
226
|
+
objective="Resolve the issue while preserving existing behavior outside the reported scope.",
|
|
227
|
+
investigation=(
|
|
228
|
+
"Understand the root cause before modifying code. Work from the evidence BugPilot "
|
|
229
|
+
"supplied, and review the Git/history evidence it includes when existing behavior may "
|
|
230
|
+
"be intentional. If historical intent matters but the supplied evidence is insufficient, "
|
|
231
|
+
"report the missing evidence instead of performing broad exploration. If the evidence is "
|
|
232
|
+
"not strong enough for a safe fix, stop and explain what additional information is required."
|
|
233
|
+
),
|
|
234
|
+
implementation=(
|
|
235
|
+
"Prefer the smallest local change possible. Minimize changed files and changed lines, "
|
|
236
|
+
"preserve existing architecture, and follow the surrounding implementation style."
|
|
237
|
+
),
|
|
238
|
+
verification=(
|
|
239
|
+
"Run focused regression checks around the changed behavior and review the diff carefully "
|
|
240
|
+
"for side effects. Document any verification that could not be completed."
|
|
241
|
+
),
|
|
242
|
+
constraints=(
|
|
243
|
+
"Do not perform unrelated cleanup, modernization, broad refactoring, public API changes, "
|
|
244
|
+
"or dependency changes unless they are strictly required by the bug."
|
|
245
|
+
),
|
|
246
|
+
completion=(
|
|
247
|
+
"Report the root cause, why the chosen fix is the minimum safe change, files changed, "
|
|
248
|
+
"verification, regression risks, and unresolved uncertainty."
|
|
249
|
+
),
|
|
250
|
+
execution_kind="fix",
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
INVESTIGATE_FIRST_FIX = FixMode(
|
|
254
|
+
id="investigate-first",
|
|
255
|
+
name="Investigate First",
|
|
256
|
+
description="Diagnose and build an evidence-backed fix plan before source modification.",
|
|
257
|
+
objective="Establish a defensible root cause and fix plan before changing source code.",
|
|
258
|
+
investigation=(
|
|
259
|
+
"Do not modify source code in the initial pass. Analyze the supplied evidence, inspect the "
|
|
260
|
+
"likely execution path, identify candidate root causes, rank the hypotheses, and record what "
|
|
261
|
+
"evidence supports or weakens each one. Identify any additional evidence needed."
|
|
262
|
+
),
|
|
263
|
+
implementation=(
|
|
264
|
+
"Produce a concrete proposed fix plan rather than editing source code. Name the likely files "
|
|
265
|
+
"or symbols to change and describe the smallest expected implementation."
|
|
266
|
+
),
|
|
267
|
+
verification=(
|
|
268
|
+
"Define how the proposed fix should be verified, including focused automated tests, manual "
|
|
269
|
+
"reproduction, or diagnostic checks where applicable. Do not report verification as performed: "
|
|
270
|
+
"implementation has not started."
|
|
271
|
+
),
|
|
272
|
+
constraints=(
|
|
273
|
+
"Do not guess a fix merely because a file matches keywords. Do not change source code until "
|
|
274
|
+
"the developer explicitly continues from the investigation result."
|
|
275
|
+
),
|
|
276
|
+
completion=(
|
|
277
|
+
"Summarize the leading root-cause hypothesis, supporting evidence, competing hypotheses, "
|
|
278
|
+
"missing information, proposed fix plan, and proposed verification. State plainly that no "
|
|
279
|
+
"source change was applied, then stop and ask the developer whether to implement."
|
|
280
|
+
),
|
|
281
|
+
execution_kind="investigate",
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
TEST_DRIVEN_FIX = FixMode(
|
|
285
|
+
id="test-driven",
|
|
286
|
+
name="Test-Driven Fix",
|
|
287
|
+
description="Reproduce with a focused test, fix the cause, then rerun verification.",
|
|
288
|
+
objective="Turn the reported behavior into a focused regression check and make that check pass.",
|
|
289
|
+
investigation=(
|
|
290
|
+
"Confirm the expected behavior from the supplied context and find the narrowest practical "
|
|
291
|
+
"test level that can reproduce the failure. Understand the root cause before broad changes."
|
|
292
|
+
),
|
|
293
|
+
implementation=(
|
|
294
|
+
"Add or update a focused failing regression test when practical, then implement the smallest "
|
|
295
|
+
"production-code change that makes the test pass without weakening existing assertions."
|
|
296
|
+
),
|
|
297
|
+
verification=(
|
|
298
|
+
"Run the new or updated regression test before and after the fix when practical, then run the "
|
|
299
|
+
"nearest relevant existing tests. Record commands and results accurately."
|
|
300
|
+
),
|
|
301
|
+
constraints=(
|
|
302
|
+
"Do not rewrite large test areas just to enable the fix. Do not weaken tests to make a failure "
|
|
303
|
+
"disappear. If a reliable automated reproduction is not practical, explain that and use the "
|
|
304
|
+
"best focused verification available."
|
|
305
|
+
),
|
|
306
|
+
completion=(
|
|
307
|
+
"Summarize the reproduced failure, root cause, test change, production fix, verification "
|
|
308
|
+
"results, and remaining regression risk."
|
|
309
|
+
),
|
|
310
|
+
execution_kind="fix",
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
DEEP_ANALYSIS_FIX = FixMode(
|
|
314
|
+
id="deep-analysis",
|
|
315
|
+
name="Deep Analysis",
|
|
316
|
+
description="Deeper evidence review for complex crashes, regressions, or cross-module bugs.",
|
|
317
|
+
objective="Build a high-confidence explanation of the failure before selecting the safest fix.",
|
|
318
|
+
investigation=(
|
|
319
|
+
"Trace the relevant execution/data path across implicated modules and inspect the related "
|
|
320
|
+
"code the evidence points to. Prefer evidence over broad repository exploration: expand only "
|
|
321
|
+
"when the supplied evidence identifies a concrete dependency or missing link that must be "
|
|
322
|
+
"inspected. Review the Git/history evidence supplied by BugPilot when regression boundaries "
|
|
323
|
+
"or coupled changes may matter. Compare competing root-cause hypotheses against that "
|
|
324
|
+
"evidence. If historical intent matters but the supplied evidence is insufficient, report "
|
|
325
|
+
"the missing evidence instead of performing broad exploration."
|
|
326
|
+
),
|
|
327
|
+
implementation=(
|
|
328
|
+
"After the analysis supports a root cause, implement the narrowest change that corrects it. "
|
|
329
|
+
"If multiple modules are involved, explain why each changed location is necessary."
|
|
330
|
+
),
|
|
331
|
+
verification=(
|
|
332
|
+
"Run focused tests across the affected path and relevant boundary conditions. Review the diff "
|
|
333
|
+
"and check likely regression areas identified during analysis."
|
|
334
|
+
),
|
|
335
|
+
constraints=(
|
|
336
|
+
"Depth of investigation does not authorize broad refactoring. Keep the implementation scoped "
|
|
337
|
+
"to the evidence-backed root cause and clearly label uncertainty."
|
|
338
|
+
),
|
|
339
|
+
completion=(
|
|
340
|
+
"Summarize the execution path, root cause, evidence considered, changes made, verification, "
|
|
341
|
+
"regression risks, and any remaining uncertainty."
|
|
342
|
+
),
|
|
343
|
+
execution_kind="fix",
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
BUILTIN_FIX_MODES: tuple[FixMode, ...] = (
|
|
347
|
+
STANDARD_FIX,
|
|
348
|
+
CONSERVATIVE_FIX,
|
|
349
|
+
INVESTIGATE_FIRST_FIX,
|
|
350
|
+
TEST_DRIVEN_FIX,
|
|
351
|
+
DEEP_ANALYSIS_FIX,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class FixModeRegistry:
|
|
356
|
+
"""Validated lookup table shared by CLI, VS Code, MCP, and task generation."""
|
|
357
|
+
|
|
358
|
+
def __init__(self, modes: Iterable[FixMode] = BUILTIN_FIX_MODES) -> None:
|
|
359
|
+
self._modes: dict[str, FixMode] = {}
|
|
360
|
+
for mode in modes:
|
|
361
|
+
self.register(mode)
|
|
362
|
+
if DEFAULT_FIX_MODE_ID not in self._modes:
|
|
363
|
+
raise FixModeError(
|
|
364
|
+
f"Fix Mode registry must contain default mode {DEFAULT_FIX_MODE_ID!r}."
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
def register(self, mode: FixMode) -> None:
|
|
368
|
+
if not isinstance(mode, FixMode):
|
|
369
|
+
raise FixModeError(
|
|
370
|
+
f"Fix Mode registry accepts FixMode objects, got {type(mode).__name__}."
|
|
371
|
+
)
|
|
372
|
+
mode.validate()
|
|
373
|
+
if mode.id in self._modes:
|
|
374
|
+
raise FixModeError(f"Duplicate Fix Mode id: {mode.id!r}.")
|
|
375
|
+
self._modes[mode.id] = mode
|
|
376
|
+
|
|
377
|
+
def resolve(self, mode_id: str | None = None) -> FixMode:
|
|
378
|
+
"""Resolve an exact ID; only ``None`` selects the deterministic default.
|
|
379
|
+
|
|
380
|
+
This is the only place an ID becomes a mode. Task generation is handed
|
|
381
|
+
the resolved object instead, so that user- and project-scoped modes can
|
|
382
|
+
be layered in here later without the renderer learning about config
|
|
383
|
+
files, repository roots, or scope precedence.
|
|
384
|
+
"""
|
|
385
|
+
requested = DEFAULT_FIX_MODE_ID if mode_id is None else mode_id
|
|
386
|
+
if not isinstance(requested, str):
|
|
387
|
+
raise FixModeError(
|
|
388
|
+
f"Fix Mode id must be a string or None, got {type(mode_id).__name__}."
|
|
389
|
+
)
|
|
390
|
+
try:
|
|
391
|
+
return self._modes[requested]
|
|
392
|
+
except KeyError as exc:
|
|
393
|
+
available = ", ".join(self._modes)
|
|
394
|
+
raise FixModeNotFoundError(
|
|
395
|
+
f"Unknown Fix Mode {requested!r}. Available modes: {available}."
|
|
396
|
+
) from exc
|
|
397
|
+
|
|
398
|
+
def list_modes(self, *, source: FixModeSource | None = None) -> tuple[FixMode, ...]:
|
|
399
|
+
modes = tuple(self._modes.values())
|
|
400
|
+
if source is None:
|
|
401
|
+
return modes
|
|
402
|
+
_require_choice(source, "source", _VALID_SOURCES)
|
|
403
|
+
return tuple(mode for mode in modes if mode.source == source)
|
|
404
|
+
|
|
405
|
+
@property
|
|
406
|
+
def default(self) -> FixMode:
|
|
407
|
+
return self.resolve()
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def builtin_fix_mode_registry() -> FixModeRegistry:
|
|
411
|
+
"""Return a fresh registry containing exactly BugPilot's packaged modes."""
|
|
412
|
+
return FixModeRegistry(BUILTIN_FIX_MODES)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""fix_report.md: the one post-agent workflow report (plan §37).
|
|
2
|
+
|
|
3
|
+
Five agent-written files used to carry the outcome — analysis, fix summary,
|
|
4
|
+
test result, diff summary, review notes — and BugPilot then derived four more
|
|
5
|
+
from them. One report remains, and the derivations (the Jira comment draft,
|
|
6
|
+
the notification email, the memory entry's Final Result, the validation
|
|
7
|
+
checklist, the retry prompt's previous-attempt summary) are rendered from it
|
|
8
|
+
in memory.
|
|
9
|
+
|
|
10
|
+
The report is **agent-owned**: the coding agent writes and updates it per
|
|
11
|
+
``task.md``'s Required Output contract, and BugPilot never rewrites it. The
|
|
12
|
+
one BugPilot writer is the ``manual-result`` template, for a developer who
|
|
13
|
+
fixed by hand, and it refuses to overwrite an existing report unless asked.
|
|
14
|
+
|
|
15
|
+
The name means "post-agent workflow report", not "confirmed fix". An
|
|
16
|
+
investigation-only mode fills the same sections with investigation state, and
|
|
17
|
+
``## Summary`` says honestly what happened: fixed, investigation only, no-op,
|
|
18
|
+
or an attempt that did not finish.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from .artifacts import FIX_REPORT_ARTIFACT
|
|
27
|
+
from .config import issue_dir
|
|
28
|
+
|
|
29
|
+
# The section contract task.md asks the agent for, in this order.
|
|
30
|
+
FIX_REPORT_SECTIONS: tuple[str, ...] = (
|
|
31
|
+
"## Summary",
|
|
32
|
+
"## Analysis",
|
|
33
|
+
"## Changes",
|
|
34
|
+
"## Tests",
|
|
35
|
+
"## Review Notes",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class FixReport:
|
|
41
|
+
"""The report as written, plus its sections. Empty string for a missing one."""
|
|
42
|
+
|
|
43
|
+
text: str
|
|
44
|
+
summary: str
|
|
45
|
+
analysis: str
|
|
46
|
+
changes: str
|
|
47
|
+
tests: str
|
|
48
|
+
review_notes: str
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def missing_sections(self) -> tuple[str, ...]:
|
|
52
|
+
by_heading = {
|
|
53
|
+
"## Summary": self.summary,
|
|
54
|
+
"## Analysis": self.analysis,
|
|
55
|
+
"## Changes": self.changes,
|
|
56
|
+
"## Tests": self.tests,
|
|
57
|
+
"## Review Notes": self.review_notes,
|
|
58
|
+
}
|
|
59
|
+
return tuple(heading for heading in FIX_REPORT_SECTIONS if not by_heading[heading])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def fix_report_path(repo_root: Path, work_item_id: str) -> Path:
|
|
63
|
+
return issue_dir(repo_root, work_item_id) / FIX_REPORT_ARTIFACT
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def read_fix_report(repo_root: Path, work_item_id: str) -> FixReport | None:
|
|
67
|
+
"""The agent's report, or ``None`` when nothing has written one yet.
|
|
68
|
+
|
|
69
|
+
Markdown a model wrote, read tolerantly: a missing heading is an empty
|
|
70
|
+
section, never an error — the readers all render "not available" for it.
|
|
71
|
+
"""
|
|
72
|
+
path = fix_report_path(repo_root, work_item_id)
|
|
73
|
+
if not path.exists():
|
|
74
|
+
return None
|
|
75
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
76
|
+
return FixReport(
|
|
77
|
+
text=text,
|
|
78
|
+
summary=section_of(text, "## Summary"),
|
|
79
|
+
analysis=section_of(text, "## Analysis"),
|
|
80
|
+
changes=section_of(text, "## Changes"),
|
|
81
|
+
tests=section_of(text, "## Tests"),
|
|
82
|
+
review_notes=section_of(text, "## Review Notes"),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def section_of(markdown: str, heading: str) -> str:
|
|
87
|
+
"""The body under a ``##`` heading, up to the next one. ``###`` stays inside.
|
|
88
|
+
|
|
89
|
+
Matched with trailing whitespace and case forgiven: the report is written by
|
|
90
|
+
a model, and ``## summary `` failing to parse would read as a missing
|
|
91
|
+
section everywhere downstream — safe, but a sharp edge for no gain.
|
|
92
|
+
"""
|
|
93
|
+
wanted = heading.strip().lower()
|
|
94
|
+
lines = markdown.splitlines()
|
|
95
|
+
start = next(
|
|
96
|
+
(index + 1 for index, line in enumerate(lines) if line.strip().lower() == wanted),
|
|
97
|
+
None,
|
|
98
|
+
)
|
|
99
|
+
if start is None:
|
|
100
|
+
return ""
|
|
101
|
+
collected: list[str] = []
|
|
102
|
+
for line in lines[start:]:
|
|
103
|
+
if line.startswith("## "):
|
|
104
|
+
break
|
|
105
|
+
collected.append(line)
|
|
106
|
+
return "\n".join(collected).strip()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def manual_fix_report_template(work_item_id: str) -> str:
|
|
110
|
+
"""The report skeleton for a developer who fixed the bug by hand."""
|
|
111
|
+
return (
|
|
112
|
+
f"# Fix Report: {work_item_id}\n\n"
|
|
113
|
+
"## Summary\n\n"
|
|
114
|
+
"Developer manual fix. TODO: one line on what was done and the outcome.\n\n"
|
|
115
|
+
"## Analysis\n\n"
|
|
116
|
+
"TODO: the root cause and the evidence for it.\n\n"
|
|
117
|
+
"## Changes\n\n"
|
|
118
|
+
"TODO: what changed, the files touched, and a short diff summary.\n\n"
|
|
119
|
+
"## Tests\n\n"
|
|
120
|
+
"TODO: the commands run and their outcomes — or state that tests were "
|
|
121
|
+
"not run, and why.\n\n"
|
|
122
|
+
"## Review Notes\n\n"
|
|
123
|
+
"TODO: risks, open questions, and the recommended next step.\n"
|
|
124
|
+
)
|