pdd-cli 0.0.45__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.
Files changed (195) hide show
  1. pdd/__init__.py +40 -8
  2. pdd/agentic_bug.py +323 -0
  3. pdd/agentic_bug_orchestrator.py +497 -0
  4. pdd/agentic_change.py +231 -0
  5. pdd/agentic_change_orchestrator.py +526 -0
  6. pdd/agentic_common.py +598 -0
  7. pdd/agentic_crash.py +534 -0
  8. pdd/agentic_e2e_fix.py +319 -0
  9. pdd/agentic_e2e_fix_orchestrator.py +426 -0
  10. pdd/agentic_fix.py +1294 -0
  11. pdd/agentic_langtest.py +162 -0
  12. pdd/agentic_update.py +387 -0
  13. pdd/agentic_verify.py +183 -0
  14. pdd/architecture_sync.py +565 -0
  15. pdd/auth_service.py +210 -0
  16. pdd/auto_deps_main.py +71 -51
  17. pdd/auto_include.py +245 -5
  18. pdd/auto_update.py +125 -47
  19. pdd/bug_main.py +196 -23
  20. pdd/bug_to_unit_test.py +2 -0
  21. pdd/change_main.py +11 -4
  22. pdd/cli.py +22 -1181
  23. pdd/cmd_test_main.py +350 -150
  24. pdd/code_generator.py +60 -18
  25. pdd/code_generator_main.py +790 -57
  26. pdd/commands/__init__.py +48 -0
  27. pdd/commands/analysis.py +306 -0
  28. pdd/commands/auth.py +309 -0
  29. pdd/commands/connect.py +290 -0
  30. pdd/commands/fix.py +163 -0
  31. pdd/commands/generate.py +257 -0
  32. pdd/commands/maintenance.py +175 -0
  33. pdd/commands/misc.py +87 -0
  34. pdd/commands/modify.py +256 -0
  35. pdd/commands/report.py +144 -0
  36. pdd/commands/sessions.py +284 -0
  37. pdd/commands/templates.py +215 -0
  38. pdd/commands/utility.py +110 -0
  39. pdd/config_resolution.py +58 -0
  40. pdd/conflicts_main.py +8 -3
  41. pdd/construct_paths.py +589 -111
  42. pdd/context_generator.py +10 -2
  43. pdd/context_generator_main.py +175 -76
  44. pdd/continue_generation.py +53 -10
  45. pdd/core/__init__.py +33 -0
  46. pdd/core/cli.py +527 -0
  47. pdd/core/cloud.py +237 -0
  48. pdd/core/dump.py +554 -0
  49. pdd/core/errors.py +67 -0
  50. pdd/core/remote_session.py +61 -0
  51. pdd/core/utils.py +90 -0
  52. pdd/crash_main.py +262 -33
  53. pdd/data/language_format.csv +71 -63
  54. pdd/data/llm_model.csv +20 -18
  55. pdd/detect_change_main.py +5 -4
  56. pdd/docs/prompting_guide.md +864 -0
  57. pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
  58. pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
  59. pdd/fix_code_loop.py +523 -95
  60. pdd/fix_code_module_errors.py +6 -2
  61. pdd/fix_error_loop.py +491 -92
  62. pdd/fix_errors_from_unit_tests.py +4 -3
  63. pdd/fix_main.py +278 -21
  64. pdd/fix_verification_errors.py +12 -100
  65. pdd/fix_verification_errors_loop.py +529 -286
  66. pdd/fix_verification_main.py +294 -89
  67. pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
  68. pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
  69. pdd/frontend/dist/index.html +376 -0
  70. pdd/frontend/dist/logo.svg +33 -0
  71. pdd/generate_output_paths.py +139 -15
  72. pdd/generate_test.py +218 -146
  73. pdd/get_comment.py +19 -44
  74. pdd/get_extension.py +8 -9
  75. pdd/get_jwt_token.py +318 -22
  76. pdd/get_language.py +8 -7
  77. pdd/get_run_command.py +75 -0
  78. pdd/get_test_command.py +68 -0
  79. pdd/git_update.py +70 -19
  80. pdd/incremental_code_generator.py +2 -2
  81. pdd/insert_includes.py +13 -4
  82. pdd/llm_invoke.py +1711 -181
  83. pdd/load_prompt_template.py +19 -12
  84. pdd/path_resolution.py +140 -0
  85. pdd/pdd_completion.fish +25 -2
  86. pdd/pdd_completion.sh +30 -4
  87. pdd/pdd_completion.zsh +79 -4
  88. pdd/postprocess.py +14 -4
  89. pdd/preprocess.py +293 -24
  90. pdd/preprocess_main.py +41 -6
  91. pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
  92. pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
  93. pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
  94. pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
  95. pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
  96. pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
  97. pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
  98. pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
  99. pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
  100. pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
  101. pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
  102. pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
  103. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
  104. pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
  105. pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
  106. pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
  107. pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
  108. pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
  109. pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
  110. pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
  111. pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
  112. pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
  113. pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
  114. pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
  115. pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
  116. pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
  117. pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
  118. pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
  119. pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
  120. pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
  121. pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
  122. pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
  123. pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
  124. pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
  125. pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
  126. pdd/prompts/agentic_update_LLM.prompt +925 -0
  127. pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
  128. pdd/prompts/auto_include_LLM.prompt +122 -905
  129. pdd/prompts/change_LLM.prompt +3093 -1
  130. pdd/prompts/detect_change_LLM.prompt +686 -27
  131. pdd/prompts/example_generator_LLM.prompt +22 -1
  132. pdd/prompts/extract_code_LLM.prompt +5 -1
  133. pdd/prompts/extract_program_code_fix_LLM.prompt +7 -1
  134. pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
  135. pdd/prompts/extract_promptline_LLM.prompt +17 -11
  136. pdd/prompts/find_verification_errors_LLM.prompt +6 -0
  137. pdd/prompts/fix_code_module_errors_LLM.prompt +12 -2
  138. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +9 -0
  139. pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
  140. pdd/prompts/generate_test_LLM.prompt +41 -7
  141. pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
  142. pdd/prompts/increase_tests_LLM.prompt +1 -5
  143. pdd/prompts/insert_includes_LLM.prompt +316 -186
  144. pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
  145. pdd/prompts/prompt_diff_LLM.prompt +82 -0
  146. pdd/prompts/trace_LLM.prompt +25 -22
  147. pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
  148. pdd/prompts/update_prompt_LLM.prompt +22 -1
  149. pdd/pytest_output.py +127 -12
  150. pdd/remote_session.py +876 -0
  151. pdd/render_mermaid.py +236 -0
  152. pdd/server/__init__.py +52 -0
  153. pdd/server/app.py +335 -0
  154. pdd/server/click_executor.py +587 -0
  155. pdd/server/executor.py +338 -0
  156. pdd/server/jobs.py +661 -0
  157. pdd/server/models.py +241 -0
  158. pdd/server/routes/__init__.py +31 -0
  159. pdd/server/routes/architecture.py +451 -0
  160. pdd/server/routes/auth.py +364 -0
  161. pdd/server/routes/commands.py +929 -0
  162. pdd/server/routes/config.py +42 -0
  163. pdd/server/routes/files.py +603 -0
  164. pdd/server/routes/prompts.py +1322 -0
  165. pdd/server/routes/websocket.py +473 -0
  166. pdd/server/security.py +243 -0
  167. pdd/server/terminal_spawner.py +209 -0
  168. pdd/server/token_counter.py +222 -0
  169. pdd/setup_tool.py +648 -0
  170. pdd/simple_math.py +2 -0
  171. pdd/split_main.py +3 -2
  172. pdd/summarize_directory.py +237 -195
  173. pdd/sync_animation.py +8 -4
  174. pdd/sync_determine_operation.py +839 -112
  175. pdd/sync_main.py +351 -57
  176. pdd/sync_orchestration.py +1400 -756
  177. pdd/sync_tui.py +848 -0
  178. pdd/template_expander.py +161 -0
  179. pdd/template_registry.py +264 -0
  180. pdd/templates/architecture/architecture_json.prompt +237 -0
  181. pdd/templates/generic/generate_prompt.prompt +174 -0
  182. pdd/trace.py +168 -12
  183. pdd/trace_main.py +4 -3
  184. pdd/track_cost.py +140 -63
  185. pdd/unfinished_prompt.py +51 -4
  186. pdd/update_main.py +567 -67
  187. pdd/update_model_costs.py +2 -2
  188. pdd/update_prompt.py +19 -4
  189. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +29 -11
  190. pdd_cli-0.0.118.dist-info/RECORD +227 -0
  191. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +1 -1
  192. pdd_cli-0.0.45.dist-info/RECORD +0 -116
  193. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
  194. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
  195. {pdd_cli-0.0.45.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