pdd-cli 0.0.45__py3-none-any.whl → 0.0.118__py3-none-any.whl

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