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.
Files changed (144) hide show
  1. pdd/__init__.py +38 -6
  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 +521 -786
  7. pdd/agentic_e2e_fix.py +319 -0
  8. pdd/agentic_e2e_fix_orchestrator.py +426 -0
  9. pdd/agentic_fix.py +118 -3
  10. pdd/agentic_update.py +25 -8
  11. pdd/architecture_sync.py +565 -0
  12. pdd/auth_service.py +210 -0
  13. pdd/auto_deps_main.py +63 -53
  14. pdd/auto_include.py +185 -3
  15. pdd/auto_update.py +125 -47
  16. pdd/bug_main.py +195 -23
  17. pdd/cmd_test_main.py +345 -197
  18. pdd/code_generator.py +4 -2
  19. pdd/code_generator_main.py +118 -32
  20. pdd/commands/__init__.py +6 -0
  21. pdd/commands/analysis.py +87 -29
  22. pdd/commands/auth.py +309 -0
  23. pdd/commands/connect.py +290 -0
  24. pdd/commands/fix.py +136 -113
  25. pdd/commands/maintenance.py +3 -2
  26. pdd/commands/misc.py +8 -0
  27. pdd/commands/modify.py +190 -164
  28. pdd/commands/sessions.py +284 -0
  29. pdd/construct_paths.py +334 -32
  30. pdd/context_generator_main.py +167 -170
  31. pdd/continue_generation.py +6 -3
  32. pdd/core/__init__.py +33 -0
  33. pdd/core/cli.py +27 -3
  34. pdd/core/cloud.py +237 -0
  35. pdd/core/errors.py +4 -0
  36. pdd/core/remote_session.py +61 -0
  37. pdd/crash_main.py +219 -23
  38. pdd/data/llm_model.csv +4 -4
  39. pdd/docs/prompting_guide.md +864 -0
  40. pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
  41. pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
  42. pdd/fix_code_loop.py +208 -34
  43. pdd/fix_code_module_errors.py +6 -2
  44. pdd/fix_error_loop.py +291 -38
  45. pdd/fix_main.py +204 -4
  46. pdd/fix_verification_errors_loop.py +235 -26
  47. pdd/fix_verification_main.py +269 -83
  48. pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
  49. pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
  50. pdd/frontend/dist/index.html +376 -0
  51. pdd/frontend/dist/logo.svg +33 -0
  52. pdd/generate_output_paths.py +46 -5
  53. pdd/generate_test.py +212 -151
  54. pdd/get_comment.py +19 -44
  55. pdd/get_extension.py +8 -9
  56. pdd/get_jwt_token.py +309 -20
  57. pdd/get_language.py +8 -7
  58. pdd/get_run_command.py +7 -5
  59. pdd/insert_includes.py +2 -1
  60. pdd/llm_invoke.py +459 -95
  61. pdd/load_prompt_template.py +15 -34
  62. pdd/path_resolution.py +140 -0
  63. pdd/postprocess.py +4 -1
  64. pdd/preprocess.py +68 -12
  65. pdd/preprocess_main.py +33 -1
  66. pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
  67. pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
  68. pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
  69. pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
  70. pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
  71. pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
  72. pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
  73. pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
  74. pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
  75. pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
  76. pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
  77. pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
  78. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
  79. pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
  80. pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
  81. pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
  82. pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
  83. pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
  84. pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
  85. pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
  86. pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
  87. pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
  88. pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
  89. pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
  90. pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
  91. pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
  92. pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
  93. pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
  94. pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
  95. pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
  96. pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
  97. pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
  98. pdd/prompts/agentic_update_LLM.prompt +192 -338
  99. pdd/prompts/auto_include_LLM.prompt +22 -0
  100. pdd/prompts/change_LLM.prompt +3093 -1
  101. pdd/prompts/detect_change_LLM.prompt +571 -14
  102. pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
  103. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
  104. pdd/prompts/generate_test_LLM.prompt +20 -1
  105. pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
  106. pdd/prompts/insert_includes_LLM.prompt +262 -252
  107. pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
  108. pdd/prompts/prompt_diff_LLM.prompt +82 -0
  109. pdd/remote_session.py +876 -0
  110. pdd/server/__init__.py +52 -0
  111. pdd/server/app.py +335 -0
  112. pdd/server/click_executor.py +587 -0
  113. pdd/server/executor.py +338 -0
  114. pdd/server/jobs.py +661 -0
  115. pdd/server/models.py +241 -0
  116. pdd/server/routes/__init__.py +31 -0
  117. pdd/server/routes/architecture.py +451 -0
  118. pdd/server/routes/auth.py +364 -0
  119. pdd/server/routes/commands.py +929 -0
  120. pdd/server/routes/config.py +42 -0
  121. pdd/server/routes/files.py +603 -0
  122. pdd/server/routes/prompts.py +1322 -0
  123. pdd/server/routes/websocket.py +473 -0
  124. pdd/server/security.py +243 -0
  125. pdd/server/terminal_spawner.py +209 -0
  126. pdd/server/token_counter.py +222 -0
  127. pdd/summarize_directory.py +236 -237
  128. pdd/sync_animation.py +8 -4
  129. pdd/sync_determine_operation.py +329 -47
  130. pdd/sync_main.py +272 -28
  131. pdd/sync_orchestration.py +136 -75
  132. pdd/template_expander.py +161 -0
  133. pdd/templates/architecture/architecture_json.prompt +41 -46
  134. pdd/trace.py +1 -1
  135. pdd/track_cost.py +0 -13
  136. pdd/unfinished_prompt.py +2 -1
  137. pdd/update_main.py +23 -5
  138. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +15 -10
  139. pdd_cli-0.0.118.dist-info/RECORD +227 -0
  140. pdd_cli-0.0.90.dist-info/RECORD +0 -153
  141. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
  142. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
  143. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +0 -0
  144. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/top_level.txt +0 -0
