pdd-cli 0.0.30__py3-none-any.whl → 0.0.31__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.

Potentially problematic release.


This version of pdd-cli might be problematic. Click here for more details.

pdd/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.0.30"
1
+ __version__ = "0.0.31"
2
2
 
3
3
  # Strength parameter used for LLM extraction across the codebase
4
4
  # Used in postprocessing, XML tagging, code generation, and other extraction operations. The module should have a large context window and be affordable.
@@ -6,9 +6,17 @@ EXTRACTION_STRENGTH = 0.9
6
6
 
7
7
  DEFAULT_STRENGTH = 0.9
8
8
 
9
+ DEFAULT_TEMPERATURE = 0.0
10
+
11
+ DEFAULT_TIME = 0.25
12
+
9
13
  """PDD - Prompt Driven Development"""
10
14
 
11
15
  # Define constants used across the package
12
16
  DEFAULT_LLM_MODEL = "gpt-4.1-nano"
17
+ # When going to production, set the following constants:
18
+ # REACT_APP_FIREBASE_API_KEY
19
+ # GITHUB_CLIENT_ID
20
+
21
+ # You can add other package-level initializations or imports here
13
22
 
14
- # You can add other package-level initializations or imports here
pdd/cli.py CHANGED
@@ -239,17 +239,39 @@ def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float
239
239
  default=None,
240
240
  help="Specify where to save the generated code (file or directory).",
241
241
  )
242
+ @click.option(
243
+ "--original-prompt",
244
+ "original_prompt_file_path",
245
+ type=click.Path(exists=True, dir_okay=False),
246
+ default=None,
247
+ help="Path to the original prompt file for incremental generation.",
248
+ )
249
+ @click.option(
250
+ "--force-incremental",
251
+ "force_incremental_flag",
252
+ is_flag=True,
253
+ default=False,
254
+ help="Force incremental generation even if full regeneration is suggested.",
255
+ )
242
256
  @click.pass_context
243
257
  @track_cost
244
- def generate(ctx: click.Context, prompt_file: str, output: Optional[str]) -> Optional[Tuple[str, float, str]]: # Modified return type
258
+ def generate(
259
+ ctx: click.Context,
260
+ prompt_file: str,
261
+ output: Optional[str],
262
+ original_prompt_file_path: Optional[str],
263
+ force_incremental_flag: bool,
264
+ ) -> Optional[Tuple[str, float, str]]: # Modified return type
245
265
  """Create runnable code from a prompt file."""
246
266
  quiet = ctx.obj.get("quiet", False)
247
267
  command_name = "generate"
248
268
  try:
