pdd-cli 0.0.45__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 (114) 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 +73 -21
  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 +258 -82
  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 -63
  40. pdd/data/llm_model.csv +20 -18
  41. pdd/detect_change_main.py +5 -4
  42. pdd/fix_code_loop.py +330 -76
  43. pdd/fix_error_loop.py +207 -61
  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 +306 -272
  48. pdd/fix_verification_main.py +28 -9
  49. pdd/generate_output_paths.py +93 -10
  50. pdd/generate_test.py +16 -5
  51. pdd/get_jwt_token.py +9 -2
  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/incremental_code_generator.py +2 -2
  56. pdd/insert_includes.py +11 -3
  57. pdd/llm_invoke.py +1269 -103
  58. pdd/load_prompt_template.py +36 -10
  59. pdd/pdd_completion.fish +25 -2
  60. pdd/pdd_completion.sh +30 -4
  61. pdd/pdd_completion.zsh +79 -4
  62. pdd/postprocess.py +10 -3
  63. pdd/preprocess.py +228 -15
  64. pdd/preprocess_main.py +8 -5
  65. pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
  66. pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
  67. pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
  68. pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
  69. pdd/prompts/agentic_update_LLM.prompt +1071 -0
  70. pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
  71. pdd/prompts/auto_include_LLM.prompt +100 -905
  72. pdd/prompts/detect_change_LLM.prompt +122 -20
  73. pdd/prompts/example_generator_LLM.prompt +22 -1
  74. pdd/prompts/extract_code_LLM.prompt +5 -1
  75. pdd/prompts/extract_program_code_fix_LLM.prompt +7 -1
  76. pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
  77. pdd/prompts/extract_promptline_LLM.prompt +17 -11
  78. pdd/prompts/find_verification_errors_LLM.prompt +6 -0
  79. pdd/prompts/fix_code_module_errors_LLM.prompt +4 -2
  80. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +8 -0
  81. pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
  82. pdd/prompts/generate_test_LLM.prompt +21 -6
  83. pdd/prompts/increase_tests_LLM.prompt +1 -5
  84. pdd/prompts/insert_includes_LLM.prompt +228 -108
  85. pdd/prompts/trace_LLM.prompt +25 -22
  86. pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
  87. pdd/prompts/update_prompt_LLM.prompt +22 -1
  88. pdd/pytest_output.py +127 -12
  89. pdd/render_mermaid.py +236 -0
  90. pdd/setup_tool.py +648 -0
  91. pdd/simple_math.py +2 -0
  92. pdd/split_main.py +3 -2
  93. pdd/summarize_directory.py +49 -6
  94. pdd/sync_determine_operation.py +543 -98
  95. pdd/sync_main.py +81 -31
  96. pdd/sync_orchestration.py +1334 -751
  97. pdd/sync_tui.py +848 -0
  98. pdd/template_registry.py +264 -0
  99. pdd/templates/architecture/architecture_json.prompt +242 -0
  100. pdd/templates/generic/generate_prompt.prompt +174 -0
  101. pdd/trace.py +168 -12
  102. pdd/trace_main.py +4 -3
  103. pdd/track_cost.py +151 -61
  104. pdd/unfinished_prompt.py +49 -3
  105. pdd/update_main.py +549 -67
  106. pdd/update_model_costs.py +2 -2
  107. pdd/update_prompt.py +19 -4
  108. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/METADATA +19 -6
  109. pdd_cli-0.0.90.dist-info/RECORD +153 -0
  110. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/licenses/LICENSE +1 -1
  111. pdd_cli-0.0.45.dist-info/RECORD +0 -116
  112. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/WHEEL +0 -0
  113. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/entry_points.txt +0 -0
  114. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/top_level.txt +0 -0
pdd/update_main.py CHANGED
@@ -1,104 +1,586 @@
1
1
  import sys
2
- from typing import Tuple, Optional
2
+ from typing import Tuple, Optional, List, Dict, Any
3
3
  import click
4
4
  from rich import print as rprint