pdd/fix_main.py CHANGED
@@ -1,8 +1,11 @@
1
1
  import sys
2
2
  from typing import Tuple, Optional
3
+ import json
3
4
  import click
4
5
  from rich import print as rprint
5
6
  from rich.markup import MarkupError, escape
7
+ from rich.console import Console
8
+ from rich.panel import Panel
6
9
 
7
10
  import requests
8
11
  import asyncio
@@ -16,10 +19,24 @@ from .fix_errors_from_unit_tests import fix_errors_from_unit_tests
16
19
  from .fix_error_loop import fix_error_loop, run_pytest_on_file
17
20
  from .get_jwt_token import get_jwt_token
18
21
  from .get_language import get_language
22
+ from .core.cloud import CloudConfig
19
23
 
20
24
  # Import DEFAULT_STRENGTH from the package
21
25
  from . import DEFAULT_STRENGTH
22
26
 
27
+ # Cloud request timeout
28
+ CLOUD_REQUEST_TIMEOUT = 400 # seconds
29
+
30
+ console = Console()
31
+
32
+
33
+ def _env_flag_enabled(name: str) -> bool:
34
+ """Return True when an env var is set to a truthy value."""
35
+ value = os.environ.get(name)
36
+ if value is None:
37
+ return False
38
+ return str(value).strip().lower() in {"1", "true", "yes", "on"}
39
+
23
40
  def fix_main(
24
41
  ctx: click.Context,
25
42
  prompt_file: str,
@@ -111,8 +128,185 @@ def fix_main(
111
128
  verbose = ctx.obj.get('verbose', False)
112
129
  time = ctx.obj.get('time') # Get time from context
113
130
 
131
+ # Determine cloud vs local execution preference
132
+ is_local_execution_preferred = ctx.obj.get('local', False)
133
+ cloud_only = _env_flag_enabled("PDD_CLOUD_ONLY") or _env_flag_enabled("PDD_NO_LOCAL_FALLBACK")
134
+ current_execution_is_local = is_local_execution_preferred and not cloud_only
135
+
136
+ # Cloud execution is only supported for single-pass mode (not loop mode)
137
+ # because loop mode requires running tests and verification programs locally
138
+ cloud_execution_attempted = False
139
+ cloud_execution_succeeded = False
140
+
141
+ if not loop and not current_execution_is_local:
142
+ if verbose:
143
+ console.print(Panel("Attempting cloud fix execution...", title="[blue]Mode[/blue]", expand=False))
144
+
145
+ jwt_token = CloudConfig.get_jwt_token(verbose=verbose)
146
+
147
+ if not jwt_token:
148
+ if cloud_only:
149
+ console.print("[red]Cloud authentication failed.[/red]")
150
+ raise click.UsageError("Cloud authentication failed")
151
+ console.print("[yellow]Cloud authentication failed. Falling back to local execution.[/yellow]")
152
+ current_execution_is_local = True
153
+
154
+ if jwt_token and not current_execution_is_local:
155
+ cloud_execution_attempted = True
156
+ # Build cloud payload
157
+ payload = {
158
+ "unitTest": input_strings["unit_test_file"],
159
+ "code": input_strings["code_file"],
160
+ "prompt": input_strings["prompt_file"],
161
+ "errors": input_strings.get("error_file", ""),
162
+ "language": get_language(os.path.splitext(code_file)[1]),
163
+ "strength": strength,
164
+ "temperature": temperature,
165
+ "time": time if time is not None else 0.25,
166
+ "verbose": verbose,
167
+ }
168
+
169
+ headers = {
170
+ "Authorization": f"Bearer {jwt_token}",
171
+ "Content-Type": "application/json"
172
+ }
173
+ cloud_url = CloudConfig.get_endpoint_url("fixCode")
174
+
175
+ try:
176
+ response = requests.post(
177
+ cloud_url,
178
+ json=payload,
179
+ headers=headers,
180
+ timeout=CLOUD_REQUEST_TIMEOUT
181
+ )
182
+ response.raise_for_status()
183
+
184
+ response_data = response.json()
185
+ fixed_unit_test = response_data.get("fixedUnitTest", "")
186
+ fixed_code = response_data.get("fixedCode", "")
187
+ analysis_results = response_data.get("analysis", "")
188
+ total_cost = float(response_data.get("totalCost", 0.0))
189
+ model_name = response_data.get("modelName", "cloud_model")
190
+ success = response_data.get("success", False)
191
+ update_unit_test = response_data.get("updateUnitTest", False)
192
+ update_code = response_data.get("updateCode", False)
193
+
194
+ if not (fixed_unit_test or fixed_code):
195
+ if cloud_only:
196
+ console.print("[red]Cloud execution returned no fixed code.[/red]")
197
+ raise click.UsageError("Cloud execution returned no fixed code")
198
+ console.print("[yellow]Cloud execution returned no fixed code. Falling back to local.[/yellow]")
199
+ current_execution_is_local = True
200
+ else:
201
+ cloud_execution_succeeded = True
202
+ attempts = 1
203
+
204
+ # Validate the fix by running tests (same as local)
205
+ if update_unit_test or update_code:
206
+ import tempfile
207
+ import shutil as shutil_module
208
+
209
+ test_dir = tempfile.mkdtemp(prefix="pdd_fix_validate_")
210
+ temp_test_file = os.path.join(test_dir, "test_temp.py")
211
+ temp_code_file = os.path.join(test_dir, "code_temp.py")
212
+
213
+ try:
214
+ test_content = fixed_unit_test if fixed_unit_test else input_strings["unit_test_file"]
215
+ code_content = fixed_code if fixed_code else input_strings["code_file"]
216
+
217
+ with open(temp_test_file, 'w') as f:
218
+ f.write(test_content)
219
+ with open(temp_code_file, 'w') as f:
220
+ f.write(code_content)
221
+
222
+ fails, errors_count, warnings, test_output = run_pytest_on_file(temp_test_file)
223
+ success = (fails == 0 and errors_count == 0)
224
+
225
+ if verbose:
226
+ rprint(f"[cyan]Fix validation: {fails} failures, {errors_count} errors, {warnings} warnings[/cyan]")
227
+ if not success:
228
+ rprint("[yellow]Fix suggested by cloud did not pass tests[/yellow]")
229
+ finally:
230
+ try:
231
+ shutil_module.rmtree(test_dir)
232
+ except Exception:
233
+ pass
234
+ else:
235
+ success = False
236
+
237
+ if verbose:
238
+ console.print(Panel(
239
+ f"Cloud fix completed. Model: {model_name}, Cost: ${total_cost:.6f}",
240
+ title="[green]Cloud Success[/green]",
241
+ expand=False
242
+ ))
243
+
244
+ except requests.exceptions.Timeout:
245
+ if cloud_only:
246
+ console.print(f"[red]Cloud execution timed out ({CLOUD_REQUEST_TIMEOUT}s).[/red]")
247
+ raise click.UsageError("Cloud execution timed out")
248
+ console.print(f"[yellow]Cloud execution timed out ({CLOUD_REQUEST_TIMEOUT}s). Falling back to local.[/yellow]")
249
+ current_execution_is_local = True
250
+
251
+ except requests.exceptions.HTTPError as e:
252
+ status_code = e.response.status_code if e.response else 0
253
+ err_content = e.response.text[:200] if e.response else "No response content"
254
+
255
+ # Non-recoverable errors: do NOT fall back to local
256
+ if status_code == 402: # Insufficient credits
257
+ try:
258
+ error_data = e.response.json()
259
+ current_balance = error_data.get("currentBalance", "unknown")
260
+ estimated_cost = error_data.get("estimatedCost", "unknown")
261
+ console.print(f"[red]Insufficient credits. Current balance: {current_balance}, estimated cost: {estimated_cost}[/red]")
262
+ except Exception:
263
+ console.print(f"[red]Insufficient credits: {err_content}[/red]")
264
+ raise click.UsageError("Insufficient credits for cloud fix")
265
+ elif status_code == 401: # Authentication error
266
+ console.print(f"[red]Authentication failed: {err_content}[/red]")
267
+ raise click.UsageError("Cloud authentication failed")
268
+ elif status_code == 403: # Authorization error (not approved)
269
+ console.print(f"[red]Access denied: {err_content}[/red]")
270
+ raise click.UsageError("Access denied - user not approved")
271
+ elif status_code == 400: # Validation error
272
+ console.print(f"[red]Invalid request: {err_content}[/red]")
273
+ raise click.UsageError(f"Invalid request: {err_content}")
274
+ else:
275
+ # Recoverable errors (5xx, unexpected errors): fall back to local
276
+ if cloud_only:
277
+ console.print(f"[red]Cloud HTTP error ({status_code}): {err_content}[/red]")
278
+ raise click.UsageError(f"Cloud HTTP error ({status_code}): {err_content}")
279
+ console.print(f"[yellow]Cloud HTTP error ({status_code}): {err_content}. Falling back to local.[/yellow]")
280
+ current_execution_is_local = True
281
+
282
+ except requests.exceptions.RequestException as e:
283
+ if cloud_only:
284
+ console.print(f"[red]Cloud network error: {e}[/red]")
285
+ raise click.UsageError(f"Cloud network error: {e}")
286
+ console.print(f"[yellow]Cloud network error: {e}. Falling back to local.[/yellow]")
287
+ current_execution_is_local = True
288
+
289
+ except json.JSONDecodeError:
290
+ if cloud_only:
291
+ console.print("[red]Cloud returned invalid JSON.[/red]")
292
+ raise click.UsageError("Cloud returned invalid JSON")
293
+ console.print("[yellow]Cloud returned invalid JSON. Falling back to local.[/yellow]")
294
+ current_execution_is_local = True
295
+
296
+ # Local execution path (for loop mode or when cloud failed/skipped)
114
297
  if loop:
115
- # Use fix_error_loop for iterative fixing
298
+ # Determine if loop should use cloud for LLM calls (hybrid mode)
299
+ # Local test execution stays local, but LLM fix calls can go to cloud
300
+ use_cloud_for_loop = not is_local_execution_preferred and not cloud_only
301
+
302
+ # If cloud_only is set but we're in loop mode, we still use hybrid approach
303
+ if cloud_only and not is_local_execution_preferred:
304
+ use_cloud_for_loop = True
305
+
306
+ if verbose:
307
+ mode_desc = "hybrid (local tests + cloud LLM)" if use_cloud_for_loop else "local"
308
+ console.print(Panel(f"Performing {mode_desc} fix loop...", title="[blue]Mode[/blue]", expand=False))
309
+
116
310
  success, fixed_unit_test, fixed_code, attempts, total_cost, model_name = fix_error_loop(
117
311
  unit_test_file=unit_test_file,
118
312
  code_file=code_file,
@@ -126,10 +320,13 @@ def fix_main(
126
320
  budget=budget,
127
321
  error_log_file=output_file_paths.get("output_results"),
128
322
  verbose=verbose,
129
- agentic_fallback=agentic_fallback
323
+ agentic_fallback=agentic_fallback,
324
+ use_cloud=use_cloud_for_loop
130
325
  )
131
- else:
132
- # Use fix_errors_from_unit_tests for single-pass fixing
326
+ elif not cloud_execution_succeeded:
327
+ # Use fix_errors_from_unit_tests for single-pass fixing (local fallback)
328
+ if verbose:
329
+ console.print(Panel("Performing local fix...", title="[blue]Mode[/blue]", expand=False))
133
330
  update_unit_test, update_code, fixed_unit_test, fixed_code, analysis_results, total_cost, model_name = fix_errors_from_unit_tests(
134
331
  unit_test=input_strings["unit_test_file"],
135
332
  code=input_strings["code_file"],
@@ -342,6 +539,9 @@ def fix_main(
342
539
  except click.Abort:
343
540
  # User cancelled - re-raise to stop the sync loop
344
541
  raise
542
+ except click.UsageError:
543
+ # Re-raise UsageError for proper CLI handling (e.g., cloud auth failures, insufficient credits)
544
+ raise
345
545
  except Exception as e:
346
546
  if not ctx.obj.get('quiet', False):
347
547
  # Safely handle and print MarkupError
@@ -1,3 +1,4 @@
1
+ import json
1
2
  import os
2
3
  import shutil
3
4
  import subprocess
@@ -8,6 +9,8 @@ from typing import Dict, Tuple, Any, Optional
8
9
  from xml.sax.saxutils import escape
9
10
  import time
10
11
 
12
+ import requests
13
+
11
14
  from rich.console import Console
12
15
 
13
16
  # Use relative import assuming fix_verification_errors is in the same package
@@ -31,6 +34,78 @@ from .get_language import get_language
31
34
  from .agentic_langtest import default_verify_cmd_for
32
35
  from .agentic_verify import run_agentic_verify
33
36
 
37
+ # Cloud configuration
38
+ try:
39
+ from .core.cloud import CloudConfig
40
+ CLOUD_AVAILABLE = True
41
+ except ImportError:
42
+ CLOUD_AVAILABLE = False
43
+ CloudConfig = None
44
+
45
+ # Cloud request timeout for verify fix
46
+ CLOUD_REQUEST_TIMEOUT = 400 # seconds
47
+
48
+
49
+ def cloud_verify_fix(
50
+ program: str,
51
+ prompt: str,
52
+ code: str,
53
+ output: str,
54
+ strength: float,
55
+ temperature: float,
56
+ time_param: float,
57
+ verbose: bool,
58
+ language: str = "python",
59
+ ) -> Dict[str, Any]:
60
+ """
61
+ Call cloud verifyCode endpoint for LLM verification fix.
62
+
63
+ Returns:
64
+ Dict with keys: fixed_code, fixed_program, explanation, verification_issues_count, total_cost, model_name
65
+ """
66
+ if not CLOUD_AVAILABLE or CloudConfig is None:
67
+ raise RuntimeError("Cloud configuration not available")
68
+
69
+ jwt_token = CloudConfig.get_jwt_token(verbose=verbose)
70
+ if not jwt_token:
71
+ raise RuntimeError("Cloud authentication failed - no JWT token")
72
+
73
+ payload = {
74
+ "programContent": program,
75
+ "promptContent": prompt,
76
+ "codeContent": code,
77
+ "outputContent": output,
78
+ "language": language,
79
+ "strength": strength,
80
+ "temperature": temperature,
81
+ "time": time_param if time_param is not None else 0.25,
82
+ "verbose": verbose,
83
+ }
84
+
85
+ headers = {
86
+ "Authorization": f"Bearer {jwt_token}",
87
+ "Content-Type": "application/json"
88
+ }
89
+ cloud_url = CloudConfig.get_endpoint_url("verifyCode")
90
+
91
+ response = requests.post(
92
+ cloud_url,
93
+ json=payload,
94
+ headers=headers,
95
+ timeout=CLOUD_REQUEST_TIMEOUT
96
+ )
97
+ response.raise_for_status()
98
+
99
+ response_data = response.json()
100
+ return {
101
+ "fixed_code": response_data.get("fixedCode", code),
102
+ "fixed_program": response_data.get("fixedProgram", program),
103
+ "explanation": response_data.get("explanation", ""),
104
+ "verification_issues_count": response_data.get("issuesCount", 0),
105
+ "total_cost": float(response_data.get("totalCost", 0.0)),
106
+ "model_name": response_data.get("modelName", "cloud_model"),
107
+ }
108
+
34
109
  def _normalize_agentic_result(result):
35
110
  """
36
111
  Normalize run_agentic_verify result into: (success: bool, msg: str, cost: float, model: str, changed_files: List[str])
@@ -156,11 +231,17 @@ def fix_verification_errors_loop(
156
231
  program_args: Optional[list[str]] = None,
157
232
  llm_time: float = DEFAULT_TIME, # Add time parameter
158
233
  agentic_fallback: bool = True,
234
+ use_cloud: bool = False,
159
235
  ) -> Dict[str, Any]:
160
236
  """
161
237
  Attempts to fix errors in a code file based on program execution output
162
238
  against the prompt's intent, iterating multiple times with secondary verification.
163
239
 
240
+ Hybrid Cloud Support:
241
+ When use_cloud=True, the LLM fix calls are routed to the cloud verifyCode endpoint
242
+ while local program execution stays local. This allows the loop to pass local
243
+ verification results to the cloud for analysis and fixes.
244
+
164
245
  Args:
165
246
  program_file: Path to the Python program exercising the code.
166
247
  code_file: Path to the code file being tested/verified.
@@ -178,6 +259,7 @@ def fix_verification_errors_loop(
178
259
  program_args: Optional list of command-line arguments for the program_file.
179
260
  llm_time: Time parameter for fix_verification_errors calls (default: DEFAULT_TIME).
180
261
  agentic_fallback: Enable agentic fallback if the primary fix mechanism fails.
262
+ use_cloud: If True, use cloud LLM for fix calls while keeping verification execution local.
181
263
 
182
264
  Returns:
183
265
  A dictionary containing:
@@ -196,8 +278,55 @@ def fix_verification_errors_loop(
196
278
  lang = get_language(os.path.splitext(code_file)[1])
197
279
  verify_cmd = default_verify_cmd_for(lang, verification_program)
198
280
  if not verify_cmd:
199
- raise ValueError(f"No default verification command for language: {lang}")
200
-
281
+ # No verify command available (e.g., Java without maven/gradle).
282
+ # Trigger agentic fallback directly.
283
+ console.print(f"[cyan]No verification command for {lang}. Triggering agentic fallback directly...[/cyan]")
284
+ verification_log_path = Path(verification_log_file)
285
+ verification_log_path.parent.mkdir(parents=True, exist_ok=True)
286
+ # Create minimal error log if it doesn't exist
287
+ if not verification_log_path.exists() or verification_log_path.stat().st_size == 0:
288
+ with open(verification_log_path, "w") as f:
289
+ f.write(f"No verification command available for language: {lang}\n")
290
+ f.write("Agentic fix will attempt to resolve the issue.\n")
291
+
292
+ agent_cwd = Path(prompt_file).parent if prompt_file else None
293
+ console.print(f"[cyan]Attempting agentic verify fallback (prompt_file={prompt_file!r})...[/cyan]")
294
+ success, agent_msg, agent_cost, agent_model, agent_changed_files = _safe_run_agentic_verify(
295
+ prompt_file=prompt_file,
296
+ code_file=code_file,
297
+ program_file=verification_program,
298
+ verification_log_file=verification_log_file,
299
+ verbose=verbose,
300
+ cwd=agent_cwd,
301
+ )
302
+ if not success:
303
+ console.print(f"[bold red]Agentic verify fallback failed: {agent_msg}[/bold red]")
304
+ if agent_changed_files:
305
+ console.print(f"[cyan]Agent modified {len(agent_changed_files)} file(s):[/cyan]")
306
+ for f in agent_changed_files:
307
+ console.print(f" • {f}")
308
+ final_program = ""
309
+ final_code = ""
310
+ try:
311
+ with open(verification_program, "r") as f:
312
+ final_program = f.read()
313
+ except Exception:
314
+ pass
315
+ try:
316
+ with open(code_file, "r") as f:
317
+ final_code = f.read()
318
+ except Exception:
319
+ pass
320
+ return {
321
+ "success": success,
322
+ "final_program": final_program,
323
+ "final_code": final_code,
324
+ "total_attempts": 1,
325
+ "total_cost": agent_cost,
326
+ "model_name": agent_model,
327
+ "statistics": {},
328
+ }
329
+
201
330
  verify_result = subprocess.run(verify_cmd, capture_output=True, text=True, shell=True)
202
331
  pytest_output = (verify_result.stdout or "") + "\n" + (verify_result.stderr or "")
203
332
  console.print("[cyan]Non-Python target detected. Triggering agentic fallback...[/cyan]")
@@ -310,6 +439,11 @@ def fix_verification_errors_loop(
310
439
  program_contents = "" # Keep track of current contents
311
440
  code_contents = "" # Keep track of current contents
312
441
 
442
+ # Create backup directory in .pdd/backups/ to avoid polluting code/test directories
443
+ backup_timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
444
+ backup_dir = Path.cwd() / '.pdd' / 'backups' / code_path.stem / backup_timestamp
445
+ backup_dir.mkdir(parents=True, exist_ok=True)
446
+
313
447
  # --- Step 3: Determine Initial State ---
314
448
  if verbose:
315
449
  console.print("[bold cyan]Step 3: Determining Initial State...[/bold cyan]")
@@ -324,6 +458,21 @@ def fix_verification_errors_loop(
324
458
  stats['status_message'] = f'Error reading initial files: {e}' # Add status message
325
459
  return {"success": False, "final_program": "", "final_code": "", "total_attempts": 0, "total_cost": 0.0, "model_name": None, "statistics": stats}
326
460
 
461
+ # 3a-pre: Validate code file is not empty (prevents infinite loops with empty content)
462
+ if not initial_code_content or len(initial_code_content.strip()) == 0:
463
+ error_msg = f"Code file is empty or contains only whitespace: {code_path}"
464
+ console.print(f"[bold red]Error: {error_msg}[/bold red]")
465
+ stats['status_message'] = f'Error: Code file is empty - cannot verify'
466
+ return {
467
+ "success": False,
468
+ "final_program": initial_program_content,
469
+ "final_code": "",
470
+ "total_attempts": 0,
471
+ "total_cost": 0.0,
472
+ "model_name": None,
473
+ "statistics": stats
474
+ }
475
+
327
476
  # 3a: Run initial program with args
328
477
  initial_return_code, initial_output = _run_program(program_path, args=program_args)
329
478
  if verbose:
@@ -370,16 +519,46 @@ def fix_verification_errors_loop(
370
519
  if verbose:
371
520
  console.print("Running initial assessment with fix_verification_errors...")
372
521
  # Use actual strength/temp for realistic initial assessment
373
- initial_fix_result = fix_verification_errors(
374
- program=initial_program_content,
375
- prompt=prompt,
376
- code=initial_code_content,
377
- output=initial_output,
378
- strength=strength,
379
- temperature=temperature,
380
- verbose=verbose,
381
- time=llm_time # Pass time
382
- )
522
+ # Use cloud or local based on use_cloud parameter
523
+ if use_cloud:
524
+ try:
525
+ initial_fix_result = cloud_verify_fix(
526
+ program=initial_program_content,
527
+ prompt=prompt,
528
+ code=initial_code_content,
529
+ output=initial_output,
530
+ strength=strength,
531
+ temperature=temperature,
532
+ time_param=llm_time,
533
+ verbose=verbose,
534
+ language="python" if is_python else get_language(os.path.splitext(code_file)[1]),
535
+ )
536
+ if verbose:
537
+ console.print(f"[cyan]Cloud verify fix completed.[/cyan]")
538
+ except (requests.exceptions.RequestException, RuntimeError) as cloud_err:
539
+ # Cloud failed - fall back to local
540
+ console.print(f"[yellow]Cloud verify fix failed: {cloud_err}. Falling back to local.[/yellow]")
541
+ initial_fix_result = fix_verification_errors(
542
+ program=initial_program_content,
543
+ prompt=prompt,
544
+ code=initial_code_content,
545
+ output=initial_output,
546
+ strength=strength,
547
+ temperature=temperature,
548
+ verbose=verbose,
549
+ time=llm_time
550
+ )
551
+ else:
552
+ initial_fix_result = fix_verification_errors(
553
+ program=initial_program_content,
554
+ prompt=prompt,
555
+ code=initial_code_content,
556
+ output=initial_output,
557
+ strength=strength,
558
+ temperature=temperature,
559
+ verbose=verbose,
560
+ time=llm_time # Pass time
561
+ )
383
562
  # 3e: Add cost
384
563
  initial_cost = initial_fix_result.get('total_cost', 0.0)
385
564
  total_cost += initial_cost
@@ -539,9 +718,9 @@ def fix_verification_errors_loop(
539
718
  # code_contents = code_path.read_text(encoding="utf-8")
540
719
  # except IOError as e: ...
541
720
 
542
- # 4d: Create backups
543
- program_backup_path = program_path.with_stem(f"{program_path.stem}_iteration_{current_attempt}").with_suffix(program_path.suffix)
544
- code_backup_path = code_path.with_stem(f"{code_path.stem}_iteration_{current_attempt}").with_suffix(code_path.suffix)
721
+ # 4d: Create backups in .pdd/backups/ (backup_dir already created above)
722
+ program_backup_path = backup_dir / f"program_{current_attempt}{program_path.suffix}"
723
+ code_backup_path = backup_dir / f"code_{current_attempt}{code_path.suffix}"
545
724
  try:
546
725
  # Copy from the *current* state before this iteration's fix
547
726
  program_path.write_text(program_contents, encoding="utf-8") # Ensure file matches memory state
@@ -561,7 +740,7 @@ def fix_verification_errors_loop(
561
740
  stats['status_message'] = f'Error creating backups on attempt {current_attempt}'
562
741
  break # Don't proceed without backups
563
742
 
564
- # 4e: Call fix_verification_errors
743
+ # 4e: Call fix_verification_errors (cloud or local based on use_cloud parameter)
565
744
  iteration_log_xml += f' <InputsToFixer>\n'
566
745
  iteration_log_xml += f' <Program>{escape(program_contents)}</Program>\n'
567
746
  iteration_log_xml += f' <Code>{escape(code_contents)}</Code>\n'
@@ -573,16 +752,46 @@ def fix_verification_errors_loop(
573
752
  try:
574
753
  if verbose:
575
754
  console.print("Calling fix_verification_errors...")
576
- fix_result = fix_verification_errors(
577
- program=program_contents,
578
- prompt=prompt,
579
- code=code_contents,
580
- output=program_output,
581
- strength=strength,
582
- temperature=temperature,
583
- verbose=verbose,
584
- time=llm_time # Pass time
585
- )
755
+ # Use cloud or local based on use_cloud parameter
756
+ if use_cloud:
757
+ try:
758
+ fix_result = cloud_verify_fix(
759
+ program=program_contents,
760
+ prompt=prompt,
761
+ code=code_contents,
762
+ output=program_output,
763
+ strength=strength,
764
+ temperature=temperature,
765
+ time_param=llm_time,
766
+ verbose=verbose,
767
+ language="python" if is_python else get_language(os.path.splitext(code_file)[1]),
768
+ )
769
+ if verbose:
770
+ console.print(f"[cyan]Cloud verify fix completed.[/cyan]")
771
+ except (requests.exceptions.RequestException, RuntimeError) as cloud_err:
772
+ # Cloud failed - fall back to local
773
+ console.print(f"[yellow]Cloud verify fix failed: {cloud_err}. Falling back to local.[/yellow]")
774
+ fix_result = fix_verification_errors(
775
+ program=program_contents,
776
+ prompt=prompt,
777
+ code=code_contents,
778
+ output=program_output,
779
+ strength=strength,
780
+ temperature=temperature,
781
+ verbose=verbose,
782
+ time=llm_time
783
+ )
784
+ else:
785
+ fix_result = fix_verification_errors(
786
+ program=program_contents,
787
+ prompt=prompt,
788
+ code=code_contents,
789
+ output=program_output,
790
+ strength=strength,
791
+ temperature=temperature,
792
+ verbose=verbose,
793
+ time=llm_time # Pass time
794
+ )
586
795
 
587
796
  # 4f: Add cost
588
797
  attempt_cost = fix_result.get('total_cost', 0.0)