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