pdd-cli 0.0.90__py3-none-any.whl → 0.0.118__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.
- pdd/__init__.py +38 -6
- pdd/agentic_bug.py +323 -0
- pdd/agentic_bug_orchestrator.py +497 -0
- pdd/agentic_change.py +231 -0
- pdd/agentic_change_orchestrator.py +526 -0
- pdd/agentic_common.py +521 -786
- pdd/agentic_e2e_fix.py +319 -0
- pdd/agentic_e2e_fix_orchestrator.py +426 -0
- pdd/agentic_fix.py +118 -3
- pdd/agentic_update.py +25 -8
- pdd/architecture_sync.py +565 -0
- pdd/auth_service.py +210 -0
- pdd/auto_deps_main.py +63 -53
- pdd/auto_include.py +185 -3
- pdd/auto_update.py +125 -47
- pdd/bug_main.py +195 -23
- pdd/cmd_test_main.py +345 -197
- pdd/code_generator.py +4 -2
- pdd/code_generator_main.py +118 -32
- pdd/commands/__init__.py +6 -0
- pdd/commands/analysis.py +87 -29
- pdd/commands/auth.py +309 -0
- pdd/commands/connect.py +290 -0
- pdd/commands/fix.py +136 -113
- pdd/commands/maintenance.py +3 -2
- pdd/commands/misc.py +8 -0
- pdd/commands/modify.py +190 -164
- pdd/commands/sessions.py +284 -0
- pdd/construct_paths.py +334 -32
- pdd/context_generator_main.py +167 -170
- pdd/continue_generation.py +6 -3
- pdd/core/__init__.py +33 -0
- pdd/core/cli.py +27 -3
- pdd/core/cloud.py +237 -0
- pdd/core/errors.py +4 -0
- pdd/core/remote_session.py +61 -0
- pdd/crash_main.py +219 -23
- pdd/data/llm_model.csv +4 -4
- pdd/docs/prompting_guide.md +864 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
- pdd/fix_code_loop.py +208 -34
- pdd/fix_code_module_errors.py +6 -2
- pdd/fix_error_loop.py +291 -38
- pdd/fix_main.py +204 -4
- pdd/fix_verification_errors_loop.py +235 -26
- pdd/fix_verification_main.py +269 -83
- pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
- pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
- pdd/frontend/dist/index.html +376 -0
- pdd/frontend/dist/logo.svg +33 -0
- pdd/generate_output_paths.py +46 -5
- pdd/generate_test.py +212 -151
- pdd/get_comment.py +19 -44
- pdd/get_extension.py +8 -9
- pdd/get_jwt_token.py +309 -20
- pdd/get_language.py +8 -7
- pdd/get_run_command.py +7 -5
- pdd/insert_includes.py +2 -1
- pdd/llm_invoke.py +459 -95
- pdd/load_prompt_template.py +15 -34
- pdd/path_resolution.py +140 -0
- pdd/postprocess.py +4 -1
- pdd/preprocess.py +68 -12
- pdd/preprocess_main.py +33 -1
- pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
- pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
- pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
- pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
- pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
- pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
- pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
- pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
- pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
- pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
- pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
- pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
- pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
- pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
- pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
- pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
- pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
- pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
- pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
- pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
- pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
- pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
- pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
- pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
- pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
- pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
- pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
- pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
- pdd/prompts/agentic_update_LLM.prompt +192 -338
- pdd/prompts/auto_include_LLM.prompt +22 -0
- pdd/prompts/change_LLM.prompt +3093 -1
- pdd/prompts/detect_change_LLM.prompt +571 -14
- pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
- pdd/prompts/generate_test_LLM.prompt +20 -1
- pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
- pdd/prompts/insert_includes_LLM.prompt +262 -252
- pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
- pdd/prompts/prompt_diff_LLM.prompt +82 -0
- pdd/remote_session.py +876 -0
- pdd/server/__init__.py +52 -0
- pdd/server/app.py +335 -0
- pdd/server/click_executor.py +587 -0
- pdd/server/executor.py +338 -0
- pdd/server/jobs.py +661 -0
- pdd/server/models.py +241 -0
- pdd/server/routes/__init__.py +31 -0
- pdd/server/routes/architecture.py +451 -0
- pdd/server/routes/auth.py +364 -0
- pdd/server/routes/commands.py +929 -0
- pdd/server/routes/config.py +42 -0
- pdd/server/routes/files.py +603 -0
- pdd/server/routes/prompts.py +1322 -0
- pdd/server/routes/websocket.py +473 -0
- pdd/server/security.py +243 -0
- pdd/server/terminal_spawner.py +209 -0
- pdd/server/token_counter.py +222 -0
- pdd/summarize_directory.py +236 -237
- pdd/sync_animation.py +8 -4
- pdd/sync_determine_operation.py +329 -47
- pdd/sync_main.py +272 -28
- pdd/sync_orchestration.py +136 -75
- pdd/template_expander.py +161 -0
- pdd/templates/architecture/architecture_json.prompt +41 -46
- pdd/trace.py +1 -1
- pdd/track_cost.py +0 -13
- pdd/unfinished_prompt.py +2 -1
- pdd/update_main.py +23 -5
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +15 -10
- pdd_cli-0.0.118.dist-info/RECORD +227 -0
- pdd_cli-0.0.90.dist-info/RECORD +0 -153
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Tuple, Optional, Dict, Any
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from .agentic_common import (
|
|
12
|
+
run_agentic_task,
|
|
13
|
+
load_workflow_state,
|
|
14
|
+
save_workflow_state,
|
|
15
|
+
clear_workflow_state
|
|
16
|
+
)
|
|
17
|
+
from .load_prompt_template import load_prompt_template
|
|
18
|
+
|
|
19
|
+
# Initialize console
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
# Per-step timeouts for the 10-step agentic bug workflow (Issue #256)
|
|
23
|
+
# Complex steps (reproduce, root cause, generate, e2e) get more time.
|
|
24
|
+
BUG_STEP_TIMEOUTS: Dict[int, float] = {
|
|
25
|
+
1: 240.0, # Duplicate Check
|
|
26
|
+
2: 400.0, # Docs Check
|
|
27
|
+
3: 400.0, # Triage
|
|
28
|
+
4: 600.0, # Reproduce (Complex)
|
|
29
|
+
5: 600.0, # Root Cause (Complex)
|
|
30
|
+
6: 340.0, # Test Plan
|
|
31
|
+
7: 1000.0, # Generate Unit Test (Most Complex)
|
|
32
|
+
8: 600.0, # Verify Unit Test
|
|
33
|
+
9: 2000.0, # E2E Test (Complex - needs to discover env & run tests)
|
|
34
|
+
10: 240.0, # Create PR
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_git_root(cwd: Path) -> Optional[Path]:
|
|
39
|
+
"""Get the root directory of the git repository."""
|
|
40
|
+
try:
|
|
41
|
+
result = subprocess.run(
|
|
42
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
43
|
+
cwd=cwd,
|
|
44
|
+
capture_output=True,
|
|
45
|
+
text=True,
|
|
46
|
+
check=True
|
|
47
|
+
)
|
|
48
|
+
return Path(result.stdout.strip())
|
|
49
|
+
except subprocess.CalledProcessError:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _get_state_dir(cwd: Path) -> Path:
|
|
54
|
+
"""Return path to state directory relative to git root."""
|
|
55
|
+
root = _get_git_root(cwd) or cwd
|
|
56
|
+
return root / ".pdd" / "bug-state"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _worktree_exists(cwd: Path, worktree_path: Path) -> bool:
|
|
60
|
+
"""Check if a path is a registered git worktree."""
|
|
61
|
+
try:
|
|
62
|
+
result = subprocess.run(
|
|
63
|
+
["git", "worktree", "list", "--porcelain"],
|
|
64
|
+
cwd=cwd,
|
|
65
|
+
capture_output=True,
|
|
66
|
+
text=True,
|
|
67
|
+
check=True
|
|
68
|
+
)
|
|
69
|
+
# The porcelain output lists 'worktree /path/to/worktree'
|
|
70
|
+
# We check if our specific path appears in the output
|
|
71
|
+
return str(worktree_path.resolve()) in result.stdout
|
|
72
|
+
except subprocess.CalledProcessError:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _branch_exists(cwd: Path, branch: str) -> bool:
|
|
77
|
+
"""Check if a local git branch exists."""
|
|
78
|
+
try:
|
|
79
|
+
subprocess.run(
|
|
80
|
+
["git", "show-ref", "--verify", f"refs/heads/{branch}"],
|
|
81
|
+
cwd=cwd,
|
|
82
|
+
capture_output=True,
|
|
83
|
+
check=True
|
|
84
|
+
)
|
|
85
|
+
return True
|
|
86
|
+
except subprocess.CalledProcessError:
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _remove_worktree(cwd: Path, worktree_path: Path) -> Tuple[bool, str]:
|
|
91
|
+
"""Remove a git worktree."""
|
|
92
|
+
try:
|
|
93
|
+
subprocess.run(
|
|
94
|
+
["git", "worktree", "remove", "--force", str(worktree_path)],
|
|
95
|
+
cwd=cwd,
|
|
96
|
+
capture_output=True,
|
|
97
|
+
check=True
|
|
98
|
+
)
|
|
99
|
+
return True, ""
|
|
100
|
+
except subprocess.CalledProcessError as e:
|
|
101
|
+
return False, e.stderr.decode('utf-8')
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _delete_branch(cwd: Path, branch: str) -> Tuple[bool, str]:
|
|
105
|
+
"""Force delete a git branch."""
|
|
106
|
+
try:
|
|
107
|
+
subprocess.run(
|
|
108
|
+
["git", "branch", "-D", branch],
|
|
109
|
+
cwd=cwd,
|
|
110
|
+
capture_output=True,
|
|
111
|
+
check=True
|
|
112
|
+
)
|
|
113
|
+
return True, ""
|
|
114
|
+
except subprocess.CalledProcessError as e:
|
|
115
|
+
return False, e.stderr.decode('utf-8')
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _setup_worktree(cwd: Path, issue_number: int, quiet: bool, resume_existing: bool = False) -> Tuple[Optional[Path], Optional[str]]:
|
|
119
|
+
"""
|
|
120
|
+
Sets up an isolated git worktree for the issue fix.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
cwd: Current working directory
|
|
124
|
+
issue_number: GitHub issue number
|
|
125
|
+
quiet: Suppress output
|
|
126
|
+
resume_existing: If True, keep existing branch with accumulated work
|
|
127
|
+
|
|
128
|
+
Returns (worktree_path, error_message).
|
|
129
|
+
"""
|
|
130
|
+
git_root = _get_git_root(cwd)
|
|
131
|
+
if not git_root:
|
|
132
|
+
return None, "Current directory is not a git repository."
|
|
133
|
+
|
|
134
|
+
worktree_rel_path = Path(".pdd") / "worktrees" / f"fix-issue-{issue_number}"
|
|
135
|
+
worktree_path = git_root / worktree_rel_path
|
|
136
|
+
branch_name = f"fix/issue-{issue_number}"
|
|
137
|
+
|
|
138
|
+
# 1. Clean up existing worktree at path (always needed to create fresh worktree)
|
|
139
|
+
if worktree_path.exists():
|
|
140
|
+
if _worktree_exists(git_root, worktree_path):
|
|
141
|
+
if not quiet:
|
|
142
|
+
console.print(f"[yellow]Removing existing worktree at {worktree_path}[/yellow]")
|
|
143
|
+
success, err = _remove_worktree(git_root, worktree_path)
|
|
144
|
+
if not success:
|
|
145
|
+
return None, f"Failed to remove existing worktree: {err}"
|
|
146
|
+
else:
|
|
147
|
+
# It's just a directory, not a registered worktree
|
|
148
|
+
if not quiet:
|
|
149
|
+
console.print(f"[yellow]Removing stale directory at {worktree_path}[/yellow]")
|
|
150
|
+
shutil.rmtree(worktree_path)
|
|
151
|
+
|
|
152
|
+
# 2. Handle existing branch based on resume_existing
|
|
153
|
+
branch_exists = _branch_exists(git_root, branch_name)
|
|
154
|
+
|
|
155
|
+
if branch_exists:
|
|
156
|
+
if resume_existing:
|
|
157
|
+
# Keep existing branch with our accumulated work
|
|
158
|
+
if not quiet:
|
|
159
|
+
console.print(f"[blue]Resuming with existing branch: {branch_name}[/blue]")
|
|
160
|
+
else:
|
|
161
|
+
# Delete for fresh start
|
|
162
|
+
if not quiet:
|
|
163
|
+
console.print(f"[yellow]Removing existing branch {branch_name}[/yellow]")
|
|
164
|
+
success, err = _delete_branch(git_root, branch_name)
|
|
165
|
+
if not success:
|
|
166
|
+
return None, f"Failed to delete existing branch: {err}"
|
|
167
|
+
|
|
168
|
+
# 3. Create worktree
|
|
169
|
+
try:
|
|
170
|
+
worktree_path.parent.mkdir(parents=True, exist_ok=True)
|
|
171
|
+
|
|
172
|
+
if branch_exists and resume_existing:
|
|
173
|
+
# Checkout existing branch into new worktree
|
|
174
|
+
subprocess.run(
|
|
175
|
+
["git", "worktree", "add", str(worktree_path), branch_name],
|
|
176
|
+
cwd=git_root,
|
|
177
|
+
capture_output=True,
|
|
178
|
+
check=True
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
# Create new branch from HEAD
|
|
182
|
+
subprocess.run(
|
|
183
|
+
["git", "worktree", "add", "-b", branch_name, str(worktree_path), "HEAD"],
|
|
184
|
+
cwd=git_root,
|
|
185
|
+
capture_output=True,
|
|
186
|
+
check=True
|
|
187
|
+
)
|
|
188
|
+
return worktree_path, None
|
|
189
|
+
except subprocess.CalledProcessError as e:
|
|
190
|
+
return None, f"Failed to create worktree: {e.stderr.decode('utf-8')}"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def run_agentic_bug_orchestrator(
|
|
194
|
+
issue_url: str,
|
|
195
|
+
issue_content: str,
|
|
196
|
+
repo_owner: str,
|
|
197
|
+
repo_name: str,
|
|
198
|
+
issue_number: int,
|
|
199
|
+
issue_author: str,
|
|
200
|
+
issue_title: str,
|
|
201
|
+
*,
|
|
202
|
+
cwd: Path,
|
|
203
|
+
verbose: bool = False,
|
|
204
|
+
quiet: bool = False,
|
|
205
|
+
timeout_adder: float = 0.0,
|
|
206
|
+
use_github_state: bool = True
|
|
207
|
+
) -> Tuple[bool, str, float, str, List[str]]:
|
|
208
|
+
"""
|
|
209
|
+
Orchestrates the 10-step agentic bug investigation workflow.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
(success, final_message, total_cost, model_used, changed_files)
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
if not quiet:
|
|
216
|
+
console.print(f"🔍 Investigating issue #{issue_number}: \"{issue_title}\"")
|
|
217
|
+
|
|
218
|
+
# Context accumulation
|
|
219
|
+
context: Dict[str, Any] = {
|
|
220
|
+
"issue_url": issue_url,
|
|
221
|
+
"issue_content": issue_content,
|
|
222
|
+
"repo_owner": repo_owner,
|
|
223
|
+
"repo_name": repo_name,
|
|
224
|
+
"issue_number": issue_number,
|
|
225
|
+
"issue_author": issue_author,
|
|
226
|
+
"issue_title": issue_title,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
total_cost = 0.0
|
|
230
|
+
last_model_used = "unknown"
|
|
231
|
+
changed_files: List[str] = []
|
|
232
|
+
current_cwd = cwd
|
|
233
|
+
worktree_path: Optional[Path] = None
|
|
234
|
+
github_comment_id: Optional[int] = None
|
|
235
|
+
|
|
236
|
+
# Resume: Load existing state if available
|
|
237
|
+
state_dir = _get_state_dir(cwd)
|
|
238
|
+
state, loaded_gh_id = load_workflow_state(
|
|
239
|
+
cwd=cwd,
|
|
240
|
+
issue_number=issue_number,
|
|
241
|
+
workflow_type="bug",
|
|
242
|
+
state_dir=state_dir,
|
|
243
|
+
repo_owner=repo_owner,
|
|
244
|
+
repo_name=repo_name,
|
|
245
|
+
use_github_state=use_github_state
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
step_outputs: Dict[str, str] = {}
|
|
249
|
+
last_completed_step = 0
|
|
250
|
+
|
|
251
|
+
if state is not None:
|
|
252
|
+
last_completed_step = state.get("last_completed_step", 0)
|
|
253
|
+
if not quiet:
|
|
254
|
+
console.print(f"[yellow]Resuming from step {last_completed_step + 1} (steps 1-{last_completed_step} cached)[/yellow]")
|
|
255
|
+
|
|
256
|
+
total_cost = state.get("total_cost", 0.0)
|
|
257
|
+
last_model_used = state.get("model_used", "unknown")
|
|
258
|
+
step_outputs = state.get("step_outputs", {})
|
|
259
|
+
changed_files = state.get("changed_files", [])
|
|
260
|
+
github_comment_id = loaded_gh_id # Use the ID returned by load_workflow_state
|
|
261
|
+
|
|
262
|
+
# Restore worktree path
|
|
263
|
+
wt_path_str = state.get("worktree_path")
|
|
264
|
+
if wt_path_str:
|
|
265
|
+
worktree_path = Path(wt_path_str)
|
|
266
|
+
if worktree_path.exists():
|
|
267
|
+
current_cwd = worktree_path
|
|
268
|
+
else:
|
|
269
|
+
# Recreate worktree with existing branch
|
|
270
|
+
wt_path, err = _setup_worktree(cwd, issue_number, quiet, resume_existing=True)
|
|
271
|
+
if err:
|
|
272
|
+
return False, f"Failed to recreate worktree on resume: {err}", total_cost, last_model_used, []
|
|
273
|
+
worktree_path = wt_path
|
|
274
|
+
current_cwd = worktree_path
|
|
275
|
+
context["worktree_path"] = str(worktree_path)
|
|
276
|
+
|
|
277
|
+
# Restore context from step outputs
|
|
278
|
+
for step_key, output in step_outputs.items():
|
|
279
|
+
context[f"step{step_key}_output"] = output
|
|
280
|
+
|
|
281
|
+
# Restore files_to_stage if available
|
|
282
|
+
if changed_files:
|
|
283
|
+
context["files_to_stage"] = ", ".join(changed_files)
|
|
284
|
+
|
|
285
|
+
# Step Definitions
|
|
286
|
+
steps = [
|
|
287
|
+
(1, "duplicate", "Searching for duplicate issues"),
|
|
288
|
+
(2, "docs", "Checking documentation for user error"),
|
|
289
|
+
(3, "triage", "Assessing information completeness"),
|
|
290
|
+
(4, "reproduce", "Attempting to reproduce the bug"),
|
|
291
|
+
(5, "root_cause", "Analyzing root cause"),
|
|
292
|
+
(6, "test_plan", "Designing test strategy"),
|
|
293
|
+
(7, "generate", "Generating failing unit test"),
|
|
294
|
+
(8, "verify", "Verifying test catches the bug"),
|
|
295
|
+
(9, "e2e_test", "Generating and running E2E tests"),
|
|
296
|
+
(10, "pr", "Creating draft PR"),
|
|
297
|
+
]
|
|
298
|
+
|
|
299
|
+
for step_num, name, description in steps:
|
|
300
|
+
|
|
301
|
+
# Skip already completed steps (resume support)
|
|
302
|
+
if step_num <= last_completed_step:
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
# --- Pre-Step Logic: Worktree Creation ---
|
|
306
|
+
if step_num == 7:
|
|
307
|
+
# Only create worktree if not already set (from resume)
|
|
308
|
+
if worktree_path is None:
|
|
309
|
+
# Check current branch before creating worktree
|
|
310
|
+
try:
|
|
311
|
+
branch_res = subprocess.run(
|
|
312
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
313
|
+
cwd=cwd,
|
|
314
|
+
capture_output=True,
|
|
315
|
+
text=True,
|
|
316
|
+
check=True
|
|
317
|
+
)
|
|
318
|
+
current_branch = branch_res.stdout.strip()
|
|
319
|
+
if current_branch not in ["main", "master"] and not quiet:
|
|
320
|
+
console.print(f"[yellow]Note: Creating branch from HEAD ({current_branch}), not origin/main. PR will include commits from this branch. Run from main for independent changes.[/yellow]")
|
|
321
|
+
except subprocess.CalledProcessError:
|
|
322
|
+
# If we can't determine branch, proceed anyway (might be detached HEAD)
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
wt_path, err = _setup_worktree(cwd, issue_number, quiet, resume_existing=False)
|
|
326
|
+
if not wt_path:
|
|
327
|
+
return False, f"Failed to create worktree: {err}", total_cost, last_model_used, changed_files
|
|
328
|
+
|
|
329
|
+
worktree_path = wt_path
|
|
330
|
+
current_cwd = worktree_path
|
|
331
|
+
context["worktree_path"] = str(worktree_path)
|
|
332
|
+
|
|
333
|
+
if not quiet:
|
|
334
|
+
console.print(f"[blue]Working in worktree: {worktree_path}[/blue]")
|
|
335
|
+
|
|
336
|
+
# --- Step Execution ---
|
|
337
|
+
if not quiet:
|
|
338
|
+
console.print(f"[bold][Step {step_num}/10][/bold] {description}...")
|
|
339
|
+
|
|
340
|
+
template_name = f"agentic_bug_step{step_num}_{name}_LLM"
|
|
341
|
+
prompt_template = load_prompt_template(template_name)
|
|
342
|
+
|
|
343
|
+
if not prompt_template:
|
|
344
|
+
return False, f"Missing prompt template: {template_name}", total_cost, last_model_used, changed_files
|
|
345
|
+
|
|
346
|
+
# Format prompt with accumulated context
|
|
347
|
+
try:
|
|
348
|
+
formatted_prompt = prompt_template.format(**context)
|
|
349
|
+
except KeyError as e:
|
|
350
|
+
return False, f"Prompt formatting error in step {step_num}: missing {e}", total_cost, last_model_used, changed_files
|
|
351
|
+
|
|
352
|
+
# Run the task
|
|
353
|
+
success, output, cost, model = run_agentic_task(
|
|
354
|
+
instruction=formatted_prompt,
|
|
355
|
+
cwd=current_cwd,
|
|
356
|
+
verbose=verbose,
|
|
357
|
+
quiet=quiet,
|
|
358
|
+
label=f"step{step_num}",
|
|
359
|
+
timeout=BUG_STEP_TIMEOUTS.get(step_num, 340.0) + timeout_adder,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# Update tracking
|
|
363
|
+
total_cost += cost
|
|
364
|
+
last_model_used = model
|
|
365
|
+
context[f"step{step_num}_output"] = output
|
|
366
|
+
|
|
367
|
+
# --- Post-Step Logic: Hard Stops & Parsing ---
|
|
368
|
+
|
|
369
|
+
# Step 1: Duplicate Check
|
|
370
|
+
if step_num == 1 and "Duplicate of #" in output:
|
|
371
|
+
msg = f"Stopped at Step 1: Issue is a duplicate. {output.strip()}"
|
|
372
|
+
if not quiet:
|
|
373
|
+
console.print(f"⏹️ {msg}")
|
|
374
|
+
return False, msg, total_cost, last_model_used, changed_files
|
|
375
|
+
|
|
376
|
+
# Step 2: User Error / Feature Request
|
|
377
|
+
if step_num == 2:
|
|
378
|
+
if "Feature Request (Not a Bug)" in output:
|
|
379
|
+
msg = "Stopped at Step 2: Identified as Feature Request."
|
|
380
|
+
if not quiet: console.print(f"⏹️ {msg}")
|
|
381
|
+
return False, msg, total_cost, last_model_used, changed_files
|
|
382
|
+
if "User Error (Not a Bug)" in output:
|
|
383
|
+
msg = "Stopped at Step 2: Identified as User Error."
|
|
384
|
+
if not quiet: console.print(f"⏹️ {msg}")
|
|
385
|
+
return False, msg, total_cost, last_model_used, changed_files
|
|
386
|
+
|
|
387
|
+
# Step 3: Needs Info
|
|
388
|
+
if step_num == 3 and "Needs More Info" in output:
|
|
389
|
+
msg = "Stopped at Step 3: Insufficient information provided."
|
|
390
|
+
if not quiet: console.print(f"⏹️ {msg}")
|
|
391
|
+
return False, msg, total_cost, last_model_used, changed_files
|
|
392
|
+
|
|
393
|
+
# Step 7: File Extraction
|
|
394
|
+
if step_num == 7:
|
|
395
|
+
# Parse output for FILES_CREATED or FILES_MODIFIED
|
|
396
|
+
extracted_files = []
|
|
397
|
+
for line in output.splitlines():
|
|
398
|
+
if line.startswith("FILES_CREATED:") or line.startswith("FILES_MODIFIED:"):
|
|
399
|
+
file_list = line.split(":", 1)[1].strip()
|
|
400
|
+
extracted_files.extend([f.strip() for f in file_list.split(",") if f.strip()])
|
|
401
|
+
|
|
402
|
+
changed_files = extracted_files
|
|
403
|
+
# Pass explicit file list to Step 9 and 10 for precise git staging
|
|
404
|
+
context["files_to_stage"] = ", ".join(changed_files)
|
|
405
|
+
|
|
406
|
+
if not changed_files:
|
|
407
|
+
msg = "Stopped at Step 7: No test file generated."
|
|
408
|
+
if not quiet: console.print(f"⏹️ {msg}")
|
|
409
|
+
return False, msg, total_cost, last_model_used, changed_files
|
|
410
|
+
|
|
411
|
+
# Step 8: Verification Failure
|
|
412
|
+
if step_num == 8 and "FAIL: Test does not work as expected" in output:
|
|
413
|
+
msg = "Stopped at Step 8: Generated test does not fail correctly (verification failed)."
|
|
414
|
+
if not quiet: console.print(f"⏹️ {msg}")
|
|
415
|
+
return False, msg, total_cost, last_model_used, changed_files
|
|
416
|
+
|
|
417
|
+
# Step 9: E2E Test Failure & File Extraction
|
|
418
|
+
if step_num == 9:
|
|
419
|
+
if "E2E_FAIL: Test does not catch bug correctly" in output:
|
|
420
|
+
msg = "Stopped at Step 9: E2E test does not catch bug correctly."
|
|
421
|
+
if not quiet: console.print(f"⏹️ {msg}")
|
|
422
|
+
return False, msg, total_cost, last_model_used, changed_files
|
|
423
|
+
|
|
424
|
+
# Parse output for E2E_FILES_CREATED to extend changed_files
|
|
425
|
+
e2e_files = []
|
|
426
|
+
for line in output.splitlines():
|
|
427
|
+
if line.startswith("E2E_FILES_CREATED:"):
|
|
428
|
+
file_list = line.split(":", 1)[1].strip()
|
|
429
|
+
e2e_files.extend([f.strip() for f in file_list.split(",") if f.strip()])
|
|
430
|
+
|
|
431
|
+
if e2e_files:
|
|
432
|
+
changed_files.extend(e2e_files)
|
|
433
|
+
# Update files_to_stage so Step 10 (PR) includes E2E files
|
|
434
|
+
context["files_to_stage"] = ", ".join(changed_files)
|
|
435
|
+
|
|
436
|
+
# Soft Failure Logging (if not a hard stop)
|
|
437
|
+
if not success and not quiet:
|
|
438
|
+
console.print(f"[yellow]Warning: Step {step_num} reported failure, but proceeding as no hard stop condition met.[/yellow]")
|
|
439
|
+
elif not quiet:
|
|
440
|
+
# Extract a brief result for display if possible, otherwise generic
|
|
441
|
+
console.print(f" → Step {step_num} complete.")
|
|
442
|
+
|
|
443
|
+
# Save state after each step (for resume support)
|
|
444
|
+
step_outputs[str(step_num)] = output
|
|
445
|
+
|
|
446
|
+
new_state = {
|
|
447
|
+
"workflow": "bug",
|
|
448
|
+
"issue_number": issue_number,
|
|
449
|
+
"issue_url": issue_url,
|
|
450
|
+
"last_completed_step": step_num,
|
|
451
|
+
"step_outputs": step_outputs,
|
|
452
|
+
"total_cost": total_cost,
|
|
453
|
+
"model_used": last_model_used,
|
|
454
|
+
"changed_files": changed_files,
|
|
455
|
+
"worktree_path": str(worktree_path) if worktree_path else None,
|
|
456
|
+
"github_comment_id": github_comment_id
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
# Save to GitHub (primary) and local (cache)
|
|
460
|
+
# The function returns the comment ID (new or updated) to track for future updates
|
|
461
|
+
github_comment_id = save_workflow_state(
|
|
462
|
+
cwd=cwd,
|
|
463
|
+
issue_number=issue_number,
|
|
464
|
+
workflow_type="bug",
|
|
465
|
+
state=new_state,
|
|
466
|
+
state_dir=state_dir,
|
|
467
|
+
repo_owner=repo_owner,
|
|
468
|
+
repo_name=repo_name,
|
|
469
|
+
use_github_state=use_github_state,
|
|
470
|
+
github_comment_id=github_comment_id
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# --- Final Summary ---
|
|
474
|
+
# Clear state file on successful completion
|
|
475
|
+
clear_workflow_state(
|
|
476
|
+
cwd=cwd,
|
|
477
|
+
issue_number=issue_number,
|
|
478
|
+
workflow_type="bug",
|
|
479
|
+
state_dir=state_dir,
|
|
480
|
+
repo_owner=repo_owner,
|
|
481
|
+
repo_name=repo_name,
|
|
482
|
+
use_github_state=use_github_state
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
final_msg = "Investigation complete"
|
|
486
|
+
if not quiet:
|
|
487
|
+
console.print(f"✅ {final_msg}")
|
|
488
|
+
console.print(f" Total cost: ${total_cost:.4f}")
|
|
489
|
+
console.print(f" Files changed: {', '.join(changed_files)}")
|
|
490
|
+
if worktree_path:
|
|
491
|
+
console.print(f" Worktree: {worktree_path}")
|
|
492
|
+
|
|
493
|
+
return True, final_msg, total_cost, last_model_used, changed_files
|
|
494
|
+
|
|
495
|
+
if __name__ == "__main__":
|
|
496
|
+
# Example usage logic could go here if needed for testing
|
|
497
|
+
pass
|