249
- generated_code, total_cost, model_name = code_generator_main(
269
+ generated_code, incremental, total_cost, model_name = code_generator_main(
250
270
  ctx=ctx,
251
271
  prompt_file=prompt_file,
252
272
  output=output,
273
+ original_prompt_file_path=original_prompt_file_path,
274
+ force_incremental_flag=force_incremental_flag,
253
275
  )
254
276
  return generated_code, total_cost, model_name
255
277
  except Exception as e:
@@ -1009,7 +1031,7 @@ def verify(
1009
1031
  quiet = ctx.obj.get("quiet", False)
1010
1032
  command_name = "verify"
1011
1033
  try:
1012
- success, final_program, final_code, attempts, cost, model = fix_verification_main(
1034
+ success, final_program, final_code, attempts, total_cost_value, model_name_value = fix_verification_main(
1013
1035
  ctx=ctx,
1014
1036
  prompt_file=prompt_file,
1015
1037
  code_file=code_file,
@@ -1029,7 +1051,7 @@ def verify(
1029
1051
  "verified_program_path": output_program,
1030
1052
  "results_log_path": output_results,
1031
1053
  }
1032
- return result_data, cost, model
1054
+ return result_data, total_cost_value, model_name_value
1033
1055
  except Exception as e:
1034
1056
  handle_error(e, command_name, quiet)
1035
1057
  return None # Return None on failure
@@ -1,124 +1,354 @@
1
- import sys
2
- from typing import Tuple, Optional
3
- import click
4
- from rich import print as rprint
1
+ import os
2
+ import asyncio
3
+ import json
4
+ import pathlib
5
+ import shlex
6
+ import subprocess
7
+ import requests
8
+ from typing import Optional, Tuple, Dict, Any, List
5
9
 
6
- import requests # <── Added at top level so the tests can patch pdd.code_generator_main.requests
10
+ import click
11
+ from rich.console import Console
12
+ from rich.panel import Panel
13
+ from rich.text import Text
7
14
 
15
+ # Relative imports for PDD package structure
16
+ from . import DEFAULT_STRENGTH, DEFAULT_TIME, EXTRACTION_STRENGTH # Assuming these are in __init__.py
8
17
  from .construct_paths import construct_paths
9
- from .code_generator import code_generator
10
- from .get_jwt_token import get_jwt_token
11
- from .preprocess import preprocess
18
+ from .preprocess import preprocess as pdd_preprocess
19
+ from .code_generator import code_generator as local_code_generator_func
20
+ from .incremental_code_generator import incremental_code_generator as incremental_code_generator_func
21
+ from .get_jwt_token import get_jwt_token, AuthError, NetworkError, TokenError, UserCancelledError, RateLimitError
22
+
23
+ # Environment variable names for Firebase/GitHub auth
24
+ FIREBASE_API_KEY_ENV_VAR = "NEXT_PUBLIC_FIREBASE_API_KEY"
25
+ GITHUB_CLIENT_ID_ENV_VAR = "GITHUB_CLIENT_ID"
26
+ PDD_APP_NAME = "PDD Code Generator"
27
+
28
+ # Cloud function URL
29
+ CLOUD_GENERATE_URL = "https://us-central1-prompt-driven-development.cloudfunctions.net/generateCode"
30
+ CLOUD_REQUEST_TIMEOUT = 200 # seconds
31
+
32
+ console = Console()
33
+
34
+ # --- Git Helper Functions ---
35
+ def _run_git_command(command: List[str], cwd: Optional[str] = None) -> Tuple[int, str, str]:
36
+ """Runs a git command and returns (return_code, stdout, stderr)."""
37
+ try:
38
+ process = subprocess.run(command, capture_output=True, text=True, check=False, cwd=cwd, encoding='utf-8')
39
+ return process.returncode, process.stdout.strip(), process.stderr.strip()
40
+ except FileNotFoundError:
41
+ return -1, "", "Git command not found. Ensure git is installed and in your PATH."
42
+ except Exception as e:
43
+ return -2, "", f"Error running git command {' '.join(command)}: {e}"
44
+
45
+ def is_git_repository(path: Optional[str] = None) -> bool:
46
+ """Checks if the given path (or current dir) is a git repository."""
47
+ start_path = pathlib.Path(path).resolve() if path else pathlib.Path.cwd()
48
+ # Check for .git in current or any parent directory
49
+ current_path = start_path
50
+ while True:
51
+ if (current_path / ".git").is_dir():
52
+ # Verify it's the root of the work tree or inside it
53
+ returncode, stdout, _ = _run_git_command(["git", "rev-parse", "--is-inside-work-tree"], cwd=str(start_path))
54
+ return returncode == 0 and stdout == "true"
55
+ parent = current_path.parent
56
+ if parent == current_path: # Reached root directory
57
+ break
58
+ current_path = parent
59
+ return False
12
60
 
13
- def code_generator_main(ctx: click.Context, prompt_file: str, output: Optional[str]) -> Tuple[str, float, str]:
14
- """
15
- Main function to generate code from a prompt file.
16
61
 
17
- :param ctx: Click context containing command-line parameters.
18
- :param prompt_file: Path to the prompt file used to generate the code.
19
- :param output: Optional path to save the generated code.
20
- :return: A tuple containing the generated code, total cost, and model name used.
62
+ def get_git_committed_content(file_path: str) -> Optional[str]:
63
+ """Gets the content of the file as it was in the last commit (HEAD)."""
64
+ abs_file_path = pathlib.Path(file_path).resolve()
65
+ if not is_git_repository(str(abs_file_path.parent)):
66
+ return None
67
+
68
+ returncode_rev, git_root_str, stderr_rev = _run_git_command(["git", "rev-parse", "--show-toplevel"], cwd=str(abs_file_path.parent))
69
+ if returncode_rev != 0:
70
+ # console.print(f"[yellow]Git (rev-parse) warning for {file_path}: {stderr_rev}[/yellow]")
71
+ return None
72
+
73
+ git_root = pathlib.Path(git_root_str)
74
+ try:
75
+ relative_path = abs_file_path.relative_to(git_root)
76
+ except ValueError:
77
+ # console.print(f"[yellow]File {file_path} is not under git root {git_root}.[/yellow]")
78
+ return None
79
+
80
+ returncode, stdout, stderr = _run_git_command(["git", "show", f"HEAD:{relative_path.as_posix()}"], cwd=str(git_root))
81
+ if returncode == 0:
82
+ return stdout
83
+ else:
84
+ # File might be new, or other git error. Not necessarily an error for this function's purpose.
85
+ # if "does not exist" not in stderr and "exists on disk, but not in 'HEAD'" not in stderr and console.is_terminal:
86
+ # console.print(f"[yellow]Git (show) warning for {file_path}: {stderr}[/yellow]")
87
+ return None
88
+
89
+ def get_file_git_status(file_path: str) -> str:
90
+ """Gets the git status of a single file (e.g., ' M', '??', 'A '). Empty if clean."""
91
+ abs_file_path = pathlib.Path(file_path).resolve()
92
+ if not is_git_repository(str(abs_file_path.parent)) or not abs_file_path.exists():
93
+ return ""
94
+ returncode, stdout, _ = _run_git_command(["git", "status", "--porcelain", str(abs_file_path)], cwd=str(abs_file_path.parent))
95
+ if returncode == 0:
96
+ # stdout might be " M path/to/file" or "?? path/to/file"
97
+ # We only want the status codes part
98
+ status_part = stdout.split(str(abs_file_path.name))[0].strip() if str(abs_file_path.name) in stdout else stdout.strip()
99
+ return status_part
100
+ return ""
101
+
102
+ def git_add_files(file_paths: List[str], verbose: bool = False) -> bool:
103
+ """Stages the given files using 'git add'."""
104
+ if not file_paths:
105
+ return True
106
+
107
+ # Resolve paths and ensure they are absolute for git command
108
+ abs_paths = [str(pathlib.Path(fp).resolve()) for fp in file_paths]
109
+
110
+ # Determine common parent directory to run git command from, or git root
111
+ # For simplicity, assume they are in the same repo and run from one of their parents
112
+ if not is_git_repository(str(pathlib.Path(abs_paths[0]).parent)):
113
+ if verbose:
114
+ console.print(f"[yellow]Cannot stage files: {abs_paths[0]} is not in a git repository.[/yellow]")
115
+ return False
116
+
117
+ returncode, _, stderr = _run_git_command(["git", "add"] + abs_paths, cwd=str(pathlib.Path(abs_paths[0]).parent))
118
+ if returncode == 0:
119
+ if verbose:
120
+ console.print(f"Successfully staged: [cyan]{', '.join(abs_paths)}[/cyan]")
121
+ return True
122
+ else:
123
+ console.print(f"[red]Error staging files with git:[/red] {stderr}")
124
+ return False
125
+ # --- End Git Helper Functions ---
126
+
127
+
128
+ def code_generator_main(
129
+ ctx: click.Context,
130
+ prompt_file: str,
131
+ output: Optional[str],
132
+ original_prompt_file_path: Optional[str],
133
+ force_incremental_flag: bool,
134
+ ) -> Tuple[str, bool, float, str]:
135
+ """
136
+ CLI wrapper for generating code from prompts. Handles full and incremental generation,
137
+ local vs. cloud execution, and output.
21
138
  """
139
+ cli_params = ctx.obj or {}
140
+ is_local_execution_preferred = cli_params.get('local', False)
141
+ strength = cli_params.get('strength', DEFAULT_STRENGTH)
142
+ temperature = cli_params.get('temperature', 0.0)
143
+ time_budget = cli_params.get('time', DEFAULT_TIME)
144
+ verbose = cli_params.get('verbose', False)
145
+ force_overwrite = cli_params.get('force', False)
146
+ quiet = cli_params.get('quiet', False)
147
+
148
+ generated_code_content: Optional[str] = None
149
+ was_incremental_operation = False
150
+ total_cost = 0.0
151
+ model_name = "unknown"
152
+
153
+ input_file_paths_dict: Dict[str, str] = {"prompt_file": prompt_file}
154
+ if original_prompt_file_path:
155
+ input_file_paths_dict["original_prompt_file"] = original_prompt_file_path
156
+
157
+ command_options: Dict[str, Any] = {"output": output}
158
+
22
159
  try:
23
- # Construct file paths
24
- input_file_paths = {
25
- "prompt_file": prompt_file
26
- }
27
- command_options = {
28
- "output": output
29
- }
30
160
  input_strings, output_file_paths, language = construct_paths(
31
- input_file_paths=input_file_paths,
32
- force=ctx.obj.get('force', False),
33
- quiet=ctx.obj.get('quiet', False),
161
+ input_file_paths=input_file_paths_dict,
162
+ force=force_overwrite,
163
+ quiet=quiet,
34
164
  command="generate",
35
- command_options=command_options
165
+ command_options=command_options,
36
166
  )
37
-
38
- # Load input file
39
167
  prompt_content = input_strings["prompt_file"]
168
+ output_path = output_file_paths.get("output")
169
+
170
+ except FileNotFoundError as e:
171
+ console.print(f"[red]Error: Input file not found: {e.filename}[/red]")
172
+ return "", False, 0.0, "error"
173
+ except Exception as e:
174
+ console.print(f"[red]Error during path construction: {e}[/red]")
175
+ return "", False, 0.0, "error"
176
+
177
+ can_attempt_incremental = False
178
+ existing_code_content: Optional[str] = None
179
+ original_prompt_content_for_incremental: Optional[str] = None
180
+
181
+ if output_path and pathlib.Path(output_path).exists():
182
+ try:
183
+ existing_code_content = pathlib.Path(output_path).read_text(encoding="utf-8")
184
+ except Exception as e:
185
+ console.print(f"[yellow]Warning: Could not read existing output file {output_path}: {e}[/yellow]")
186
+ existing_code_content = None
187
+
188
+ if existing_code_content is not None:
189
+ if "original_prompt_file" in input_strings:
190
+ original_prompt_content_for_incremental = input_strings["original_prompt_file"]
191
+ can_attempt_incremental = True
192
+ if verbose:
193
+ console.print(f"Using specified original prompt: [cyan]{original_prompt_file_path}[/cyan]")
194
+ elif is_git_repository(str(pathlib.Path(prompt_file).parent)):
195
+ original_prompt_git_content = get_git_committed_content(prompt_file)
196
+ if original_prompt_git_content is not None:
197
+ original_prompt_content_for_incremental = original_prompt_git_content
198
+ can_attempt_incremental = True
199
+ if verbose:
200
+ console.print(f"Using last committed version of [cyan]{prompt_file}[/cyan] as original prompt.")
201
+ elif verbose:
202
+ console.print(f"[yellow]Warning: Could not find committed version of {prompt_file} in git for incremental generation.[/yellow]")
203
+
204
+ if force_incremental_flag and existing_code_content:
205
+ if not (original_prompt_content_for_incremental or "original_prompt_file" in input_strings): # Check if original prompt is actually available
206
+ console.print(
207
+ "[yellow]Warning: --incremental flag used, but original prompt could not be determined. "
208
+ "Falling back to full generation.[/yellow]"
209
+ )
210
+ else:
211
+ can_attempt_incremental = True
212
+
213
+ if force_incremental_flag and (not output_path or not pathlib.Path(output_path).exists()):
214
+ console.print(
215
+ "[yellow]Warning: --incremental flag used, but output file does not exist or path not specified. "
216
+ "Performing full generation.[/yellow]"
217
+ )
218
+ can_attempt_incremental = False
219
+
220
+ try:
221
+ if can_attempt_incremental and existing_code_content is not None and original_prompt_content_for_incremental is not None:
222
+ if verbose:
223
+ console.print(Panel("Attempting incremental code generation...", title="[blue]Mode[/blue]", expand=False))
40
224
 
41
- # Generate code
42
- strength = ctx.obj.get('strength', 0.5)
43
- temperature = ctx.obj.get('temperature', 0.0)
44
- verbose = not ctx.obj.get('quiet', False)
45
- local = ctx.obj.get('local', False)
46
-
47
- if local:
48
- print("Running in local mode")
49
- # Local execution
50
- generated_code, total_cost, model_name = code_generator(
51
- prompt_content,
52
- language,
53
- strength,
54
- temperature,
55
- verbose=verbose
225
+ if is_git_repository(str(pathlib.Path(prompt_file).parent)):
226
+ files_to_stage_for_rollback: List[str] = []
227
+ paths_to_check = [pathlib.Path(prompt_file).resolve()]
228
+ if output_path and pathlib.Path(output_path).exists():
229
+ paths_to_check.append(pathlib.Path(output_path).resolve())
230
+
231
+ for p_to_check in paths_to_check:
232
+ if not p_to_check.exists(): continue
233
+
234
+ is_untracked = get_file_git_status(str(p_to_check)).startswith("??")
235
+ # Check if different from HEAD or untracked
236
+ is_different_from_head_rc = 1 if is_untracked else _run_git_command(["git", "diff", "--quiet", "HEAD", "--", str(p_to_check)], cwd=str(p_to_check.parent))[0]
237
+
238
+ if is_different_from_head_rc != 0: # Different from HEAD or untracked
239
+ files_to_stage_for_rollback.append(str(p_to_check))
240
+
241
+ if files_to_stage_for_rollback:
242
+ git_add_files(files_to_stage_for_rollback, verbose=verbose)
243
+
244
+ generated_code_content, was_incremental_operation, total_cost, model_name = incremental_code_generator_func(
245
+ original_prompt=original_prompt_content_for_incremental,
246
+ new_prompt=prompt_content,
247
+ existing_code=existing_code_content,
248
+ language=language,
249
+ strength=strength,
250
+ temperature=temperature,
251
+ time=time_budget,
252
+ force_incremental=force_incremental_flag,
253
+ verbose=verbose,
254
+ preprocess_prompt=True
56
255
  )
57
- else:
58
- # Cloud execution
59
- try:
60
- import asyncio
61
- import os
62
- # Get JWT token for cloud authentication
63
- jwt_token = asyncio.run(get_jwt_token(
64
- firebase_api_key=os.environ.get("REACT_APP_FIREBASE_API_KEY"),
65
- github_client_id=os.environ.get("GITHUB_CLIENT_ID"),
66
- app_name="PDD Code Generator"
67
- ))
68
- # Call cloud code generator
69
- headers = {
70
- "Authorization": f"Bearer {jwt_token}",
71
- "Content-Type": "application/json"
72
- }
73
- # Preprocess the prompt
74
- processed_prompt = preprocess(prompt_content, recursive=False, double_curly_brackets=True)
256
+
257
+ if not was_incremental_operation:
75
258
  if verbose:
76
- print(f"Processed prompt: {processed_prompt}")
77
- data = {
78
- "promptContent": processed_prompt,
79
- "language": language,
80
- "strength": strength,
81
- "temperature": temperature,
82
- "verbose": verbose
83
- }
84
- response = requests.post(
85
- "https://us-central1-prompt-driven-development.cloudfunctions.net/generateCode",
86
- headers=headers,
87
- json=data
88
- )
89
- response.raise_for_status()
90
- result = response.json()
91
- generated_code = result["generatedCode"]
92
- total_cost = result["totalCost"]
93
- model_name = result["modelName"]
94
-
95
- except Exception as e:
96
- if not ctx.obj.get('quiet', False):
97
- rprint("[bold red]Cloud execution failed, falling back to local mode[/bold red]")
98
- generated_code, total_cost, model_name = code_generator(
99
- prompt_content,
100
- language,
101
- strength,
102
- temperature,
103
- verbose=verbose
104
- )
259
+ console.print(Panel("Incremental generator suggested full regeneration. Falling back.", title="[yellow]Fallback[/yellow]", expand=False))
260
+ elif verbose:
261
+ console.print(Panel(f"Incremental update successful. Model: {model_name}, Cost: ${total_cost:.6f}", title="[green]Incremental Success[/green]", expand=False))
105
262
 
106
- # Save results
107
- if output_file_paths["output"]:
108
- with open(output_file_paths["output"], 'w') as f:
109
- f.write(generated_code)
263
+ if not was_incremental_operation: # Full generation path
264
+ if verbose:
265
+ console.print(Panel("Performing full code generation...", title="[blue]Mode[/blue]", expand=False))
266
+
267
+ current_execution_is_local = is_local_execution_preferred
268
+
269
+ if not current_execution_is_local:
270
+ if verbose: console.print("Attempting cloud code generation...")
271
+
272
+ processed_prompt_for_cloud = pdd_preprocess(prompt_content, recursive=True, double_curly_brackets=True, exclude_keys=[])
273
+ if verbose: console.print(Panel(Text(processed_prompt_for_cloud, overflow="fold"), title="[cyan]Preprocessed Prompt for Cloud[/cyan]", expand=False))
274
+
275
+ jwt_token: Optional[str] = None
276
+ try:
277
+ firebase_api_key_val = os.environ.get(FIREBASE_API_KEY_ENV_VAR)
278
+ github_client_id_val = os.environ.get(GITHUB_CLIENT_ID_ENV_VAR)
110
279
 
111
- # Provide user feedback
112
- if not ctx.obj.get('quiet', False):
113
- rprint("[bold green]Code generation completed successfully.[/bold green]")
114
- rprint(f"[bold]Model used:[/bold] {model_name}")
115
- rprint(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
116
- if output:
117
- rprint(f"[bold]Code saved to:[/bold] {output_file_paths['output']}")
280
+ if not firebase_api_key_val: raise AuthError(f"{FIREBASE_API_KEY_ENV_VAR} not set.")
281
+ if not github_client_id_val: raise AuthError(f"{GITHUB_CLIENT_ID_ENV_VAR} not set.")
118
282
 
119
- return generated_code, total_cost, model_name
283
+ jwt_token = asyncio.run(get_jwt_token(
284
+ firebase_api_key=firebase_api_key_val,
285
+ github_client_id=github_client_id_val,
286
+ app_name=PDD_APP_NAME
287
+ ))
288
+ except (AuthError, NetworkError, TokenError, UserCancelledError, RateLimitError) as e:
289
+ console.print(f"[yellow]Cloud authentication/token error: {e}. Falling back to local execution.[/yellow]")
290
+ current_execution_is_local = True
291
+ except Exception as e:
292
+ console.print(f"[yellow]Unexpected error during cloud authentication: {e}. Falling back to local execution.[/yellow]")
293
+ current_execution_is_local = True
294
+
295
+ if jwt_token and not current_execution_is_local:
296
+ payload = {"promptContent": processed_prompt_for_cloud, "language": language, "strength": strength, "temperature": temperature, "verbose": verbose}
297
+ headers = {"Authorization": f"Bearer {jwt_token}", "Content-Type": "application/json"}
298
+ try:
299
+ response = requests.post(CLOUD_GENERATE_URL, json=payload, headers=headers, timeout=CLOUD_REQUEST_TIMEOUT)
300
+ response.raise_for_status()
301
+
302
+ response_data = response.json()
303
+ generated_code_content = response_data.get("generatedCode")
304
+ total_cost = float(response_data.get("totalCost", 0.0))
305
+ model_name = response_data.get("modelName", "cloud_model")
306
+
307
+ if generated_code_content is None:
308
+ console.print("[yellow]Cloud execution returned no code. Falling back to local.[/yellow]")
309
+ current_execution_is_local = True
310
+ elif verbose:
311
+ console.print(Panel(f"Cloud generation successful. Model: {model_name}, Cost: ${total_cost:.6f}", title="[green]Cloud Success[/green]", expand=False))
312
+ except requests.exceptions.Timeout:
313
+ console.print(f"[yellow]Cloud execution timed out ({CLOUD_REQUEST_TIMEOUT}s). Falling back to local.[/yellow]")
314
+ current_execution_is_local = True
315
+ except requests.exceptions.HTTPError as e:
316
+ err_content = e.response.text[:200] if e.response else "No response content"
317
+ console.print(f"[yellow]Cloud HTTP error ({e.response.status_code}): {err_content}. Falling back to local.[/yellow]")
318
+ current_execution_is_local = True
319
+ except requests.exceptions.RequestException as e:
320
+ console.print(f"[yellow]Cloud network error: {e}. Falling back to local.[/yellow]")
321
+ current_execution_is_local = True
322
+ except json.JSONDecodeError:
323
+ console.print("[yellow]Cloud returned invalid JSON. Falling back to local.[/yellow]")
324
+ current_execution_is_local = True
325
+
326
+ if current_execution_is_local:
327
+ if verbose: console.print("Executing code generator locally...")
328
+ generated_code_content, total_cost, model_name = local_code_generator_func(
329
+ prompt=prompt_content, language=language, strength=strength,
330
+ temperature=temperature, verbose=verbose
331
+ )
332
+ if verbose:
333
+ console.print(Panel(f"Local generation successful. Model: {model_name}, Cost: ${total_cost:.6f}", title="[green]Local Success[/green]", expand=False))
334
+
335
+ if generated_code_content is not None:
336
+ if output_path:
337
+ p_output = pathlib.Path(output_path)
338
+ p_output.parent.mkdir(parents=True, exist_ok=True)
339
+ p_output.write_text(generated_code_content, encoding="utf-8")
340
+ if verbose or not quiet:
341
+ console.print(f"Generated code saved to: [green]{p_output.resolve()}[/green]")
342
+ elif not quiet: # No output path, print to console if not quiet
343
+ console.print(Panel(Text(generated_code_content, overflow="fold"), title="[cyan]Generated Code[/cyan]", expand=True))
344
+ else:
345
+ console.print("[red]Error: Code generation failed. No code was produced.[/red]")
346
+ return "", was_incremental_operation, total_cost, model_name or "error"
120
347
 
121
348
  except Exception as e:
122
- if not ctx.obj.get('quiet', False):
123
- rprint(f"[bold red]Error:[/bold red] {str(e)}")
124
- sys.exit(1)
349
+ console.print(f"[red]An unexpected error occurred: {e}[/red]")
350
+ import traceback
351
+ if verbose: console.print(traceback.format_exc())
352
+ return "", was_incremental_operation, total_cost, "error"
353
+
354
+ return generated_code_content or "", was_incremental_operation, total_cost, model_name
@@ -507,44 +507,70 @@ def fix_verification_errors_loop(
507
507
  # FIX: Restructured logic for success check and secondary verification
508
508
  secondary_verification_passed = True # Assume pass unless changes made and verification fails
509
509
  changes_applied_this_iteration = False
510
+ verify_ret_code = 0 # Default for skipped verification
511
+ verify_output = "Secondary verification not run." # Default for skipped
510
512
 
511
- # Run secondary verification ONLY if code was updated
512
513
  if code_updated:
513
514
  if verbose:
514
- console.print("Code change suggested, running secondary verification...")
515
- try:
516
- # Temporarily write the proposed code change
517
- code_path.write_text(fixed_code, encoding="utf-8")
515
+ console.print("Code change suggested, attempting secondary verification...")
516
+
517
+ if verification_program is not None and verification_program_path.is_file():
518
+ try:
519
+ # Temporarily write the proposed code change
520
+ code_path.write_text(fixed_code, encoding="utf-8")
518
521
 
519
- # Run verification program
520
- verify_ret_code, verify_output = _run_program(verification_program_path)
522
+ # Run verification program
523
+ # Consider if verification_program_path needs arguments or specific env vars
524
+ # For now, assuming it can run directly or uses env vars set externally
525
+ current_verify_ret_code, current_verify_output = _run_program(verification_program_path)
521
526
 
522
- # Determine pass/fail (simple: exit code 0 = pass)
523
- secondary_verification_passed = (verify_ret_code == 0)
527
+ # Determine pass/fail (simple: exit code 0 = pass)
528
+ secondary_verification_passed = (current_verify_ret_code == 0)
529
+ verify_ret_code = current_verify_ret_code
530
+ verify_output = current_verify_output
524
531
 
532
+ if verbose:
533
+ console.print(f"Secondary verification ran. Exit code: {verify_ret_code}")
534
+ console.print(f"Secondary verification passed: {secondary_verification_passed}")
535
+ # console.print(f"Secondary verification output:\\n{verify_output}")
536
+
537
+ if not secondary_verification_passed:
538
+ console.print("[yellow]Secondary verification failed. Restoring code file from memory.[/yellow]")
539
+ code_path.write_text(code_contents, encoding="utf-8") # Restore from memory state before this attempt
540
+
541
+ except IOError as e:
542
+ console.print(f"[bold red]Error during secondary verification I/O: {e}[/bold red]")
543
+ verify_output = f"Error during secondary verification I/O: {str(e)}"
544
+ secondary_verification_passed = False # Treat I/O error as failure
545
+ verify_ret_code = -1 # Indicate error
546
+ try:
547
+ code_path.write_text(code_contents, encoding="utf-8")
548
+ except IOError:
549
+ console.print(f"[bold red]Failed to restore code file after I/O error.[/bold red]")
550
+ else:
551
+ # No valid verification program provided, or it's not a file
552
+ secondary_verification_passed = True # Effectively skipped, so it doesn't block progress
553
+ verify_ret_code = 0
554
+ if verification_program is None:
555
+ verify_output = "Secondary verification skipped: No verification program provided."
556
+ else:
557
+ verify_output = f"Secondary verification skipped: Verification program '{verification_program}' not found or is not a file at '{verification_program_path}'."
525
558
  if verbose:
526
- console.print(f"Secondary verification exit code: {verify_ret_code}")
527
- console.print(f"Secondary verification passed: {secondary_verification_passed}")
528
- # console.print(f"Secondary verification output:\n{verify_output}")
529
-
530
- passed_str = str(secondary_verification_passed).lower()
531
- iteration_log_xml += f' <SecondaryVerification passed="{passed_str}">\n'
532
- iteration_log_xml += f' <ExitCode>{verify_ret_code}</ExitCode>\n'
533
- iteration_log_xml += f' <Output>{escape(verify_output)}</Output>\n'
534
- iteration_log_xml += f' </SecondaryVerification>\n'
535
-
536
- if not secondary_verification_passed:
537
- console.print("[yellow]Secondary verification failed. Restoring code file.[/yellow]")
538
- code_path.write_text(code_contents, encoding="utf-8") # Restore from memory state before this attempt
539
-
540
- except IOError as e:
541
- console.print(f"[bold red]Error during secondary verification I/O: {e}[/bold red]")
542
- iteration_log_xml += f' <Status>Error during secondary verification I/O: {escape(str(e))}</Status>\n'
543
- secondary_verification_passed = False # Treat I/O error as failure
544
- try:
545
- code_path.write_text(code_contents, encoding="utf-8")
546
- except IOError:
547
- console.print(f"[bold red]Failed to restore code file after I/O error.[/bold red]")
559
+ console.print(f"[dim]{verify_output}[/dim]")
560
+ else:
561
+ # Code was not updated by the fixer, so secondary verification is not strictly needed
562
+ secondary_verification_passed = True # No changes to verify
563
+ verify_ret_code = 0
564
+ verify_output = "Secondary verification not needed: Code was not modified by the fixer."
565
+ if verbose:
566
+ console.print(f"[dim]{verify_output}[/dim]")
567
+
568
+ # Always log the SecondaryVerification block
569
+ passed_str = str(secondary_verification_passed).lower()
570
+ iteration_log_xml += f' <SecondaryVerification passed="{passed_str}">\n'
571
+ iteration_log_xml += f' <ExitCode>{verify_ret_code}</ExitCode>\n'
572
+ iteration_log_xml += f' <Output>{escape(verify_output)}</Output>\n'
573
+ iteration_log_xml += f' </SecondaryVerification>\n'
548
574
 
549
575
  # Now, decide outcome based on issue count and verification status
550
576
  if secondary_verification_passed: