pdd-cli 0.0.42__py3-none-any.whl → 0.0.90__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 (119) hide show
  1. pdd/__init__.py +4 -4
  2. pdd/agentic_common.py +863 -0
  3. pdd/agentic_crash.py +534 -0
  4. pdd/agentic_fix.py +1179 -0
  5. pdd/agentic_langtest.py +162 -0
  6. pdd/agentic_update.py +370 -0
  7. pdd/agentic_verify.py +183 -0
  8. pdd/auto_deps_main.py +15 -5
  9. pdd/auto_include.py +63 -5
  10. pdd/bug_main.py +3 -2
  11. pdd/bug_to_unit_test.py +2 -0
  12. pdd/change_main.py +11 -4
  13. pdd/cli.py +22 -1181
  14. pdd/cmd_test_main.py +80 -19
  15. pdd/code_generator.py +58 -18
  16. pdd/code_generator_main.py +672 -25
  17. pdd/commands/__init__.py +42 -0
  18. pdd/commands/analysis.py +248 -0
  19. pdd/commands/fix.py +140 -0
  20. pdd/commands/generate.py +257 -0
  21. pdd/commands/maintenance.py +174 -0
  22. pdd/commands/misc.py +79 -0
  23. pdd/commands/modify.py +230 -0
  24. pdd/commands/report.py +144 -0
  25. pdd/commands/templates.py +215 -0
  26. pdd/commands/utility.py +110 -0
  27. pdd/config_resolution.py +58 -0
  28. pdd/conflicts_main.py +8 -3
  29. pdd/construct_paths.py +281 -81
  30. pdd/context_generator.py +10 -2
  31. pdd/context_generator_main.py +113 -11
  32. pdd/continue_generation.py +47 -7
  33. pdd/core/__init__.py +0 -0
  34. pdd/core/cli.py +503 -0
  35. pdd/core/dump.py +554 -0
  36. pdd/core/errors.py +63 -0
  37. pdd/core/utils.py +90 -0
  38. pdd/crash_main.py +44 -11
  39. pdd/data/language_format.csv +71 -62
  40. pdd/data/llm_model.csv +20 -18
  41. pdd/detect_change_main.py +5 -4
  42. pdd/fix_code_loop.py +331 -77
  43. pdd/fix_error_loop.py +209 -60
  44. pdd/fix_errors_from_unit_tests.py +4 -3
  45. pdd/fix_main.py +75 -18
  46. pdd/fix_verification_errors.py +12 -100
  47. pdd/fix_verification_errors_loop.py +319 -272
  48. pdd/fix_verification_main.py +57 -17
  49. pdd/generate_output_paths.py +93 -10
  50. pdd/generate_test.py +16 -5
  51. pdd/get_jwt_token.py +48 -9
  52. pdd/get_run_command.py +73 -0
  53. pdd/get_test_command.py +68 -0
  54. pdd/git_update.py +70 -19
  55. pdd/increase_tests.py +7 -0
  56. pdd/incremental_code_generator.py +2 -2
  57. pdd/insert_includes.py +11 -3
  58. pdd/llm_invoke.py +1278 -110
  59. pdd/load_prompt_template.py +36 -10
  60. pdd/pdd_completion.fish +25 -2
  61. pdd/pdd_completion.sh +30 -4
  62. pdd/pdd_completion.zsh +79 -4
  63. pdd/postprocess.py +10 -3
  64. pdd/preprocess.py +228 -15
  65. pdd/preprocess_main.py +8 -5
  66. pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
  67. pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
  68. pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
  69. pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
  70. pdd/prompts/agentic_update_LLM.prompt +1071 -0
  71. pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
  72. pdd/prompts/auto_include_LLM.prompt +98 -101
  73. pdd/prompts/change_LLM.prompt +1 -3
  74. pdd/prompts/detect_change_LLM.prompt +562 -3
  75. pdd/prompts/example_generator_LLM.prompt +22 -1
  76. pdd/prompts/extract_code_LLM.prompt +5 -1
  77. pdd/prompts/extract_program_code_fix_LLM.prompt +14 -2
  78. pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
  79. pdd/prompts/extract_promptline_LLM.prompt +17 -11
  80. pdd/prompts/find_verification_errors_LLM.prompt +6 -0
  81. pdd/prompts/fix_code_module_errors_LLM.prompt +16 -4
  82. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +6 -41
  83. pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
  84. pdd/prompts/generate_test_LLM.prompt +21 -6
  85. pdd/prompts/increase_tests_LLM.prompt +1 -2
  86. pdd/prompts/insert_includes_LLM.prompt +1181 -6
  87. pdd/prompts/split_LLM.prompt +1 -62
  88. pdd/prompts/trace_LLM.prompt +25 -22
  89. pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
  90. pdd/prompts/update_prompt_LLM.prompt +22 -1
  91. pdd/prompts/xml_convertor_LLM.prompt +3246 -7
  92. pdd/pytest_output.py +188 -21
  93. pdd/python_env_detector.py +151 -0
  94. pdd/render_mermaid.py +236 -0
  95. pdd/setup_tool.py +648 -0
  96. pdd/simple_math.py +2 -0
  97. pdd/split_main.py +3 -2
  98. pdd/summarize_directory.py +56 -7
  99. pdd/sync_determine_operation.py +918 -186
  100. pdd/sync_main.py +82 -32
  101. pdd/sync_orchestration.py +1456 -453
  102. pdd/sync_tui.py +848 -0
  103. pdd/template_registry.py +264 -0
  104. pdd/templates/architecture/architecture_json.prompt +242 -0
  105. pdd/templates/generic/generate_prompt.prompt +174 -0
  106. pdd/trace.py +168 -12
  107. pdd/trace_main.py +4 -3
  108. pdd/track_cost.py +151 -61
  109. pdd/unfinished_prompt.py +49 -3
  110. pdd/update_main.py +549 -67
  111. pdd/update_model_costs.py +2 -2
  112. pdd/update_prompt.py +19 -4
  113. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/METADATA +20 -7
  114. pdd_cli-0.0.90.dist-info/RECORD +153 -0
  115. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/licenses/LICENSE +1 -1
  116. pdd_cli-0.0.42.dist-info/RECORD +0 -115
  117. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/WHEEL +0 -0
  118. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/entry_points.txt +0 -0
  119. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/top_level.txt +0 -0
