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
pdd/fix_error_loop.py CHANGED
@@ -6,9 +6,12 @@ import shutil
6
6
  import json
7
7
  from datetime import datetime
8
8
  from pathlib import Path
9
+ from typing import Tuple, Optional
9
10
 
11
+ import requests
10
12
  from rich import print as rprint
11
13
  from rich.console import Console
14
+ from rich.panel import Panel
12
15
 
13
16
  # Relative import from an internal module.
14
17
  from .get_language import get_language
@@ -17,7 +20,10 @@ from . import DEFAULT_TIME # Import DEFAULT_TIME
17
20
  from .python_env_detector import detect_host_python_executable
18
21
  from .agentic_fix import run_agentic_fix
19
22
  from .agentic_langtest import default_verify_cmd_for
23
+ from .core.cloud import CloudConfig
20
24
 
25
+ # Cloud request timeout for LLM calls
26
+ CLOUD_FIX_TIMEOUT = 400 # seconds
21
27
 
22
28
  console = Console()
23
29
 
@@ -25,6 +31,133 @@ def escape_brackets(text: str) -> str:
25
31
  """Escape square brackets so Rich doesn't misinterpret them."""
26
32
  return text.replace("[", "\\[").replace("]", "\\]")
27
33
 
34
+
35
+ def cloud_fix_errors(
36
+ unit_test: str,
37
+ code: str,
38
+ prompt: str,
39
+ error: str,
40
+ error_file: str,
41
+ strength: float,
42
+ temperature: float,
43
+ verbose: bool = False,
44
+ time: float = DEFAULT_TIME,
45
+ code_file_ext: str = ".py"
46
+ ) -> Tuple[bool, bool, str, str, str, float, str]:
47
+ """
48
+ Call the cloud fixCode endpoint to fix errors in code and unit tests.
49
+
50
+ This function has the same interface as fix_errors_from_unit_tests to allow
51
+ seamless switching between local and cloud execution in the fix loop.
52
+
53
+ Args:
54
+ unit_test: Unit test code string
55
+ code: Source code string
56
+ prompt: Prompt that generated the code
57
+ error: Error messages/logs from test failures
58
+ error_file: Path to write error analysis (not used in cloud, but kept for interface compatibility)
59
+ strength: Model strength parameter [0,1]
60
+ temperature: Model temperature parameter [0,1]
61
+ verbose: Enable verbose logging
62
+ time: Time budget for thinking effort
63
+ code_file_ext: File extension to determine language (e.g., ".py", ".java")
64
+
65
+ Returns:
66
+ Tuple of:
67
+ - update_unit_test: Whether unit test was updated
68
+ - update_code: Whether code was updated
69
+ - fixed_unit_test: Fixed unit test code
70
+ - fixed_code: Fixed source code
71
+ - analysis: Analysis/explanation of fixes
72
+ - total_cost: Cost of the operation
73
+ - model_name: Name of model used
74
+
75
+ Raises:
76
+ RuntimeError: When cloud execution fails with non-recoverable error
77
+ """
78
+ jwt_token = CloudConfig.get_jwt_token(verbose=verbose)
79
+
80
+ if not jwt_token:
81
+ raise RuntimeError("Cloud authentication failed - no JWT token available")
82
+
83
+ # Build cloud payload
84
+ payload = {
85
+ "unitTest": unit_test,
86
+ "code": code,
87
+ "prompt": prompt,
88
+ "errors": error,
89
+ "language": get_language(code_file_ext),
90
+ "strength": strength,
91
+ "temperature": temperature,
92
+ "time": time if time is not None else 0.25,
93
+ "verbose": verbose,
94
+ }
95
+
96
+ headers = {
97
+ "Authorization": f"Bearer {jwt_token}",
98
+ "Content-Type": "application/json"
99
+ }
100
+ cloud_url = CloudConfig.get_endpoint_url("fixCode")
101
+
102
+ if verbose:
103
+ console.print(Panel(f"Calling cloud fix at {cloud_url}", title="[blue]Cloud LLM[/blue]", expand=False))
104
+
105
+ try:
106
+ response = requests.post(
107
+ cloud_url,
108
+ json=payload,
109
+ headers=headers,
110
+ timeout=CLOUD_FIX_TIMEOUT
111
+ )
112
+ response.raise_for_status()
113
+
114
+ response_data = response.json()
115
+ fixed_unit_test = response_data.get("fixedUnitTest", "")
116
+ fixed_code = response_data.get("fixedCode", "")
117
+ analysis = response_data.get("analysis", "")
118
+ total_cost = float(response_data.get("totalCost", 0.0))
119
+ model_name = response_data.get("modelName", "cloud_model")
120
+ update_unit_test = response_data.get("updateUnitTest", False)
121
+ update_code = response_data.get("updateCode", False)
122
+
123
+ if verbose:
124
+ console.print(f"[cyan]Cloud fix completed. Model: {model_name}, Cost: ${total_cost:.6f}[/cyan]")
125
+
126
+ return update_unit_test, update_code, fixed_unit_test, fixed_code, analysis, total_cost, model_name
127
+
128
+ except requests.exceptions.Timeout:
129
+ raise RuntimeError(f"Cloud fix timed out after {CLOUD_FIX_TIMEOUT}s")
130
+
131
+ except requests.exceptions.HTTPError as e:
132
+ status_code = e.response.status_code if e.response else 0
133
+ err_content = e.response.text[:200] if e.response else "No response content"
134
+
135
+ # Non-recoverable errors
136
+ if status_code == 402:
137
+ try:
138
+ error_data = e.response.json()
139
+ current_balance = error_data.get("currentBalance", "unknown")
140
+ estimated_cost = error_data.get("estimatedCost", "unknown")
141
+ raise RuntimeError(f"Insufficient credits. Balance: {current_balance}, estimated cost: {estimated_cost}")
142
+ except json.JSONDecodeError:
143
+ raise RuntimeError(f"Insufficient credits: {err_content}")
144
+ elif status_code == 401:
145
+ raise RuntimeError(f"Authentication failed: {err_content}")
146
+ elif status_code == 403:
147
+ raise RuntimeError(f"Access denied: {err_content}")
148
+ elif status_code == 400:
149
+ raise RuntimeError(f"Invalid request: {err_content}")
150
+ else:
151
+ # 5xx or other errors - raise for caller to handle
152
+ raise RuntimeError(f"Cloud HTTP error ({status_code}): {err_content}")
153
+
154
+ except requests.exceptions.RequestException as e:
155
+ raise RuntimeError(f"Cloud network error: {e}")
156
+
157
+ except json.JSONDecodeError:
158
+ raise RuntimeError("Cloud returned invalid JSON response")
159
+
160
+
28
161
  # ---------- Normalize any agentic return shape to a 4-tuple ----------
