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.
Files changed (59) hide show
  1. bugpilot/__init__.py +5 -0
  2. bugpilot/__main__.py +5 -0
  3. bugpilot/cli.py +2615 -0
  4. bugpilot/cli_json.py +173 -0
  5. bugpilot/core/__init__.py +1 -0
  6. bugpilot/core/agent_runner.py +138 -0
  7. bugpilot/core/artifact_io.py +40 -0
  8. bugpilot/core/artifacts.py +47 -0
  9. bugpilot/core/attachments.py +247 -0
  10. bugpilot/core/branch_policy.py +223 -0
  11. bugpilot/core/cleanup.py +58 -0
  12. bugpilot/core/code_files.py +99 -0
  13. bugpilot/core/config.py +242 -0
  14. bugpilot/core/context.py +436 -0
  15. bugpilot/core/copilot.py +47 -0
  16. bugpilot/core/delivery_instructions.py +106 -0
  17. bugpilot/core/doctor.py +66 -0
  18. bugpilot/core/email_notify.py +316 -0
  19. bugpilot/core/errors.py +105 -0
  20. bugpilot/core/executables.py +87 -0
  21. bugpilot/core/fix_mode_state.py +212 -0
  22. bugpilot/core/fix_mode_store.py +600 -0
  23. bugpilot/core/fix_modes.py +412 -0
  24. bugpilot/core/fix_report.py +124 -0
  25. bugpilot/core/git_history.py +1579 -0
  26. bugpilot/core/git_ops.py +254 -0
  27. bugpilot/core/handoff.py +127 -0
  28. bugpilot/core/identity.py +112 -0
  29. bugpilot/core/input_adapters.py +97 -0
  30. bugpilot/core/instructions.py +337 -0
  31. bugpilot/core/issue.py +487 -0
  32. bugpilot/core/jira.py +886 -0
  33. bugpilot/core/jira_adf.py +167 -0
  34. bugpilot/core/jira_parse.py +442 -0
  35. bugpilot/core/keywords.py +386 -0
  36. bugpilot/core/logging_utils.py +28 -0
  37. bugpilot/core/memory.py +151 -0
  38. bugpilot/core/models.py +322 -0
  39. bugpilot/core/project_settings.py +283 -0
  40. bugpilot/core/prompts.py +567 -0
  41. bugpilot/core/repository_profile.py +739 -0
  42. bugpilot/core/retrieval.py +485 -0
  43. bugpilot/core/review_changes.py +155 -0
  44. bugpilot/core/review_report.py +171 -0
  45. bugpilot/core/run.py +171 -0
  46. bugpilot/core/safe_paths.py +173 -0
  47. bugpilot/core/search.py +765 -0
  48. bugpilot/core/search_terms.py +310 -0
  49. bugpilot/core/setup.py +212 -0
  50. bugpilot/core/user_config.py +211 -0
  51. bugpilot/core/verification_report.py +313 -0
  52. bugpilot/core/workflow.py +2385 -0
  53. bugpilot/mcp_server.py +651 -0
  54. bugpilot-0.1.0.dist-info/METADATA +270 -0
  55. bugpilot-0.1.0.dist-info/RECORD +59 -0
  56. bugpilot-0.1.0.dist-info/WHEEL +5 -0
  57. bugpilot-0.1.0.dist-info/entry_points.txt +3 -0
  58. bugpilot-0.1.0.dist-info/licenses/LICENSE +122 -0
  59. 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}"