5
+ import os
6
+ from pathlib import Path
7
+ import git
8
+ from rich.console import Console
9
+ from rich.progress import (
10
+ Progress,
11
+ SpinnerColumn,
12
+ TextColumn,
13
+ BarColumn,
14
+ TimeRemainingColumn,
15
+ )
16
+ from rich.table import Table
17
+ from rich.theme import Theme
5
18
 
6
19
  from .construct_paths import construct_paths
20
+ from .get_language import get_language
7
21
  from .update_prompt import update_prompt
8
22
  from .git_update import git_update
23
+ from .agentic_common import get_available_agents
24
+ from .agentic_update import run_agentic_update
9
25
  from . import DEFAULT_TIME
26
+
27
+ custom_theme = Theme({
28
+ "info": "cyan",
29
+ "warning": "yellow",
30
+ "error": "bold red",
31
+ "success": "green",
32
+ "path": "dim blue",
33
+ })
34
+ console = Console(theme=custom_theme)
35
+
36
+ def resolve_prompt_code_pair(code_file_path: str, quiet: bool = False, output_dir: Optional[str] = None) -> Tuple[str, str]:
37
+ """
38
+ Derives the corresponding prompt file path from a code file path.
39
+ Searches for and creates prompts only in the specified output directory or 'prompts' directory.
40
+ If the prompt file does not exist, it creates an empty one in the target directory.
41
+
42
+ Args:
43
+ code_file_path: Path to the code file
44
+ quiet: Whether to suppress output messages
45
+ output_dir: Custom output directory (overrides default 'prompts' directory)
46
+ """
47
+ language = get_language(os.path.splitext(code_file_path)[1])
48
+ language = language.lower() if language else "unknown"
49
+
50
+ # Extract the filename without extension and directory
51
+ code_filename = os.path.basename(code_file_path)
52
+ base_name, _ = os.path.splitext(code_filename)
53
+
54
+ # Determine the output directory
55
+ if output_dir:
56
+ # Use the custom output directory (absolute path)
57
+ prompts_dir = os.path.abspath(output_dir)
58
+ else:
59
+ # Find the repository root (where the code file is located)
60
+ code_file_abs_path = os.path.abspath(code_file_path)
61
+ code_dir = os.path.dirname(code_file_abs_path)
62
+
63
+ # For repository mode, find the actual repo root
64
+ repo_root = code_dir
65
+ try:
66
+ import git
67
+ repo = git.Repo(code_dir, search_parent_directories=True)
68
+ repo_root = repo.working_tree_dir
69
+ except:
70
+ # If not a git repo, use the directory containing the code file
71
+ pass
72
+
73
+ # Use the default prompts directory at repo root
74
+ prompts_dir = os.path.join(repo_root, "prompts")
75
+
76
+ # Construct the prompt filename in the determined directory
77
+ prompt_filename = f"{base_name}_{language}.prompt"
78
+ prompt_path_str = os.path.join(prompts_dir, prompt_filename)
79
+ prompt_path = Path(prompt_path_str)
80
+
81
+ # Ensure prompts directory exists
82
+ prompts_path = Path(prompts_dir)
83
+ if not prompts_path.exists():
84
+ try:
85
+ prompts_path.mkdir(parents=True, exist_ok=True)
86
+ if not quiet:
87
+ console.print(f"[success]Created prompts directory:[/success] [path]{prompts_dir}[/path]")
88
+ except OSError as e:
89
+ console.print(f"[error]Failed to create prompts directory {prompts_dir}: {e}[/error]")
90
+
91
+ if not prompt_path.exists():
92
+ try:
93
+ prompt_path.touch()
94
+ if not quiet:
95
+ console.print(f"[success]Created missing prompt file:[/success] [path]{prompt_path_str}[/path]")
96
+ except OSError as e:
97
+ console.print(f"[error]Failed to create file {prompt_path_str}: {e}[/error]")
98
+ # Even if creation fails, return the intended path
99
+
100
+ return prompt_path_str, code_file_path
101
+
102
+ def find_and_resolve_all_pairs(repo_root: str, quiet: bool = False, extensions: Optional[str] = None, output_dir: Optional[str] = None) -> List[Tuple[str, str]]:
103
+ """
104
+ Scans the repo for code files, resolves their prompt pairs, and returns all pairs.
105
+ """
106
+ pairs = []
107
+ ignored_dirs = {'.git', '.idea', '.vscode', '__pycache__', 'node_modules', '.venv', 'venv', 'dist', 'build'}
108
+
109
+ if not quiet:
110
+ console.print(f"[info]Scanning repository and resolving prompt/code pairs...[/info]")
111
+
112
+ allowed_extensions: Optional[set] = None
113
+ if extensions:
114
+ ext_list = [e.strip().lower() for e in extensions.split(',')]
115
+ allowed_extensions = {f'.{e}' if not e.startswith('.') else e for e in ext_list}
116
+ if not quiet:
117
+ console.print(f"[info]Filtering for extensions: {', '.join(allowed_extensions)}[/info]")
118
+
119
+ all_files = []
120
+ for root, dirs, files in os.walk(repo_root, topdown=True):
121
+ dirs[:] = [d for d in dirs if d not in ignored_dirs]
122
+ for file in files:
123
+ all_files.append(os.path.join(root, file))
124
+
125
+ code_files = [
126
+ f for f in all_files
127
+ if (
128
+ get_language(f) is not None and
129
+ not f.endswith('.prompt') and
130
+ not os.path.splitext(os.path.basename(f))[0].startswith('test_') and
131
+ not os.path.splitext(os.path.basename(f))[0].endswith('_example')
132
+ )
133
+ ]
134
+
135
+ if allowed_extensions:
136
+ code_files = [
137
+ f for f in code_files
138
+ if os.path.splitext(f)[1].lower() in allowed_extensions
139
+ ]
140
+
141
+ for file_path in code_files:
142
+ prompt_path, code_path = resolve_prompt_code_pair(file_path, quiet, output_dir)
143
+ pairs.append((prompt_path, code_path))
144
+
145
+ return pairs
146
+
147
+ def update_file_pair(prompt_file: str, code_file: str, ctx: click.Context, repo: git.Repo, simple: bool = False) -> Dict[str, Any]:
148
+ """
149
+ Wrapper to update a single file pair, choosing the correct method based on Git status and prompt content.
150
+ """
151
+ try:
152
+ verbose = ctx.obj.get("verbose", False)
153
+ quiet = ctx.obj.get("quiet", False)
154
+
155
+ # Agentic routing - try first before legacy paths
156
+ use_agentic = not simple and get_available_agents()
157
+
158
+ if use_agentic:
159
+ success, message, agentic_cost, provider, changed_files = run_agentic_update(
160
+ prompt_file=prompt_file,
161
+ code_file=code_file,
162
+ test_files=None,
163
+ verbose=verbose,
164
+ quiet=quiet,
165
+ )
166
+
167
+ if success:
168
+ with open(prompt_file, 'r') as f:
169
+ modified_prompt = f.read()
170
+ return {
171
+ "prompt_file": prompt_file,
172
+ "status": "✅ Success (agentic)",
173
+ "cost": agentic_cost,
174
+ "model": provider,
175
+ "error": "",
176
+ }
177
+ # Agentic failed - fall through to legacy
178
+
179
+ # Legacy path: Read the prompt first to decide the strategy.
180
+ try:
181
+ with open(prompt_file, 'r') as f:
182
+ input_prompt = f.read()
183
+ except FileNotFoundError:
184
+ input_prompt = ""
185
+
186
+ relative_code_path = os.path.relpath(code_file, repo.working_tree_dir)
187
+ is_untracked = relative_code_path in repo.untracked_files
188
+
189
+ # GENERATION MODE: Trigger if the file is new OR if the prompt is empty.
190
+ if is_untracked or not input_prompt.strip():
191
+ if not quiet:
192
+ if is_untracked:
193
+ console.print(f"[info]New untracked file detected, generating new prompt for:[/info] [path]{relative_code_path}[/path]")
194
+ else:
195
+ console.print(f"[info]Empty prompt detected, generating new prompt for:[/info] [path]{relative_code_path}[/path]")
196
+
197
+ with open(code_file, 'r') as f:
198
+ modified_code = f.read()
199
+
200
+ modified_prompt, total_cost, model_name = update_prompt(
201
+ input_prompt="no prompt exists yet, create a new one",
202
+ input_code="", # No previous version for generation
203
+ modified_code=modified_code,
204
+ strength=ctx.obj.get("strength", 0.5),
205
+ temperature=ctx.obj.get("temperature", 0),
206
+ verbose=verbose,
207
+ time=ctx.obj.get('time', DEFAULT_TIME),
208
+ )
209
+ # UPDATE MODE: Only trigger if the file is tracked AND the prompt has content.
210
+ else:
211
+ modified_prompt, total_cost, model_name = git_update(
212
+ input_prompt=input_prompt,
213
+ modified_code_file=code_file,
214
+ strength=ctx.obj.get("strength", 0.5),
215
+ temperature=ctx.obj.get("temperature", 0),
216
+ verbose=verbose,
217
+ time=ctx.obj.get('time', DEFAULT_TIME),
218
+ simple=True, # Force legacy since we already tried agentic
219
+ quiet=quiet,
220
+ prompt_file=prompt_file,
221
+ )
222
+
223
+ if modified_prompt is not None:
224
+ # Overwrite the original prompt file
225
+ with open(prompt_file, "w") as f:
226
+ f.write(modified_prompt)
227
+ return {
228
+ "prompt_file": prompt_file,
229
+ "status": "✅ Success",
230
+ "cost": total_cost,
231
+ "model": model_name,
232
+ "error": "",
233
+ }
234
+ else:
235
+ return {
236
+ "prompt_file": prompt_file,
237
+ "status": "❌ Failed",
238
+ "cost": 0.0,
239
+ "model": "",
240
+ "error": "Update process returned no result.",
241
+ }
242
+ except click.Abort:
243
+ # User cancelled - re-raise to stop the sync loop
244
+ raise
245
+ except Exception as e:
246
+ return {
247
+ "prompt_file": prompt_file,
248
+ "status": "❌ Failed",
249
+ "cost": 0.0,
250
+ "model": "",
251
+ "error": str(e),
252
+ }
253
+
10
254
  def update_main(
11
255
  ctx: click.Context,
12
- input_prompt_file: str,
13
- modified_code_file: str,
256
+ input_prompt_file: Optional[str],
257
+ modified_code_file: Optional[str],
14
258
  input_code_file: Optional[str],
15
259
  output: Optional[str],
16
- git: bool = False,
17
- ) -> Tuple[str, float, str]:
260
+ use_git: bool = False,
261
+ repo: bool = False,
262
+ extensions: Optional[str] = None,
263
+ strength: Optional[float] = None,
264
+ temperature: Optional[float] = None,
265
+ simple: bool = False,
266
+ ) -> Optional[Tuple[str, float, str]]:
18
267
  """
19
268
  CLI wrapper for updating prompts based on modified code.
269
+ Can operate on a single file or an entire repository.
20
270
 
21
271
  :param ctx: Click context object containing CLI options and parameters.
22
272
  :param input_prompt_file: Path to the original prompt file.
23
273
  :param modified_code_file: Path to the modified code file.
24
274
  :param input_code_file: Optional path to the original code file. If None, Git history is used if --git is True.
25
275
  :param output: Optional path to save the updated prompt.
26
- :param git: Use Git history to retrieve the original code if True.
276
+ :param use_git: Use Git history to retrieve the original code if True.
277
+ :param repo: If True, run in repository-wide mode.
278
+ :param extensions: Comma-separated string of file extensions to filter by in repo mode.
279
+ :param strength: Optional strength parameter (overrides ctx.obj if provided).
280
+ :param temperature: Optional temperature parameter (overrides ctx.obj if provided).
27
281
  :return: Tuple containing the updated prompt, total cost, and model name.
28
282
  """
29
- try:
30
- # Construct file paths
31
- input_file_paths = {"input_prompt_file": input_prompt_file, "modified_code_file": modified_code_file}
32
- if input_code_file:
33
- input_file_paths["input_code_file"] = input_code_file
34
-
35
- # Validate input requirements
36
- if not git and input_code_file is None:
37
- raise ValueError("Must provide an input code file or use --git option.")
38
-
39
- if output is None:
40
- # Default to overwriting the original prompt file when no explicit output specified
41
- # This preserves the "prompts as source of truth" philosophy
42
- command_options = {"output": input_prompt_file}
43
- else:
44
- command_options = {"output": output}
45
- resolved_config, input_strings, output_file_paths, _ = construct_paths(
46
- input_file_paths=input_file_paths,
47
- force=ctx.obj.get("force", False),
48
- quiet=ctx.obj.get("quiet", False),
49
- command="update",
50
- command_options=command_options,
283
+ quiet = ctx.obj.get("quiet", False)
284
+ # Resolve strength/temperature (prefer passed parameters over ctx.obj)
285
+ resolved_strength = strength if strength is not None else ctx.obj.get("strength", 0.5)
286
+ resolved_temperature = temperature if temperature is not None else ctx.obj.get("temperature", 0)
287
+ # Update ctx.obj so internal calls use the resolved values
288
+ ctx.obj["strength"] = resolved_strength
289
+ ctx.obj["temperature"] = resolved_temperature
290
+ if repo:
291
+ try:
292
+ # Find the repo root by searching up from the current directory
293
+ repo_obj = git.Repo(os.getcwd(), search_parent_directories=True)
294
+ repo_root = repo_obj.working_tree_dir
295
+ except git.InvalidGitRepositoryError:
296
+ rprint("[bold red]Error:[/bold red] Repository-wide mode requires the current directory to be within a Git repository.")
297
+ # Return error result instead of sys.exit(1) to allow orchestrator to handle gracefully
298
+ return None
299
+
300
+ pairs = find_and_resolve_all_pairs(repo_root, quiet, extensions, output)
301
+
302
+ if not pairs:
303
+ rprint("[info]No scannable code files found in the repository.[/info]")
304
+ return None
305
+
306
+ rprint(f"[info]Found {len(pairs)} total prompt/code pairs to process.[/info]")
307
+
308
+ results = []
309
+ total_repo_cost = 0.0
310
+
311
+ progress = Progress(
312
+ SpinnerColumn(),
313
+ TextColumn("[progress.description]{task.description}", justify="right"),
314
+ BarColumn(bar_width=None),
315
+ TextColumn("[progress.percentage]{task.percentage:>3.1f}%"),
316
+ TextColumn("•"),
317
+ TimeRemainingColumn(),
318
+ TextColumn("•"),
319
+ TextColumn("Total Cost: $[bold green]{task.fields[total_cost]:.6f}[/bold green]"),
320
+ console=console,
321
+ transient=True,
51
322
  )
52
323
 
53
- # Extract input strings
54
- input_prompt = input_strings["input_prompt_file"]
55
- modified_code = input_strings["modified_code_file"]
56
- input_code = input_strings.get("input_code_file")
57
- time = ctx.obj.get('time', DEFAULT_TIME)
324
+ with progress:
325
+ task = progress.add_task(
326
+ "Updating prompts...",
327
+ total=len(pairs),
328
+ total_cost=0.0
329
+ )
330
+
331
+ for prompt_path, code_path in pairs:
332
+ relative_path = os.path.relpath(code_path, repo_root)
333
+ progress.update(task, description=f"Processing [path]{relative_path}[/path]")
334
+
335
+ result = update_file_pair(prompt_path, code_path, ctx, repo_obj, simple=simple)
336
+ results.append(result)
337
+
338
+ total_repo_cost += result.get("cost", 0.0)
339
+
340
+ progress.update(task, advance=1, total_cost=total_repo_cost)
58
341
 
59
- # Update prompt using appropriate method
60
- if git:
61
- if input_code_file:
62
- raise ValueError("Cannot use both --git and provide an input code file.")
63
- modified_prompt, total_cost, model_name = git_update(
64
- input_prompt=input_prompt,
65
- modified_code_file=modified_code_file,
66
- strength=ctx.obj.get("strength", 0.5),
67
- temperature=ctx.obj.get("temperature", 0),
68
- verbose=ctx.obj.get("verbose", False),
69
- time=time
342
+ table = Table(show_header=True, header_style="bold magenta")
343
+ table.add_column("Prompt File", style="dim", width=50)
344
+ table.add_column("Status")
345
+ table.add_column("Cost", justify="right")
346
+ table.add_column("Model")
347
+ table.add_column("Error", style="error")
348
+
349
+ models_used = set()
350
+ for res in sorted(results, key=lambda x: x["prompt_file"]):
351
+ table.add_row(
352
+ os.path.relpath(res["prompt_file"], repo_root),
353
+ res["status"],
354
+ f"${res['cost']:.6f}",
355
+ res["model"],
356
+ res["error"],
70
357
  )
71
- else:
72
- if input_code is None:
73
- raise ValueError("Must provide an input code file or use --git option.")
358
+ if res["model"]:
359
+ models_used.add(res["model"])
360
+
361
+ console.print("\n[bold]Repository Update Summary[/bold]")
362
+ console.print(table)
363
+ console.print(f"\n[bold]Total Estimated Cost: ${total_repo_cost:.6f}[/bold]")
364
+
365
+ final_model_str = ", ".join(sorted(models_used)) if models_used else "N/A"
366
+ return "Repository update complete.", total_repo_cost, final_model_str
367
+
368
+ # --- Single file logic ---
369
+ try:
370
+ # Case 1: Regeneration Mode.
371
+ # Triggered when ONLY the modified_code_file is provided.
372
+ # This creates a new prompt or overwrites an existing one from scratch.
373
+ is_regeneration_mode = (input_prompt_file is None and input_code_file is None)
374
+
375
+ if is_regeneration_mode:
376
+ if not quiet:
377
+ rprint("[bold yellow]Regeneration mode: Creating or overwriting prompt from code file.[/bold yellow]")
378
+
379
+ # Determine output path based on --output flag
380
+ if output:
381
+ # Check if output is a directory or file path
382
+ if os.path.isdir(output) or output.endswith('/'):
383
+ # Output is a directory, pass as output_dir to resolve_prompt_code_pair
384
+ prompt_path, _ = resolve_prompt_code_pair(modified_code_file, quiet, output)
385
+ else:
386
+ # Output is a specific file path, use it directly
387
+ prompt_path = os.path.abspath(output)
388
+ # Ensure the directory exists
389
+ os.makedirs(os.path.dirname(prompt_path), exist_ok=True)
390
+ else:
391
+ # No output specified, use default behavior
392
+ prompt_path, _ = resolve_prompt_code_pair(modified_code_file, quiet)
393
+
394
+ # Agentic routing for regeneration mode
395
+ use_agentic = not simple and get_available_agents()
396
+ verbose = ctx.obj.get("verbose", False)
397
+
398
+ if use_agentic:
399
+ # Ensure prompt file exists for agentic
400
+ Path(prompt_path).touch(exist_ok=True)
401
+
402
+ success, message, agentic_cost, provider, changed_files = run_agentic_update(
403
+ prompt_file=prompt_path,
404
+ code_file=modified_code_file,
405
+ test_files=None,
406
+ verbose=verbose,
407
+ quiet=quiet,
408
+ )
409
+
410
+ if success:
411
+ with open(prompt_path, 'r') as f:
412
+ generated_prompt = f.read()
413
+
414
+ if not quiet:
415
+ rprint("[bold green]Prompt generated successfully (agentic).[/bold green]")
416
+ rprint(f"[bold]Provider:[/bold] {provider}")
417
+ rprint(f"[bold]Total cost:[/bold] ${agentic_cost:.6f}")
418
+ rprint(f"[bold]Prompt saved to:[/bold] {prompt_path}")
419
+
420
+ return generated_prompt, agentic_cost, provider
421
+
422
+ # Agentic failed - fall through to legacy
423
+ if not quiet:
424
+ rprint(f"[warning]Agentic failed: {message}. Falling back to legacy.[/warning]")
425
+
426
+ # Legacy path
427
+ with open(modified_code_file, 'r') as f:
428
+ modified_code_content = f.read()
429
+
74
430
  modified_prompt, total_cost, model_name = update_prompt(
75
- input_prompt=input_prompt,
76
- input_code=input_code,
77
- modified_code=modified_code,
431
+ input_prompt="no prompt exists yet, create a new one",
432
+ input_code="",
433
+ modified_code=modified_code_content,
78
434
  strength=ctx.obj.get("strength", 0.5),
79
435
  temperature=ctx.obj.get("temperature", 0),
80
- verbose=ctx.obj.get("verbose", False),
81
- time=time
436
+ verbose=verbose,
437
+ time=ctx.obj.get('time', DEFAULT_TIME)
82
438
  )
83
439
 
84
- # Save the modified prompt
85
- with open(output_file_paths["output"], "w") as f:
86
- f.write(modified_prompt)
440
+ # Write the result to the derived/correct prompt path.
441
+ with open(prompt_path, "w") as f:
442
+ f.write(modified_prompt)
443
+
444
+ if not quiet:
445
+ rprint("[bold green]Prompt generated successfully.[/bold green]")
446
+ rprint(f"[bold]Model used:[/bold] {model_name}")
447
+ rprint(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
448
+ rprint(f"[bold]Prompt saved to:[/bold] {prompt_path}")
449
+
450
+ return modified_prompt, total_cost, model_name
451
+
452
+ # Case 2: True Update Mode.
453
+ # Triggered when the user provides the prompt file, indicating a desire to update it.
454
+ else:
455
+ actual_input_prompt_file = input_prompt_file
456
+ final_output_path = output or actual_input_prompt_file
457
+ verbose = ctx.obj.get("verbose", False)
458
+
459
+ # Agentic routing for true update mode (try before construct_paths)
460
+ use_agentic = not simple and get_available_agents()
461
+
462
+ if use_agentic:
463
+ success, message, agentic_cost, provider, changed_files = run_agentic_update(
464
+ prompt_file=actual_input_prompt_file,
465
+ code_file=modified_code_file,
466
+ test_files=None,
467
+ verbose=verbose,
468
+ quiet=quiet,
469
+ )
470
+
471
+ if success:
472
+ with open(actual_input_prompt_file, 'r') as f:
473
+ updated_prompt = f.read()
474
+
475
+ # Handle output path if different from input
476
+ if final_output_path != actual_input_prompt_file:
477
+ with open(final_output_path, 'w') as f:
478
+ f.write(updated_prompt)
479
+
480
+ if not quiet:
481
+ rprint("[bold green]Prompt updated successfully (agentic).[/bold green]")
482
+ rprint(f"[bold]Provider:[/bold] {provider}")
483
+ rprint(f"[bold]Total cost:[/bold] ${agentic_cost:.6f}")
484
+ rprint(f"[bold]Updated prompt saved to:[/bold] {final_output_path}")
485
+
486
+ return updated_prompt, agentic_cost, provider
487
+
488
+ # Agentic failed - fall through to legacy
489
+ if not quiet:
490
+ rprint(f"[warning]Agentic failed: {message}. Falling back to legacy.[/warning]")
491
+
492
+ # Legacy path: Prepare input_file_paths for construct_paths
493
+ input_file_paths = {
494
+ "input_prompt_file": actual_input_prompt_file,
495
+ "modified_code_file": modified_code_file
496
+ }
497
+ if input_code_file:
498
+ input_file_paths["input_code_file"] = input_code_file
499
+
500
+ command_options = {"output": final_output_path}
501
+
502
+ _, input_strings, output_file_paths, _ = construct_paths(
503
+ input_file_paths=input_file_paths,
504
+ force=ctx.obj.get("force", False),
505
+ quiet=quiet,
506
+ command="update",
507
+ command_options=command_options,
508
+ context_override=ctx.obj.get('context'),
509
+ confirm_callback=ctx.obj.get('confirm_callback')
510
+ )
511
+
512
+ input_prompt = input_strings["input_prompt_file"]
513
+ modified_code = input_strings["modified_code_file"]
514
+ input_code = input_strings.get("input_code_file")
515
+ time = ctx.obj.get('time', DEFAULT_TIME)
516
+
517
+ if not modified_code.strip():
518
+ raise ValueError("Modified code file cannot be empty when updating or generating a prompt.")
519
+
520
+ if not input_prompt.strip():
521
+ input_prompt = "no prompt exists yet, create a new one"
522
+ if not use_git and input_code is None:
523
+ input_code = ""
524
+ if not quiet:
525
+ rprint("[bold yellow]Empty prompt file detected. Generating a new prompt from the modified code.[/bold yellow]")
526
+
527
+ if use_git:
528
+ if input_code_file:
529
+ raise ValueError("Cannot use both --git and provide an input code file.")
530
+ modified_prompt, total_cost, model_name = git_update(
531
+ input_prompt=input_prompt,
532
+ modified_code_file=modified_code_file,
533
+ strength=ctx.obj.get("strength", 0.5),
534
+ temperature=ctx.obj.get("temperature", 0),
535
+ verbose=verbose,
536
+ time=time,
537
+ simple=True if use_agentic else simple, # Force legacy if agentic was tried
538
+ quiet=quiet,
539
+ prompt_file=actual_input_prompt_file,
540
+ )
541
+ else:
542
+ if input_code is None:
543
+ # This will now only be triggered if --git is not used and no input_code_file is provided,
544
+ # which is an error state for a true update.
545
+ raise ValueError("For a true update, you must either provide an original code file or use the --git flag.")
546
+
547
+ modified_prompt, total_cost, model_name = update_prompt(
548
+ input_prompt=input_prompt,
549
+ input_code=input_code,
550
+ modified_code=modified_code,
551
+ strength=ctx.obj.get("strength", 0.5),
552
+ temperature=ctx.obj.get("temperature", 0),
553
+ verbose=verbose,
554
+ time=time
555
+ )
556
+
557
+ # Defense-in-depth: validate prompt is not empty before writing
558
+ if not modified_prompt or not modified_prompt.strip():
559
+ raise ValueError(
560
+ "Update produced an empty prompt. The LLM may have failed to generate a valid response."
561
+ )
562
+
563
+ with open(output_file_paths["output"], "w") as f:
564
+ f.write(modified_prompt)
87
565
 
88
- # Provide user feedback
89
- if not ctx.obj.get("quiet", False):
90
- rprint("[bold green]Prompt updated successfully.[/bold green]")
91
- rprint(f"[bold]Model used:[/bold] {model_name}")
92
- rprint(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
93
- rprint(f"[bold]Updated prompt saved to:[/bold] {output_file_paths['output']}")
566
+ if not quiet:
567
+ rprint("[bold green]Prompt updated successfully.[/bold green]")
568
+ rprint(f"[bold]Model used:[/bold] {model_name}")
569
+ rprint(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
570
+ rprint(f"[bold]Updated prompt saved to:[/bold] {output_file_paths['output']}")
94
571
 
95
- return modified_prompt, total_cost, model_name
572
+ return modified_prompt, total_cost, model_name
96
573
 
97
- except ValueError as e:
98
- if not ctx.obj.get("quiet", False):
574
+ except (ValueError, git.InvalidGitRepositoryError) as e:
575
+ if not quiet:
99
576
  rprint(f"[bold red]Input error:[/bold red] {str(e)}")
100
- sys.exit(1)
577
+ # Return error result instead of sys.exit(1) to allow orchestrator to handle gracefully
578
+ return None
579
+ except click.Abort:
580
+ # User cancelled - re-raise to stop the sync loop
581
+ raise
101
582
  except Exception as e:
102
- if not ctx.obj.get("quiet", False):
583
+ if not quiet:
103
584
  rprint(f"[bold red]Error:[/bold red] {str(e)}")
104
- sys.exit(1)
585
+ # Return error result instead of sys.exit(1) to allow orchestrator to handle gracefully
586
+ return None
pdd/update_model_costs.py CHANGED
@@ -404,8 +404,8 @@ def main():
404
404
  parser.add_argument(
405
405
  "--csv-path",
406
406
  type=str,
407
- default="data/llm_model.csv",
408
- help="Path to the llm_model.csv file (default: data/llm_model.csv)"
407
+ default=".pdd/llm_model.csv",
408
+ help="Path to the llm_model.csv file (default: .pdd/llm_model.csv)"
409
409
  )
410
410
  args = parser.parse_args()
411
411