29
162
  def _normalize_agentic_result(result):
30
163
  """
@@ -137,15 +270,21 @@ def fix_error_loop(unit_test_file: str,
137
270
  error_log_file: str = "error_log.txt",
138
271
  verbose: bool = False,
139
272
  time: float = DEFAULT_TIME,
140
- agentic_fallback: bool = True):
273
+ agentic_fallback: bool = True,
274
+ use_cloud: bool = False):
141
275
  """
142
- Attempt to fix errors in a unit test and corresponding code using repeated iterations,
143
- counting only the number of times we actually call the LLM fix function.
276
+ Attempt to fix errors in a unit test and corresponding code using repeated iterations,
277
+ counting only the number of times we actually call the LLM fix function.
144
278
  The tests are re-run in the same iteration after a fix to see if we've succeeded,
145
279
  so that 'attempts' matches the number of fix attempts (not the total test runs).
146
280
 
147
281
  This updated version uses structured logging to avoid redundant entries.
148
282
 
283
+ Hybrid Cloud Support:
284
+ When use_cloud=True, the LLM fix calls are routed to the cloud fixCode endpoint
285
+ while local test execution (pytest, verification programs) stays local. This allows
286
+ the loop to pass local test results to the cloud for analysis and fixes.
287
+
149
288
  Inputs:
150
289
  unit_test_file: Path to the file containing unit tests.
151
290
  code_file: Path to the file containing the code under test.
@@ -159,6 +298,7 @@ def fix_error_loop(unit_test_file: str,
159
298
  verbose: Enable verbose logging (default: False).
160
299
  time: Time parameter for the fix_errors_from_unit_tests call.
161
300
  agentic_fallback: Whether to trigger cli agentic fallback when fix fails.
301
+ use_cloud: If True, use cloud LLM for fix calls while keeping test execution local.
162
302
  Outputs:
163
303
  success: Boolean indicating if the overall process succeeded.
164
304
  final_unit_test: String contents of the final unit test file.
@@ -213,9 +353,10 @@ def fix_error_loop(unit_test_file: str,
213
353
 
214
354
  # We do up to max_attempts fix attempts or until budget is exceeded
215
355
  iteration = 0
356
+ # Determine if target is Python (moved before try block for use in exception handler)
357
+ is_python = str(code_file).lower().endswith(".py")
216
358
  # Run an initial test to determine starting state
217
359
  try:
218
- is_python = str(code_file).lower().endswith(".py")
219
360
  if is_python:
220
361
  initial_fails, initial_errors, initial_warnings, pytest_output = run_pytest_on_file(unit_test_file)
221
362
  else:
@@ -224,8 +365,44 @@ def fix_error_loop(unit_test_file: str,
224
365
  lang = get_language(os.path.splitext(code_file)[1])
225
366
  verify_cmd = default_verify_cmd_for(lang, unit_test_file)
226
367
  if not verify_cmd:
227
- raise ValueError(f"No default verification command for language: {lang}")
228
-
368
+ # No verify command available (e.g., Java without maven/gradle).
369
+ # Trigger agentic fallback directly.
370
+ rprint(f"[cyan]No verification command for {lang}. Triggering agentic fallback directly...[/cyan]")
371
+ error_log_path = Path(error_log_file)
372
+ error_log_path.parent.mkdir(parents=True, exist_ok=True)
373
+ if not error_log_path.exists() or error_log_path.stat().st_size == 0:
374
+ with open(error_log_path, "w") as f:
375
+ f.write(f"No verification command available for language: {lang}\n")
376
+ f.write("Agentic fix will attempt to resolve the issue.\n")
377
+
378
+ rprint(f"[cyan]Attempting agentic fix fallback (prompt_file={prompt_file!r})...[/cyan]")
379
+ success, agent_msg, agent_cost, agent_model, agent_changed_files = _safe_run_agentic_fix(
380
+ prompt_file=prompt_file,
381
+ code_file=code_file,
382
+ unit_test_file=unit_test_file,
383
+ error_log_file=error_log_file,
384
+ cwd=None, # Use project root (cwd), not prompt file's parent
385
+ )
386
+ if not success:
387
+ rprint(f"[bold red]Agentic fix fallback failed: {agent_msg}[/bold red]")
388
+ if agent_changed_files:
389
+ rprint(f"[cyan]Agent modified {len(agent_changed_files)} file(s):[/cyan]")
390
+ for f in agent_changed_files:
391
+ rprint(f" • {f}")
392
+ final_unit_test = ""
393
+ final_code = ""
394
+ try:
395
+ with open(unit_test_file, "r") as f:
396
+ final_unit_test = f.read()
397
+ except Exception:
398
+ pass
399
+ try:
400
+ with open(code_file, "r") as f:
401
+ final_code = f.read()
402
+ except Exception:
403
+ pass
404
+ return success, final_unit_test, final_code, 1, agent_cost, agent_model
405
+
229
406
  verify_result = subprocess.run(verify_cmd, capture_output=True, text=True, shell=True, stdin=subprocess.DEVNULL)
230
407
  pytest_output = (verify_result.stdout or "") + "\n" + (verify_result.stderr or "")
231
408
  if verify_result.returncode == 0:
@@ -246,7 +423,43 @@ def fix_error_loop(unit_test_file: str,
246
423
  }
247
424
  except Exception as e:
248
425
  rprint(f"[red]Error running initial test/verification:[/red] {e}")
249
- return False, "", "", fix_attempts, total_cost, model_name
426
+ # Instead of returning early, trigger agentic fallback if enabled (Issue #266)
427
+ if agentic_fallback:
428
+ rprint("[cyan]Initial test failed with exception. Triggering agentic fallback...[/cyan]")
429
+ error_log_path = Path(error_log_file)
430
+ error_log_path.parent.mkdir(parents=True, exist_ok=True)
431
+ with open(error_log_path, "w") as f:
432
+ f.write(f"Initial test/verification failed with exception:\n{e}\n")
433
+
434
+ success, agent_msg, agent_cost, agent_model, agent_changed_files = _safe_run_agentic_fix(
435
+ prompt_file=prompt_file,
436
+ code_file=code_file,
437
+ unit_test_file=unit_test_file,
438
+ error_log_file=error_log_file,
439
+ cwd=None,
440
+ )
441
+ if not success:
442
+ rprint(f"[bold red]Agentic fix fallback failed: {agent_msg}[/bold red]")
443
+ if agent_changed_files:
444
+ rprint(f"[cyan]Agent modified {len(agent_changed_files)} file(s):[/cyan]")
445
+ for f in agent_changed_files:
446
+ rprint(f" • {f}")
447
+ final_unit_test = ""
448
+ final_code = ""
449
+ try:
450
+ with open(unit_test_file, "r") as f:
451
+ final_unit_test = f.read()
452
+ except Exception:
453
+ pass
454
+ try:
455
+ with open(code_file, "r") as f:
456
+ final_code = f.read()
457
+ except Exception:
458
+ pass
459
+ return success, final_unit_test, final_code, 1, agent_cost, agent_model
460
+ else:
461
+ # Agentic fallback disabled, return failure
462
+ return False, "", "", fix_attempts, total_cost, model_name
250
463
 
251
464
  # If target is not a Python file, trigger agentic fallback if tests fail
252
465
  if not is_python:
@@ -263,7 +476,7 @@ def fix_error_loop(unit_test_file: str,
263
476
  code_file=code_file,
264
477
  unit_test_file=unit_test_file,
265
478
  error_log_file=error_log_file,
266
- cwd=Path(prompt_file).parent if prompt_file else None,
479
+ cwd=None, # Use project root (cwd), not prompt file's parent
267
480
  )
268
481
  if not success:
269
482
  rprint(f"[bold red]Agentic fix fallback failed: {agent_msg}[/bold red]")
@@ -383,17 +596,18 @@ def fix_error_loop(unit_test_file: str,
383
596
  break
384
597
 
385
598
  # We only attempt to fix if test is failing or has warnings:
386
- # Let's create backups:
387
- unit_test_dir, unit_test_name = os.path.split(unit_test_file)
388
- code_dir, code_name = os.path.split(code_file)
389
- unit_test_backup = os.path.join(
390
- unit_test_dir,
391
- f"{os.path.splitext(unit_test_name)[0]}_{iteration}_{errors}_{fails}_{warnings}_{timestamp}.py"
392
- )
393
- code_backup = os.path.join(
394
- code_dir,
395
- f"{os.path.splitext(code_name)[0]}_{iteration}_{errors}_{fails}_{warnings}_{timestamp}.py"
396
- )
599
+ # Let's create backups in .pdd/backups/ to avoid polluting code/test directories
600
+ code_name = os.path.basename(code_file)
601
+ code_basename = os.path.splitext(code_name)[0]
602
+ unit_test_name = os.path.basename(unit_test_file)
603
+ unit_test_ext = os.path.splitext(unit_test_name)[1]
604
+ code_ext = os.path.splitext(code_name)[1]
605
+
606
+ backup_dir = Path.cwd() / '.pdd' / 'backups' / code_basename / timestamp
607
+ backup_dir.mkdir(parents=True, exist_ok=True)
608
+
609
+ unit_test_backup = str(backup_dir / f"test_{iteration}_{errors}_{fails}_{warnings}{unit_test_ext}")
610
+ code_backup = str(backup_dir / f"code_{iteration}_{errors}_{fails}_{warnings}{code_ext}")
397
611
  try:
398
612
  shutil.copy(unit_test_file, unit_test_backup)
399
613
  shutil.copy(code_file, code_backup)
@@ -402,7 +616,8 @@ def fix_error_loop(unit_test_file: str,
402
616
  rprint(f"[green]Created backup for code file:[/green] {code_backup}")
403
617
  except Exception as e:
404
618
  rprint(f"[red]Error creating backup files:[/red] {e}")
405
- return False, "", "", fix_attempts, total_cost, model_name
619
+ success = False
620
+ break # Exit loop but continue to agentic fallback (Issue #266)
406
621
 
407
622
  # Update best iteration if needed:
408
623
  if (errors < best_iteration_info["errors"] or
@@ -425,30 +640,67 @@ def fix_error_loop(unit_test_file: str,
425
640
  code_contents = f.read()
426
641
  except Exception as e:
427
642
  rprint(f"[red]Error reading input files:[/red] {e}")
428
- return False, "", "", fix_attempts, total_cost, model_name
643
+ success = False
644
+ break # Exit loop but continue to agentic fallback (Issue #266)
429
645
 
430
- # Call fix:
646
+ # Call fix (cloud or local based on use_cloud parameter):
431
647
  try:
432
- # Format the log for the LLM
648
+ # Format the log for the LLM - includes local test results
433
649
  formatted_log = format_log_for_output(log_structure)
434
650
 
435
- updated_unit_test, updated_code, fixed_unit_test, fixed_code, analysis, cost, model_name = fix_errors_from_unit_tests(
436
- unit_test_contents,
437
- code_contents,
438
- prompt,
439
- formatted_log, # Use formatted log instead of reading the file
440
- error_log_file,
441
- strength,
442
- temperature,
443
- verbose=verbose,
444
- time=time # Pass time parameter
445
- )
446
-
651
+ if use_cloud:
652
+ # Use cloud LLM for fix - local test results passed via formatted_log
653
+ try:
654
+ updated_unit_test, updated_code, fixed_unit_test, fixed_code, analysis, cost, model_name = cloud_fix_errors(
655
+ unit_test=unit_test_contents,
656
+ code=code_contents,
657
+ prompt=prompt,
658
+ error=formatted_log, # Pass local test results to cloud
659
+ error_file=error_log_file,
660
+ strength=strength,
661
+ temperature=temperature,
662
+ verbose=verbose,
663
+ time=time,
664
+ code_file_ext=os.path.splitext(code_file)[1]
665
+ )
666
+ except RuntimeError as cloud_err:
667
+ # Cloud failed - fall back to local if it's a recoverable error
668
+ if "Insufficient credits" in str(cloud_err) or "Authentication failed" in str(cloud_err) or "Access denied" in str(cloud_err):
669
+ # Non-recoverable errors - stop the loop
670
+ rprint(f"[red]Cloud fix error (non-recoverable):[/red] {cloud_err}")
671
+ break
672
+ # Recoverable errors - fall back to local
673
+ rprint(f"[yellow]Cloud fix failed, falling back to local:[/yellow] {cloud_err}")
674
+ updated_unit_test, updated_code, fixed_unit_test, fixed_code, analysis, cost, model_name = fix_errors_from_unit_tests(
675
+ unit_test_contents,
676
+ code_contents,
677
+ prompt,
678
+ formatted_log,
679
+ error_log_file,
680
+ strength,
681
+ temperature,
682
+ verbose=verbose,
683
+ time=time
684
+ )
685
+ else:
686
+ # Use local LLM for fix
687
+ updated_unit_test, updated_code, fixed_unit_test, fixed_code, analysis, cost, model_name = fix_errors_from_unit_tests(
688
+ unit_test_contents,
689
+ code_contents,
690
+ prompt,
691
+ formatted_log, # Use formatted log instead of reading the file
692
+ error_log_file,
693
+ strength,
694
+ temperature,
695
+ verbose=verbose,
696
+ time=time # Pass time parameter
697
+ )
698
+
447
699
  # Update the fix attempt in the structured log
448
700
  log_structure["iterations"][-1]["fix_attempt"] = analysis
449
701
  log_structure["iterations"][-1]["model_name"] = model_name
450
702
  except Exception as e:
451
- rprint(f"[red]Error during fix_errors_from_unit_tests call:[/red] {e}")
703
+ rprint(f"[red]Error during fix call:[/red] {e}")
452
704
  break
453
705
 
454
706
  fix_attempts += 1 # We used one fix attempt
@@ -544,7 +796,8 @@ def fix_error_loop(unit_test_file: str,
544
796
  stats["final_warnings"] = warnings
545
797
  except Exception as e:
546
798
  rprint(f"[red]Error running pytest for next iteration:[/red] {e}")
547
- return False, "", "", fix_attempts, total_cost, model_name
799
+ success = False
800
+ break # Exit loop but continue to agentic fallback (Issue #266)
548
801
 
549
802
  # Possibly restore best iteration if the final run is not as good:
550
803
  if best_iteration_info["attempt"] is not None and not success:
@@ -643,7 +896,7 @@ def fix_error_loop(unit_test_file: str,
643
896
  code_file=code_file,
644
897
  unit_test_file=unit_test_file,
645
898
  error_log_file=error_log_file,
646
- cwd=Path(prompt_file).parent if prompt_file else None,
899
+ cwd=None, # Use project root (cwd), not prompt file's parent
647
900
  )
648
901
  total_cost += agent_cost
649
902
  if not agent_success: