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,212 @@
|
|
|
1
|
+
"""The Fix Mode selected for one work item: choosing it, and remembering it.
|
|
2
|
+
|
|
3
|
+
The selection is persisted in `.ai/<work item>/issue.json`, as
|
|
4
|
+
`guidance.fix_mode`, beside the hint the same run used. That is generated
|
|
5
|
+
runtime state, not team policy — `.bugpilot/` is where team-owned mode
|
|
6
|
+
definitions live, and mixing the two would make a run's own bookkeeping look
|
|
7
|
+
like something a developer should edit and commit.
|
|
8
|
+
|
|
9
|
+
**Only the id selects a mode.** Everything else in the record is audit metadata:
|
|
10
|
+
what the package was generated with, so a report's Review Notes read months later
|
|
11
|
+
still says which workflow the agent was given. A name or an instruction copied
|
|
12
|
+
out of a stale record must never become the workflow, so the id is re-resolved
|
|
13
|
+
through the registry on every read. That is also what makes a mode which has
|
|
14
|
+
since disappeared fail loudly instead of quietly becoming Standard Fix — the
|
|
15
|
+
case that matters once modes can come from a project directory.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass, replace
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Literal
|
|
23
|
+
|
|
24
|
+
from .artifacts import ISSUE_ARTIFACT
|
|
25
|
+
from .fix_mode_store import effective_registry_for
|
|
26
|
+
from .fix_modes import (
|
|
27
|
+
FixMode,
|
|
28
|
+
FixModeError,
|
|
29
|
+
FixModeNotFoundError,
|
|
30
|
+
FixModeRegistry,
|
|
31
|
+
)
|
|
32
|
+
from .issue import IssueArtifact, IssueArtifactError, IssueGuidance, jira_stub, load_issue, save_issue
|
|
33
|
+
|
|
34
|
+
_METADATA_KEYS: tuple[str, ...] = (
|
|
35
|
+
"id",
|
|
36
|
+
"name",
|
|
37
|
+
"version",
|
|
38
|
+
"source",
|
|
39
|
+
"execution_kind",
|
|
40
|
+
"based_on",
|
|
41
|
+
"based_on_version",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
SelectionOrigin = Literal["explicit", "persisted", "default"]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class FixModeSelection:
|
|
49
|
+
"""A resolved mode, where the choice came from, and what to tell the developer."""
|
|
50
|
+
|
|
51
|
+
mode: FixMode
|
|
52
|
+
origin: SelectionOrigin
|
|
53
|
+
warnings: tuple[str, ...] = ()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def fix_mode_registry(repo_root: Path | None = None) -> FixModeRegistry:
|
|
57
|
+
"""The registry every entry point resolves ids through.
|
|
58
|
+
|
|
59
|
+
Built-ins plus the developer's own modes plus the repository's, with project
|
|
60
|
+
beating user for a custom id and built-ins reserved. One function, so
|
|
61
|
+
nothing downstream learns about scopes or config paths: callers hand an id
|
|
62
|
+
in and get a resolved `FixMode` back.
|
|
63
|
+
|
|
64
|
+
`repo_root` is explicit rather than taken from the process's directory,
|
|
65
|
+
because "which repository" is the caller's question — a command run from a
|
|
66
|
+
nested directory, a VS Code workspace and a test in a temporary directory
|
|
67
|
+
all answer it differently. Without a root only built-ins and the
|
|
68
|
+
developer's own modes are available; a project has none to offer.
|
|
69
|
+
|
|
70
|
+
Rebuilt on every call: these files are meant to be edited and committed, so
|
|
71
|
+
a cached registry would answer with the content of the last run.
|
|
72
|
+
"""
|
|
73
|
+
return effective_registry_for(repo_root)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def fix_mode_metadata(mode: FixMode) -> dict[str, object]:
|
|
77
|
+
"""The audit record of which mode produced a package.
|
|
78
|
+
|
|
79
|
+
One shape for all three consumers — the persisted file, the status file and
|
|
80
|
+
the `--json` envelope — because a consumer that learns to read it in one
|
|
81
|
+
place should not have to learn a second spelling for the next.
|
|
82
|
+
"""
|
|
83
|
+
return {key: getattr(mode, key) for key in _METADATA_KEYS}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def select_fix_mode(
|
|
87
|
+
repo_root: Path,
|
|
88
|
+
work_item_id: str,
|
|
89
|
+
requested_id: str | None = None,
|
|
90
|
+
*,
|
|
91
|
+
use_persisted: bool = True,
|
|
92
|
+
registry: FixModeRegistry | None = None,
|
|
93
|
+
) -> FixModeSelection:
|
|
94
|
+
"""Explicit selection, else the persisted one, else Standard Fix.
|
|
95
|
+
|
|
96
|
+
Reads; never writes. `use_persisted=False` is for a fresh run, which is
|
|
97
|
+
about to delete the previous package: the mode recorded there belongs to the
|
|
98
|
+
run being discarded, and resolving before the deletion means a mistyped
|
|
99
|
+
`--fix-mode` costs nothing.
|
|
100
|
+
"""
|
|
101
|
+
registry = registry or fix_mode_registry(repo_root)
|
|
102
|
+
if requested_id is not None:
|
|
103
|
+
return FixModeSelection(registry.resolve(requested_id), "explicit")
|
|
104
|
+
stored = _read_state(repo_root, work_item_id) if use_persisted else None
|
|
105
|
+
if stored is None:
|
|
106
|
+
return FixModeSelection(registry.resolve(None), "default")
|
|
107
|
+
return _selection_from_state(stored, registry, repo_root, work_item_id)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def persist_fix_mode(
|
|
111
|
+
repo_root: Path,
|
|
112
|
+
work_item_id: str,
|
|
113
|
+
mode: FixMode,
|
|
114
|
+
issue: IssueArtifact | None = None,
|
|
115
|
+
) -> IssueArtifact:
|
|
116
|
+
"""Record the selection in ``issue.json`` for regeneration, resume, refine and retry.
|
|
117
|
+
|
|
118
|
+
``issue`` is the caller's in-memory issue, so recording the mode and the
|
|
119
|
+
rest of the issue is one write. Without it the persisted issue is updated;
|
|
120
|
+
a work item with none yet gets a Jira stub, the only kind that can exist
|
|
121
|
+
before its content does. Returns what was written.
|
|
122
|
+
"""
|
|
123
|
+
if issue is None:
|
|
124
|
+
issue = load_issue(repo_root, work_item_id) or jira_stub(work_item_id, IssueGuidance())
|
|
125
|
+
recorded = issue.with_guidance(replace(issue.guidance, fix_mode=fix_mode_metadata(mode)))
|
|
126
|
+
save_issue(repo_root, recorded)
|
|
127
|
+
return recorded
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def stored_fix_mode_metadata(repo_root: Path, work_item_id: str) -> dict[str, object] | None:
|
|
131
|
+
"""What was recorded, for the status file. Never raises.
|
|
132
|
+
|
|
133
|
+
Status writing runs on the failure path too, so an unreadable issue file
|
|
134
|
+
must not be the reason a run cannot report what it did.
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
stored = _read_state(repo_root, work_item_id)
|
|
138
|
+
except FixModeError:
|
|
139
|
+
return None
|
|
140
|
+
if not stored:
|
|
141
|
+
return None
|
|
142
|
+
return {key: stored.get(key) for key in _METADATA_KEYS}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _read_state(repo_root: Path, work_item_id: str) -> dict[str, object] | None:
|
|
146
|
+
"""The persisted selection as raw data, or None when there is none."""
|
|
147
|
+
try:
|
|
148
|
+
issue = load_issue(repo_root, work_item_id)
|
|
149
|
+
except IssueArtifactError as exc:
|
|
150
|
+
raise FixModeError(
|
|
151
|
+
f"{exc} Choose a mode explicitly with --fix-mode, or re-prepare the work item."
|
|
152
|
+
) from exc
|
|
153
|
+
if issue is None or issue.guidance.fix_mode is None:
|
|
154
|
+
return None
|
|
155
|
+
return dict(issue.guidance.fix_mode)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _selection_from_state(
|
|
159
|
+
stored: dict[str, object],
|
|
160
|
+
registry: FixModeRegistry,
|
|
161
|
+
repo_root: Path,
|
|
162
|
+
work_item_id: str,
|
|
163
|
+
) -> FixModeSelection:
|
|
164
|
+
stored_id = stored.get("id")
|
|
165
|
+
if not isinstance(stored_id, str) or not stored_id.strip():
|
|
166
|
+
raise FixModeError(
|
|
167
|
+
f"{_display_path(work_item_id)} does not record a Fix Mode id. "
|
|
168
|
+
"Choose a mode explicitly with --fix-mode, or re-prepare the work item."
|
|
169
|
+
)
|
|
170
|
+
try:
|
|
171
|
+
mode = registry.resolve(stored_id)
|
|
172
|
+
except FixModeNotFoundError as exc:
|
|
173
|
+
raise FixModeNotFoundError(
|
|
174
|
+
f"Stored Fix Mode {stored_id!r} cannot be resolved. Choose another mode "
|
|
175
|
+
"explicitly with --fix-mode or restore the mode definition."
|
|
176
|
+
) from exc
|
|
177
|
+
return FixModeSelection(mode, "persisted", _drift_warnings(stored, mode))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _drift_warnings(stored: dict[str, object], mode: FixMode) -> tuple[str, ...]:
|
|
181
|
+
"""Say so when the definition behind an id is no longer the one that ran.
|
|
182
|
+
|
|
183
|
+
Two ways that happens, and the second is the one custom modes introduce: the
|
|
184
|
+
recorded mode was edited (a new version), or another scope started answering
|
|
185
|
+
to the same id — a project mode appearing beside a developer's own, or being
|
|
186
|
+
deleted again. Only the id is authority, so re-resolution is correct; doing
|
|
187
|
+
it *silently* would swap a personal workflow for a team one, or back, with
|
|
188
|
+
nothing on screen to say so.
|
|
189
|
+
|
|
190
|
+
BugPilot regenerates from the current definition rather than replaying an old
|
|
191
|
+
one, the same compatibility model as every other artifact here. Pinning a
|
|
192
|
+
mode to a snapshot is a later decision, not this phase's.
|
|
193
|
+
"""
|
|
194
|
+
changes: list[str] = []
|
|
195
|
+
stored_source = stored.get("source")
|
|
196
|
+
if isinstance(stored_source, str) and stored_source and stored_source != mode.source:
|
|
197
|
+
changes.append(f"source: {stored_source} -> {mode.source}")
|
|
198
|
+
stored_version = stored.get("version")
|
|
199
|
+
if isinstance(stored_version, int) and not isinstance(stored_version, bool):
|
|
200
|
+
if stored_version != mode.version:
|
|
201
|
+
changes.append(f"version: {stored_version} -> {mode.version}")
|
|
202
|
+
if not changes:
|
|
203
|
+
return ()
|
|
204
|
+
return (
|
|
205
|
+
f"Fix Mode {mode.id!r} changed since this work item was prepared: "
|
|
206
|
+
+ "; ".join(changes)
|
|
207
|
+
+ ". Regenerating with the current effective definition.",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _display_path(work_item_id: str) -> str:
|
|
212
|
+
return f".ai/{work_item_id}/{ISSUE_ARTIFACT}"
|