pdd/fix_code_loop.py CHANGED
@@ -1,30 +1,201 @@
1
+ from __future__ import annotations
1
2
  import os
2
3
  import shutil
3
4
  import subprocess
4
5
  import sys
6
+ import threading
5
7
  from pathlib import Path
6
- from typing import Tuple
7
- from . import DEFAULT_TIME # Added DEFAULT_TIME
8
+ from typing import Tuple, Optional, Union, List
9
+
10
+ # Try to import DEFAULT_TIME, with fallback
11
+ try:
12
+ from . import DEFAULT_TIME
13
+ except ImportError:
14
+ DEFAULT_TIME = 0.5
15
+
16
+ # Try to import agentic modules, with fallbacks
17
+ try:
18
+ from .agentic_crash import run_agentic_crash
19
+ except ImportError:
20
+ def run_agentic_crash(**kwargs):
21
+ return (False, "Agentic crash handler not available", 0.0, "N/A", [])
22
+
23
+ try:
24
+ from .get_language import get_language
25
+ except ImportError:
26
+ def get_language(ext):
27
+ return "unknown"
28
+
29
+ try:
30
+ from .agentic_langtest import default_verify_cmd_for
31
+ except ImportError:
32
+ def default_verify_cmd_for(lang, verification_program):
33
+ return None
34
+
35
+ def _normalize_agentic_result(result):
36
+ """
37
+ Normalize run_agentic_crash result into: (success: bool, msg: str, cost: float, model: str, changed_files: List[str])
38
+ Handles older 2/3/4-tuple shapes used by tests/monkeypatches.
39
+ """
40
+ if isinstance(result, tuple):
41
+ if len(result) == 5:
42
+ ok, msg, cost, model, changed_files = result
43
+ return bool(ok), str(msg), float(cost), str(model or "agentic-cli"), list(changed_files or [])
44
+ if len(result) == 4:
45
+ ok, msg, cost, model = result
46
+ return bool(ok), str(msg), float(cost), str(model or "agentic-cli"), []
47
+ if len(result) == 3:
48
+ ok, msg, cost = result
49
+ return bool(ok), str(msg), float(cost), "agentic-cli", []
50
+ if len(result) == 2:
51
+ ok, msg = result
52
+ return bool(ok), str(msg), 0.0, "agentic-cli", []
53
+ # Fallback (shouldn't happen)
54
+ return False, "Invalid agentic result shape", 0.0, "agentic-cli", []
55
+
56
+ def _safe_run_agentic_crash(*, prompt_file, code_file, program_file, crash_log_file, cwd=None):
57
+ """
58
+ Call (possibly monkeypatched) run_agentic_crash and normalize its return.
59
+ Maps arguments to the expected signature of run_agentic_crash.
60
+
61
+ Note: cwd parameter is accepted for compatibility but not passed to run_agentic_crash
62
+ as it determines the working directory from prompt_file.parent internally.
63
+ """
64
+ if not prompt_file:
65
+ return False, "Agentic fix requires a valid prompt file.", 0.0, "agentic-cli", []
66
+
67
+ try:
68
+ # Ensure inputs are Path objects as expected by run_agentic_crash
69
+ call_args = {
70
+ "prompt_file": Path(prompt_file),
71
+ "code_file": Path(code_file),
72
+ "program_file": Path(program_file),
73
+ "crash_log_file": Path(crash_log_file),
74
+ "verbose": True,
75
+ "quiet": False,
76
+ }
77
+ # Note: cwd is not passed - run_agentic_crash uses prompt_file.parent as project root
78
+
79
+ res = run_agentic_crash(**call_args)
80
+ return _normalize_agentic_result(res)
81
+ except Exception as e:
82
+ return False, f"Agentic crash handler failed: {e}", 0.0, "agentic-cli", []
8
83
 
9
84
  # Use Rich for pretty printing to the console
10
- from rich.console import Console
11
- # Initialize Rich Console
12
- console = Console(record=True)
13
- rprint = console.print
85
+ try:
86
+ from rich.console import Console
87
+ console = Console(record=True)
88
+ rprint = console.print
89
+ except ImportError:
90
+ # Fallback if Rich is not available
91
+ def rprint(*args, **kwargs):
92
+ print(*args)
14
93
 
15
94
  # Use relative import for internal modules
16
95
  try:
17
- # Attempt relative import for package context
18
96
  from .fix_code_module_errors import fix_code_module_errors
19
97
  except ImportError:
20
- # Fallback for script execution context (e.g., testing)
21
- # This assumes fix_code_module_errors.py is in the same directory or Python path
22
- # You might need to adjust this based on your project structure during testing
23
- print("Warning: Relative import failed. Attempting direct import for fix_code_module_errors.", file=sys.stderr)
24
- # Add parent directory to sys.path if necessary for testing outside a package
25
- # import sys
26
- # sys.path.append(str(Path(__file__).parent.parent)) # Adjust based on structure
27
- from fix_code_module_errors import fix_code_module_errors
98
+ try:
99
+ from fix_code_module_errors import fix_code_module_errors
100
+ except ImportError:
101
+ # Provide a stub that will fail gracefully
102
+ def fix_code_module_errors(**kwargs):
103
+ return (False, False, "", "", "Module not available", 0.0, None)
104
+
105
+
106
+ class ProcessResult:
107
+ def __init__(self, returncode, stdout, stderr):
108
+ self.returncode = returncode
109
+ self.stdout = stdout
110
+ self.stderr = stderr
111
+
112
+ def run_process_with_output(cmd_args, timeout=300):
113
+ """
114
+ Runs a process, streaming stdout/stderr to the console while capturing them.
115
+ Allows interaction via stdin.
116
+
117
+ Uses start_new_session=True to create a new process group, allowing us to
118
+ kill all child processes if the main process times out.
119
+ """
120
+ import os
121
+ import signal
122
+
123
+ try:
124
+ proc = subprocess.Popen(
125
+ cmd_args,
126
+ stdin=subprocess.DEVNULL,
127
+ stdout=subprocess.PIPE,
128
+ stderr=subprocess.PIPE,
129
+ bufsize=0,
130
+ start_new_session=True # Create new process group for clean termination
131
+ )
132
+ except Exception as e:
133
+ return -1, "", str(e)
134
+
135
+ captured_stdout = []
136
+ captured_stderr = []
137
+
138
+ def stream_pipe(pipe, sink, capture_list):
139
+ while True:
140
+ try:
141
+ chunk = pipe.read(1)
142
+ if not chunk:
143
+ break
144
+ capture_list.append(chunk)
145
+ except (ValueError, IOError, OSError):
146
+ # OSError can occur when pipe is closed during read
147
+ break
148
+
149
+ t_out = threading.Thread(target=stream_pipe, args=(proc.stdout, sys.stdout, captured_stdout), daemon=True)
150
+ t_err = threading.Thread(target=stream_pipe, args=(proc.stderr, sys.stderr, captured_stderr), daemon=True)
151
+
152
+ t_out.start()
153
+ t_err.start()
154
+
155
+ timed_out = False
156
+ try:
157
+ proc.wait(timeout=timeout)
158
+ except subprocess.TimeoutExpired:
159
+ timed_out = True
160
+ captured_stderr.append(b"\n[Timeout]\n")
161
+
162
+ # Kill process and entire process group if needed
163
+ if timed_out or proc.returncode is None:
164
+ try:
165
+ # Kill entire process group to handle forked children
166
+ os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
167
+ except (ProcessLookupError, OSError):
168
+ # Process group may already be dead
169
+ pass
170
+ try:
171
+ proc.kill()
172
+ proc.wait(timeout=5)
173
+ except Exception:
174
+ pass
175
+
176
+ # Close pipes to unblock reader threads
177
+ try:
178
+ proc.stdout.close()
179
+ except Exception:
180
+ pass
181
+ try:
182
+ proc.stderr.close()
183
+ except Exception:
184
+ pass
185
+
186
+ # Wait for threads with timeout to prevent indefinite hangs
187
+ THREAD_JOIN_TIMEOUT = 5 # seconds
188
+ t_out.join(timeout=THREAD_JOIN_TIMEOUT)
189
+ t_err.join(timeout=THREAD_JOIN_TIMEOUT)
190
+
191
+ # If threads are still alive after timeout, log it (they're daemon threads so won't block exit)
192
+ if t_out.is_alive() or t_err.is_alive():
193
+ captured_stderr.append(b"\n[Thread join timeout - some output may be lost]\n")
194
+
195
+ stdout_str = b"".join(captured_stdout).decode('utf-8', errors='replace')
196
+ stderr_str = b"".join(captured_stderr).decode('utf-8', errors='replace')
197
+
198
+ return proc.returncode if proc.returncode is not None else -1, stdout_str, stderr_str
28
199
 
29
200
 
30
201
  def fix_code_loop(
@@ -38,7 +209,9 @@ def fix_code_loop(
38
209
  error_log_file: str,
39
210
  verbose: bool = False,
40
211
  time: float = DEFAULT_TIME,
41
- ) -> Tuple[bool, str, str, int, float, str | None]:
212
+ prompt_file: str = "",
213
+ agentic_fallback: bool = True,
214
+ ) -> Tuple[bool, str, str, int, float, Optional[str]]:
42
215
  """
43
216
  Attempts to fix errors in a code module through multiple iterations.
44
217
 
@@ -53,6 +226,8 @@ def fix_code_loop(
53
226
  error_log_file: Path to the error log file.
54
227
  verbose: Enable detailed logging (default: False).
55
228
  time: Time limit for the LLM calls (default: DEFAULT_TIME).
229
+ prompt_file: Path to the prompt file.
230
+ agentic_fallback: Enable agentic fallback if the primary fix mechanism fails.
56
231
 
57
232
  Returns:
58
233
  Tuple containing the following in order:
@@ -63,15 +238,68 @@ def fix_code_loop(
63
238
  - total_cost (float): Total cost of all fix attempts.
64
239
  - model_name (str | None): Name of the LLM model used (or None if no LLM calls were made).
65
240
  """
66
- # --- Start: Modified File Checks ---
241
+ # Handle default time if passed as None (though signature defaults to DEFAULT_TIME)
242
+ if time is None:
243
+ time = DEFAULT_TIME
244
+
245
+ # --- Start: File Checks ---
67
246
  if not Path(code_file).is_file():
68
- # Raising error for code file is acceptable as it's fundamental
69
247
  raise FileNotFoundError(f"Code file not found: {code_file}")
70
248
  if not Path(verification_program).is_file():
71
- # Handle missing verification program gracefully as per test expectation
72
249
  rprint(f"[bold red]Error: Verification program not found: {verification_program}[/bold red]")
73
250
  return False, "", "", 0, 0.0, None
74
- # --- End: Modified File Checks ---
251
+ # --- End: File Checks ---
252
+
253
+ is_python = str(code_file).lower().endswith(".py")
254
+ if not is_python:
255
+ # For non-Python files, run the verification program to get an initial error state
256
+ rprint(f"[cyan]Non-Python target detected. Running verification program to get initial state...[/cyan]")
257
+ lang = get_language(os.path.splitext(code_file)[1])
258
+ verify_cmd = default_verify_cmd_for(lang, verification_program)
259
+ if not verify_cmd:
260
+ raise ValueError(f"No default verification command for language: {lang}")
261
+
262
+ verify_result = subprocess.run(verify_cmd, capture_output=True, text=True, shell=True)
263
+ pytest_output = (verify_result.stdout or "") + "\n" + (verify_result.stderr or "")
264
+ if verify_result.returncode != 0:
265
+ rprint("[cyan]Non-Python target failed initial verification. Triggering agentic fallback...[/cyan]")
266
+ error_log_path = Path(error_log_file)
267
+ error_log_path.parent.mkdir(parents=True, exist_ok=True)
268
+ with open(error_log_path, "w") as f:
269
+ f.write(pytest_output)
270
+
271
+ success, _msg, agent_cost, agent_model, agent_changed_files = _safe_run_agentic_crash(
272
+ prompt_file=prompt_file,
273
+ code_file=code_file,
274
+ program_file=verification_program,
275
+ crash_log_file=error_log_file,
276
+ cwd=Path(prompt_file).parent if prompt_file else None
277
+ )
278
+ final_program = ""
279
+ final_code = ""
280
+ try:
281
+ with open(verification_program, "r") as f:
282
+ final_program = f.read()
283
+ except Exception:
284
+ pass
285
+ try:
286
+ with open(code_file, "r") as f:
287
+ final_code = f.read()
288
+ except Exception:
289
+ pass
290
+ return success, final_program, final_code, 1, agent_cost, agent_model
291
+ else:
292
+ rprint("[green]Non-Python tests passed. No fix needed.[/green]")
293
+ try:
294
+ final_program = ""
295
+ final_code = ""
296
+ with open(verification_program, "r") as f:
297
+ final_program = f.read()
298
+ with open(code_file, "r") as f:
299
+ final_code = f.read()
300
+ except Exception as e:
301
+ rprint(f"[yellow]Warning: Could not read final files: {e}[/yellow]")
302
+ return True, final_program, final_code, 0, 0.0, "N/A"
75
303
 
76
304
  # Step 1: Remove existing error log file
77
305
  try:
@@ -83,14 +311,13 @@ def fix_code_loop(
83
311
  rprint(f"Error log file not found, no need to remove: {error_log_file}")
84
312
  except OSError as e:
85
313
  rprint(f"[bold red]Error removing log file {error_log_file}: {e}[/bold red]")
86
- # Decide if this is fatal or not; for now, we continue
87
314
 
88
315
  # Step 2: Initialize variables
89
316
  attempts = 0
90
317
  total_cost = 0.0
91
318
  success = False
92
319
  model_name = None
93
- history_log = "<history>\n" # Initialize history log XML root
320
+ history_log = "<history>\n"
94
321
 
95
322
  # Create initial backups before any modifications
96
323
  code_file_path = Path(code_file)
@@ -105,33 +332,41 @@ def fix_code_loop(
105
332
  rprint(f"Created initial backups: {original_code_backup}, {original_program_backup}")
106
333
  except Exception as e:
107
334
  rprint(f"[bold red]Error creating initial backups: {e}[/bold red]")
108
- # If backups fail, we cannot guarantee restoration. Return failure.
109
335
  return False, "", "", 0, 0.0, None
110
336
 
337
+ # Initialize process for scope
338
+ process = None
111
339
 
112
340
  # Step 3: Enter the fixing loop
113
341
  while attempts < max_attempts and total_cost <= budget:
114
- current_attempt = attempts + 1 # User-facing attempt number (starts at 1)
115
- rprint(f"\n[bold cyan]Attempt {current_attempt}/{max_attempts}...[/bold cyan]")
116
- attempt_log_entry = f' <attempt number="{current_attempt}">\n' # Start XML for this attempt
342
+ # current_attempt is used for logging the current iteration number
343
+ current_iteration_number = attempts + 1
344
+ rprint(f"\n[bold cyan]Attempt {current_iteration_number}/{max_attempts}...[/bold cyan]")
345
+ attempt_log_entry = f' <attempt number="{current_iteration_number}">\n'
117
346
 
118
347
  # b. Run the verification program
119
348
  if verbose:
120
349
  rprint(f"Running verification: {sys.executable} {verification_program}")
121
350
 
122
- process = subprocess.run(
123
- [sys.executable, verification_program],
124
- capture_output=True,
125
- text=True,
126
- encoding='utf-8', # Ensure consistent encoding
127
- )
351
+ try:
352
+ returncode, stdout, stderr = run_process_with_output(
353
+ [sys.executable, verification_program],
354
+ timeout=300
355
+ )
356
+ process = ProcessResult(returncode, stdout, stderr)
357
+
358
+ verification_status = f"Success (Return Code: {process.returncode})" if process.returncode == 0 else f"Failure (Return Code: {process.returncode})"
359
+ verification_output = process.stdout or "[No standard output]"
360
+ verification_error = process.stderr or "[No standard error]"
361
+ except Exception as e:
362
+ verification_status = f"Failure (Exception: {e})"
363
+ verification_output = "[Exception occurred]"
364
+ verification_error = str(e)
365
+ process = ProcessResult(-1, "", str(e))
128
366
 
129
- verification_status = f"Success (Return Code: {process.returncode})" if process.returncode == 0 else f"Failure (Return Code: {process.returncode})"
130
- verification_output = process.stdout or "[No standard output]"
131
- verification_error = process.stderr or "[No standard error]"
132
367
 
133
368
  # Add verification results to the attempt log entry
134
- attempt_log_entry += f"""\
369
+ attempt_log_entry += f"""
135
370
  <verification>
136
371
  <status>{verification_status}</status>
137
372
  <output><![CDATA[
@@ -155,7 +390,7 @@ def fix_code_loop(
155
390
  current_error_message = verification_error # Use stderr as the primary error source
156
391
 
157
392
  # Add current error to the attempt log entry
158
- attempt_log_entry += f"""\
393
+ attempt_log_entry += f"""
159
394
  <current_error><![CDATA[
160
395
  {current_error_message}
161
396
  ]]></current_error>
@@ -164,13 +399,13 @@ def fix_code_loop(
164
399
  # Check budget *before* making the potentially expensive LLM call for the next attempt
165
400
  # (Only check if cost > 0 to avoid breaking before first attempt if budget is 0)
166
401
  if total_cost > budget and attempts > 0: # Check after first attempt cost is added
167
- rprint(f"[bold yellow]Budget exceeded (${total_cost:.4f} > ${budget:.4f}) before attempt {current_attempt}. Stopping.[/bold yellow]")
402
+ rprint(f"[bold yellow]Budget exceeded (${total_cost:.4f} > ${budget:.4f}) before attempt {current_iteration_number}. Stopping.[/bold yellow]")
168
403
  history_log += attempt_log_entry + " <error>Budget exceeded before LLM call</error>\n </attempt>\n"
169
404
  break
170
405
 
171
406
  # Check max attempts *before* the LLM call for this attempt
172
407
  if attempts >= max_attempts:
173
- rprint(f"[bold red]Maximum attempts ({max_attempts}) reached before attempt {current_attempt}. Stopping.[/bold red]")
408
+ rprint(f"[bold red]Maximum attempts ({max_attempts}) reached before attempt {current_iteration_number}. Stopping.[/bold red]")
174
409
  # No need to add to history here, loop condition handles it
175
410
  break
176
411
 
@@ -178,16 +413,16 @@ def fix_code_loop(
178
413
  # Create backup copies for this iteration BEFORE calling LLM
179
414
  code_base, code_ext = os.path.splitext(code_file)
180
415
  program_base, program_ext = os.path.splitext(verification_program)
181
- code_backup_path = f"{code_base}_{current_attempt}{code_ext}"
182
- program_backup_path = f"{program_base}_{current_attempt}{program_ext}"
416
+ code_backup_path = f"{code_base}_{current_iteration_number}{code_ext}"
417
+ program_backup_path = f"{program_base}_{current_iteration_number}{program_ext}"
183
418
 
184
419
  try:
185
420
  shutil.copy2(code_file, code_backup_path)
186
421
  shutil.copy2(verification_program, program_backup_path)
187
422
  if verbose:
188
- rprint(f"Created backups for attempt {current_attempt}: {code_backup_path}, {program_backup_path}")
423
+ rprint(f"Created backups for attempt {current_iteration_number}: {code_backup_path}, {program_backup_path}")
189
424
  except Exception as e:
190
- rprint(f"[bold red]Error creating backups for attempt {current_attempt}: {e}[/bold red]")
425
+ rprint(f"[bold red]Error creating backups for attempt {current_iteration_number}: {e}[/bold red]")
191
426
  history_log += attempt_log_entry + f" <error>Failed to create backups: {e}</error>\n </attempt>\n"
192
427
  break # Cannot proceed reliably without backups
193
428
 
@@ -233,17 +468,17 @@ def fix_code_loop(
233
468
  rprint(f"[bold red]Error calling fix_code_module_errors: {e}[/bold red]")
234
469
  cost = 0.0 # Assume no cost if the call failed
235
470
  # Add error to the attempt log entry
236
- attempt_log_entry += f"""\
471
+ attempt_log_entry += f"""
237
472
  <fixing>
238
473
  <error>LLM call failed: {e}</error>
239
474
  </fixing>
240
475
  """
241
- # Continue to the next attempt or break if limits reached? Let's break.
242
476
  history_log += attempt_log_entry + " </attempt>\n" # Log the attempt with the LLM error
477
+ attempts += 1 # Increment attempts even if LLM call failed
243
478
  break # Stop if the fixing mechanism itself fails
244
479
 
245
480
  # Add fixing results to the attempt log entry
246
- attempt_log_entry += f"""\
481
+ attempt_log_entry += f"""
247
482
  <fixing>
248
483
  <llm_analysis><![CDATA[
249
484
  {program_code_fix or "[No analysis provided]"}
@@ -269,11 +504,13 @@ def fix_code_loop(
269
504
  rprint(f"[bold red]Error writing to log file {error_log_file}: {e}[/bold red]")
270
505
 
271
506
 
272
- # Add cost and check budget *after* the LLM call
507
+ # Add cost and increment attempt counter (as per fix report) *before* checking budget
273
508
  total_cost += cost
509
+ attempts += 1 # Moved this line here as per fix report
274
510
  rprint(f"Attempt Cost: ${cost:.4f}, Total Cost: ${total_cost:.4f}, Budget: ${budget:.4f}")
511
+
275
512
  if total_cost > budget:
276
- rprint(f"[bold yellow]Budget exceeded (${total_cost:.4f} > ${budget:.4f}) after attempt {current_attempt}. Stopping.[/bold yellow]")
513
+ rprint(f"[bold yellow]Budget exceeded (${total_cost:.4f} > ${budget:.4f}) after attempt {attempts}. Stopping.[/bold yellow]")
277
514
  break # Stop loop
278
515
 
279
516
  # If LLM suggested no changes but verification failed, stop to prevent loops
@@ -295,8 +532,7 @@ def fix_code_loop(
295
532
  success = False # Mark as failed if we can't write updates
296
533
  break # Stop if we cannot apply fixes
297
534
 
298
- # e. Increment attempt counter (used for loop condition)
299
- attempts += 1
535
+ # The original 'attempts += 1' was here. It has been moved earlier.
300
536
 
301
537
  # Check if max attempts reached after incrementing (for the next loop iteration check)
302
538
  if attempts >= max_attempts:
@@ -358,39 +594,57 @@ def fix_code_loop(
358
594
  rprint(f"[bold red]Final write to log file {error_log_file} failed: {e}[/bold red]")
359
595
 
360
596
  # Determine final number of attempts for reporting
361
- # If loop finished by verification success (success=True), attempts = attempts made
362
- # If loop finished by failure (budget, max_attempts, no_change_needed, error),
363
- # the number of attempts *initiated* is 'attempts + 1' unless max_attempts was exactly hit.
364
- # The tests seem to expect the number of attempts *initiated*.
365
- # Let's refine the calculation slightly for clarity.
366
- # 'attempts' holds the count of *completed* loops (0-indexed).
367
- # 'current_attempt' holds the user-facing number (1-indexed) of the loop *currently running or just finished*.
597
+ # The 'attempts' variable correctly counts the number of LLM fix cycles that were initiated.
368
598
  final_attempts_reported = attempts
369
- if not success:
370
- # If failure occurred, it happened *during* or *after* the 'current_attempt' was initiated.
371
- # If loop broke due to budget/no_change/error, current_attempt reflects the attempt number where failure occurred.
372
- # If loop broke because attempts >= max_attempts, the last valid value for current_attempt was max_attempts.
373
- # The number of attempts *tried* is current_attempt.
374
- # However, the tests seem aligned with the previous logic. Let's stick to it unless further tests fail.
375
- final_attempts_reported = attempts if success else (attempts + 1 if attempts < max_attempts and process.returncode != 0 else attempts)
376
- # Re-evaluating the test logic:
377
- # - Budget test: attempts=1 when loop breaks, expects 2. (attempts+1) -> 2. Correct.
378
- # - Max attempts test: attempts=0 when loop breaks (no change), max_attempts=2, expects <=2. (attempts+1) -> 1. Correct.
379
- # - If max_attempts=2 was reached *normally* (failed attempt 1, failed attempt 2), attempts would be 2.
380
- # The logic `attempts + 1 if attempts < max_attempts else attempts` would return 2. Correct.
381
- # Let's simplify the return calculation based on 'attempts' which counts completed loops.
382
- final_attempts_reported = attempts # Number of fully completed fix cycles
383
- if not success and process and process.returncode != 0: # If we failed after at least one verification run
384
- # Count the final failed attempt unless success was achieved on the very last possible attempt
385
- if attempts < max_attempts:
386
- final_attempts_reported += 1
387
599
 
600
+ if not success and agentic_fallback:
601
+ # Ensure error_log_file exists before calling agentic fix
602
+ try:
603
+ if not os.path.exists(error_log_file) or os.path.getsize(error_log_file) == 0:
604
+ # Write minimal error log for agentic fix
605
+ error_log_path = Path(error_log_file)
606
+ error_log_path.parent.mkdir(parents=True, exist_ok=True)
607
+ with open(error_log_path, "w") as elog:
608
+ if process:
609
+ elog.write(f"Verification failed with return code: {process.returncode}\n")
610
+ if process.stdout:
611
+ elog.write(f"\nStdout:\n{process.stdout}\n")
612
+ if process.stderr:
613
+ elog.write(f"\nStderr:\n{process.stderr}\n")
614
+ else:
615
+ elog.write("No error information available\n")
616
+ except Exception as e:
617
+ rprint(f"[yellow]Warning: Could not write error log before agentic fallback: {e}[/yellow]")
618
+
619
+ rprint(f"[cyan]Attempting agentic fallback (prompt_file={prompt_file!r})...[/cyan]")
620
+ agent_success, agent_msg, agent_cost, agent_model, agent_changed_files = _safe_run_agentic_crash(
621
+ prompt_file=prompt_file,
622
+ code_file=code_file,
623
+ program_file=verification_program,
624
+ crash_log_file=error_log_file,
625
+ cwd=Path(prompt_file).parent if prompt_file else None
626
+ )
627
+ total_cost += agent_cost
628
+ if not agent_success:
629
+ rprint(f"[bold red]Agentic fallback failed: {agent_msg}[/bold red]")
630
+ if agent_changed_files:
631
+ rprint(f"[cyan]Agent modified {len(agent_changed_files)} file(s):[/cyan]")
632
+ for f in agent_changed_files:
633
+ rprint(f" • {f}")
634
+ if agent_success:
635
+ model_name = agent_model or model_name
636
+ try:
637
+ final_code_content = Path(code_file).read_text(encoding='utf-8')
638
+ final_program_content = Path(verification_program).read_text(encoding='utf-8')
639
+ except Exception as e:
640
+ rprint(f"[yellow]Warning: Could not read files after successful agentic fix: {e}[/yellow]")
641
+ success = True
388
642
 
389
643
  return (
390
644
  success,
391
645
  final_program_content,
392
646
  final_code_content,
393
- final_attempts_reported, # Use the refined calculation
647
+ final_attempts_reported,
394
648
  total_cost,
395
649
  model_name,
396
650
  )
@@ -519,4 +773,4 @@ sys.exit(0) # Exit with zero code for success
519
773
  # for f in Path(".").glob("dummy_verify_*.py"): # Remove attempt backups like dummy_verify_1.py
520
774
  # if "_original_backup" not in f.name: os.remove(f)
521
775
  # except OSError as e:
522
- # print(f"Error cleaning up dummy files: {e}")
776
+ # print(f"Error cleaning up dummy files: {e}")