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/auth_service.py ADDED
@@ -0,0 +1,210 @@
1
+ """Shared authentication service for PDD Cloud.
2
+
3
+ This module provides common authentication functions used by both:
4
+ - REST API endpoints (pdd/server/routes/auth.py) for the web frontend
5
+ - CLI commands (pdd/commands/auth.py) for terminal-based auth management
6
+
7
+ By centralizing auth logic here, we ensure consistent behavior across interfaces.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import time
13
+ from pathlib import Path
14
+ from typing import Optional, Tuple, Dict, Any
15
+
16
+
17
+ # JWT file cache path
18
+ JWT_CACHE_FILE = Path.home() / ".pdd" / "jwt_cache"
19
+
20
+ # Keyring configuration (must match app_name="PDD CLI" used in commands/auth.py)
21
+ KEYRING_SERVICE_NAME = "firebase-auth-PDD CLI"
22
+ KEYRING_USER_NAME = "refresh_token"
23
+
24
+
25
+ def get_jwt_cache_info() -> Tuple[bool, Optional[float]]:
26
+ """
27
+ Check JWT cache file for valid token.
28
+
29
+ Returns:
30
+ Tuple of (is_valid, expires_at). If valid, expires_at is the timestamp
31
+ when the token expires. If invalid or not found, returns (False, None).
32
+ """
33
+ if not JWT_CACHE_FILE.exists():
34
+ return False, None
35
+
36
+ try:
37
+ with open(JWT_CACHE_FILE, "r") as f:
38
+ cache = json.load(f)
39
+ expires_at = cache.get("expires_at", 0)
40
+ # Check if token is still valid (with 5 minute buffer)
41
+ if expires_at > time.time() + 300:
42
+ return True, expires_at
43
+ except (json.JSONDecodeError, IOError, KeyError):
44
+ pass
45
+
46
+ return False, None
47
+
48
+
49
+ def get_cached_jwt() -> Optional[str]:
50
+ """
51
+ Get the cached JWT token if it exists and is valid.
52
+
53
+ Returns:
54
+ The JWT token string if valid, None otherwise.
55
+ """
56
+ if not JWT_CACHE_FILE.exists():
57
+ return None
58
+
59
+ try:
60
+ with open(JWT_CACHE_FILE, "r") as f:
61
+ cache = json.load(f)
62
+ expires_at = cache.get("expires_at", 0)
63
+ # Check if token is still valid (with 5 minute buffer)
64
+ if expires_at > time.time() + 300:
65
+ # Check both 'id_token' (new) and 'jwt' (legacy) keys for backwards compatibility
66
+ return cache.get("id_token") or cache.get("jwt")
67
+ except (json.JSONDecodeError, IOError, KeyError):
68
+ pass
69
+
70
+ return None
71
+
72
+
73
+ def has_refresh_token() -> bool:
74
+ """
75
+ Check if there's a stored refresh token in keyring.
76
+
77
+ Returns:
78
+ True if a refresh token exists, False otherwise.
79
+ """
80
+ try:
81
+ import keyring
82
+
83
+ token = keyring.get_password(KEYRING_SERVICE_NAME, KEYRING_USER_NAME)
84
+ return token is not None
85
+ except ImportError:
86
+ # Try alternative keyring
87
+ try:
88
+ import keyrings.alt.file
89
+
90
+ kr = keyrings.alt.file.PlaintextKeyring()
91
+ token = kr.get_password(KEYRING_SERVICE_NAME, KEYRING_USER_NAME)
92
+ return token is not None
93
+ except ImportError:
94
+ pass
95
+ except Exception:
96
+ pass
97
+
98
+ return False
99
+
100
+
101
+ def clear_jwt_cache() -> Tuple[bool, Optional[str]]:
102
+ """
103
+ Clear the JWT cache file.
104
+
105
+ Returns:
106
+ Tuple of (success, error_message). If successful, error_message is None.
107
+ """
108
+ if not JWT_CACHE_FILE.exists():
109
+ return True, None
110
+
111
+ try:
112
+ JWT_CACHE_FILE.unlink()
113
+ return True, None
114
+ except Exception as e:
115
+ return False, f"Failed to clear JWT cache: {e}"
116
+
117
+
118
+ def clear_refresh_token() -> Tuple[bool, Optional[str]]:
119
+ """
120
+ Clear the refresh token from keyring.
121
+
122
+ Returns:
123
+ Tuple of (success, error_message). If successful, error_message is None.
124
+ """
125
+ try:
126
+ import keyring
127
+
128
+ keyring.delete_password(KEYRING_SERVICE_NAME, KEYRING_USER_NAME)
129
+ return True, None
130
+ except ImportError:
131
+ # Try alternative keyring
132
+ try:
133
+ import keyrings.alt.file
134
+
135
+ kr = keyrings.alt.file.PlaintextKeyring()
136
+ kr.delete_password(KEYRING_SERVICE_NAME, KEYRING_USER_NAME)
137
+ return True, None
138
+ except ImportError:
139
+ return True, None # No keyring available, nothing to clear
140
+ except Exception as e:
141
+ return False, f"Failed to clear refresh token: {e}"
142
+ except Exception as e:
143
+ error_str = str(e)
144
+ # Ignore "not found" errors - token was already deleted
145
+ if "not found" in error_str.lower() or "no matching" in error_str.lower():
146
+ return True, None
147
+ return False, f"Failed to clear refresh token: {e}"
148
+
149
+
150
+ def get_auth_status() -> Dict[str, Any]:
151
+ """
152
+ Get current authentication status.
153
+
154
+ Returns:
155
+ Dict with keys:
156
+ - authenticated: bool - True if user has valid auth
157
+ - cached: bool - True if using cached JWT (vs refresh token)
158
+ - expires_at: Optional[float] - JWT expiration timestamp if cached
159
+ """
160
+ # First check JWT cache
161
+ cache_valid, expires_at = get_jwt_cache_info()
162
+ if cache_valid:
163
+ return {
164
+ "authenticated": True,
165
+ "cached": True,
166
+ "expires_at": expires_at,
167
+ }
168
+
169
+ # Check for refresh token in keyring
170
+ has_refresh = has_refresh_token()
171
+ if has_refresh:
172
+ return {
173
+ "authenticated": True,
174
+ "cached": False,
175
+ "expires_at": None,
176
+ }
177
+
178
+ return {
179
+ "authenticated": False,
180
+ "cached": False,
181
+ "expires_at": None,
182
+ }
183
+
184
+
185
+ def logout() -> Tuple[bool, Optional[str]]:
186
+ """
187
+ Clear all authentication tokens (logout).
188
+
189
+ Clears both the JWT cache file and the refresh token from keyring.
190
+
191
+ Returns:
192
+ Tuple of (success, error_message). If any error occurred,
193
+ success is False and error_message contains the details.
194
+ """
195
+ errors = []
196
+
197
+ # Clear JWT cache
198
+ jwt_success, jwt_error = clear_jwt_cache()
199
+ if not jwt_success and jwt_error:
200
+ errors.append(jwt_error)
201
+
202
+ # Clear refresh token from keyring
203
+ refresh_success, refresh_error = clear_refresh_token()
204
+ if not refresh_success and refresh_error:
205
+ errors.append(refresh_error)
206
+
207
+ if errors:
208
+ return False, "; ".join(errors)
209
+
210
+ return True, None
pdd/auto_deps_main.py CHANGED
@@ -1,38 +1,36 @@
1
- """Main function for the auto-deps command."""
1
+ from __future__ import annotations
2
2
  import sys
3
3
  from pathlib import Path
4
- from typing import Tuple, Optional
4
+ from typing import Optional, Tuple, Callable
5
5
  import click
6
6
  from rich import print as rprint
7
+ from filelock import FileLock
7
8
 
8
9
  from . import DEFAULT_STRENGTH, DEFAULT_TIME
9
10
  from .construct_paths import construct_paths
10
11
  from .insert_includes import insert_includes
11
12
 
12
- def auto_deps_main( # pylint: disable=too-many-arguments, too-many-locals
13
+
14
+ def auto_deps_main(
13
15
  ctx: click.Context,
14
16
  prompt_file: str,
15
17
  directory_path: str,
16
18
  auto_deps_csv_path: Optional[str],
17
19
  output: Optional[str],
18
- force_scan: Optional[bool]
20
+ force_scan: Optional[bool] = False,
21
+ progress_callback: Optional[Callable[[int, int], None]] = None
19
22
  ) -> Tuple[str, float, str]:
20
23
  """
21
- Main function to analyze and insert dependencies into a prompt file.
22
-
23
- Args:
24
- ctx: Click context containing command-line parameters.
25
- prompt_file: Path to the input prompt file.
26
- directory_path: Path to directory containing potential dependency files.
27
- auto_deps_csv_path: Path to CSV file containing auto-dependency information.
28
- output: Optional path to save the modified prompt file.
29
- force_scan: Flag to force rescan of directory by deleting CSV file.
24
+ Main function to analyze a prompt file and insert dependencies found in a directory.
30
25
 
31
- Returns:
32
- Tuple containing:
33
- - str: Modified prompt with auto-dependencies added
34
- - float: Total cost of the operation
35
- - str: Name of the model used
26
+ :param ctx: Click context containing command-line parameters.
27
+ :param prompt_file: Path to the input prompt file.
28
+ :param directory_path: Path to the directory or glob pattern containing potential dependency files.
29
+ :param auto_deps_csv_path: Preferred CSV file path for dependency info (may be overridden by resolved paths).
30
+ :param output: File path (or directory) to save the modified prompt file.
31
+ :param force_scan: Flag to force a rescan by deleting the existing CSV cache.
32
+ :param progress_callback: Optional callback for progress updates (current, total).
33
+ :return: A tuple containing the modified prompt, total cost, and model name used.
36
34
  """
37
35
  try:
38
36
  # Construct file paths
@@ -43,62 +41,84 @@ def auto_deps_main( # pylint: disable=too-many-arguments, too-many-locals
43
41
  "output": output,
44
42
  "csv": auto_deps_csv_path
45
43
  }
46
-
44
+
47
45
  resolved_config, input_strings, output_file_paths, _ = construct_paths(
48
46
  input_file_paths=input_file_paths,
49
47
  force=ctx.obj.get('force', False),
50
48
  quiet=ctx.obj.get('quiet', False),
51
49
  command="auto-deps",
52
- command_options=command_options
50
+ command_options=command_options,
51
+ context_override=ctx.obj.get('context'),
52
+ confirm_callback=ctx.obj.get('confirm_callback')
53
53
  )
54
54
 
55
- # Get the CSV file path
55
+ # Resolve CSV path
56
56
  csv_path = output_file_paths.get("csv", "project_dependencies.csv")
57
57
 
58
- # Handle force_scan option
58
+ # Handle force scan option
59
59
  if force_scan and Path(csv_path).exists():
60
60
  if not ctx.obj.get('quiet', False):
61
- rprint(
62
- "[yellow]Removing existing CSV file due to "
63
- f"--force-scan option: {csv_path}[/yellow]"
64
- )
65
- Path(csv_path).unlink()
61
+ rprint(f"[yellow]Removing existing CSV file due to --force-scan option: {csv_path}[/yellow]")
62
+ try:
63
+ Path(csv_path).unlink()
64
+ except OSError as e:
65
+ if not ctx.obj.get('quiet', False):
66
+ rprint(f"[yellow]Warning: Could not delete CSV file: {e}[/yellow]")
66
67
 
67
- # Get strength and temperature from context
68
- strength = ctx.obj.get('strength', DEFAULT_STRENGTH)
69
- temperature = ctx.obj.get('temperature', 0)
70
- time_budget = ctx.obj.get('time', DEFAULT_TIME)
68
+ # Acquire lock to prevent concurrent access to the CSV cache
69
+ lock_path = f"{csv_path}.lock"
70
+ lock = FileLock(lock_path)
71
+
72
+ with lock:
73
+ # Load input file
74
+ prompt_content = input_strings["prompt_file"]
71
75
 
72
- # Call insert_includes with the prompt content and directory path
73
- modified_prompt, csv_output, total_cost, model_name = insert_includes(
74
- input_prompt=input_strings["prompt_file"],
75
- directory_path=directory_path,
76
- csv_filename=csv_path,
77
- strength=strength,
78
- temperature=temperature,
79
- time=time_budget,
80
- verbose=not ctx.obj.get('quiet', False)
81
- )
76
+ # Get LLM parameters
77
+ strength = ctx.obj.get('strength', DEFAULT_STRENGTH)
78
+ temperature = ctx.obj.get('temperature', 0.0)
79
+ time_budget = ctx.obj.get('time', DEFAULT_TIME)
80
+ verbose = not ctx.obj.get('quiet', False)
81
+
82
+ # Run the dependency analysis and insertion
83
+ modified_prompt, csv_output, total_cost, model_name = insert_includes(
84
+ input_prompt=prompt_content,
85
+ directory_path=directory_path,
86
+ csv_filename=csv_path,
87
+ prompt_filename=prompt_file,
88
+ strength=strength,
89
+ temperature=temperature,
90
+ time=time_budget,
91
+ verbose=verbose,
92
+ progress_callback=progress_callback
93
+ )
82
94
 
83
- # Save the modified prompt to the output file
84
- output_path = output_file_paths["output"]
85
- Path(output_path).write_text(modified_prompt, encoding="utf-8")
95
+ # Save the modified prompt
96
+ output_path = output_file_paths["output"]
97
+ if output_path:
98
+ with open(output_path, 'w', encoding='utf-8') as f:
99
+ f.write(modified_prompt)
86
100
 
87
- # Save the CSV output if it was generated
88
- if csv_output:
89
- Path(csv_path).write_text(csv_output, encoding="utf-8")
101
+ # Save the CSV output if content exists
102
+ if csv_output:
103
+ with open(csv_path, 'w', encoding='utf-8') as f:
104
+ f.write(csv_output)
90
105
 
91
106
  # Provide user feedback
92
107
  if not ctx.obj.get('quiet', False):
93
108
  rprint("[bold green]Successfully analyzed and inserted dependencies![/bold green]")
94
109
  rprint(f"[bold]Model used:[/bold] {model_name}")
95
110
  rprint(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
96
- rprint(f"[bold]Modified prompt saved to:[/bold] {output_path}")
111
+ if output_path:
112
+ rprint(f"[bold]Modified prompt saved to:[/bold] {output_path}")
97
113
  rprint(f"[bold]Dependency information saved to:[/bold] {csv_path}")
98
114
 
99
115
  return modified_prompt, total_cost, model_name
100
116
 
101
- except Exception as exc:
117
+ except click.Abort:
118
+ # User cancelled - re-raise to stop the sync loop
119
+ raise
120
+ except Exception as e:
102
121
  if not ctx.obj.get('quiet', False):
103
- rprint(f"[bold red]Error:[/bold red] {str(exc)}")
104
- sys.exit(1)
122
+ rprint(f"[bold red]Error:[/bold red] {str(e)}")
123
+ # Return error result instead of sys.exit(1) to allow orchestrator to handle gracefully
124
+ return "", 0.0, f"Error: {e}"
pdd/auto_include.py CHANGED
@@ -2,8 +2,10 @@
2
2
  This module provides the `auto_include` function to automatically find and
3
3
  insert dependencies into a prompt.
4
4
  """
5
+ import re
5
6
  from io import StringIO
6
- from typing import Tuple, Optional
7
+ from pathlib import Path
8
+ from typing import Callable, List, Optional, Set, Tuple
7
9
 
8
10
  import pandas as pd
9
11
  from pydantic import BaseModel, Field
@@ -61,11 +63,17 @@ def _load_prompts() -> tuple[str, str]:
61
63
  return auto_include_prompt, extract_prompt
62
64
 
63
65
 
64
- def _summarize(directory_path: str, csv_file: Optional[str], llm_kwargs: dict) -> tuple[str, float, str]:
66
+ def _summarize(
67
+ directory_path: str,
68
+ csv_file: Optional[str],
69
+ llm_kwargs: dict,
70
+ progress_callback: Optional[Callable[[int, int], None]] = None
71
+ ) -> tuple[str, float, str]:
65
72
  """Summarize the directory."""
66
73
  return summarize_directory(
67
74
  directory_path=directory_path,
68
75
  csv_file=csv_file,
76
+ progress_callback=progress_callback,
69
77
  **llm_kwargs
70
78
  )
71
79
 
@@ -108,14 +116,221 @@ def _run_llm_and_extract(
108
116
  return dependencies, total_cost, model_name
109
117
 
110
118
 
119
+ def _extract_module_name(prompt_filename: Optional[str]) -> Optional[str]:
120
+ """Extract module name from prompt filename.
121
+
122
+ Handles various language suffixes:
123
+ - 'prompts/agentic_fix_python.prompt' -> 'agentic_fix'
124
+ - 'prompts/some_module_LLM.prompt' -> 'some_module'
125
+ - 'prompts/cli_bash.prompt' -> 'cli'
126
+
127
+ Args:
128
+ prompt_filename: The prompt filename to extract the module name from.
129
+
130
+ Returns:
131
+ The module name, or None if it cannot be extracted.
132
+ """
133
+ if not prompt_filename:
134
+ return None
135
+ # Pattern: captures module name before the last underscore + language + .prompt
136
+ # e.g., "agentic_fix_python.prompt" captures "agentic_fix"
137
+ match = re.search(r'([^/]+)_[^_]+\.prompt$', prompt_filename)
138
+ if match:
139
+ return match.group(1)
140
+ return None
141
+
142
+
143
+ def _filter_self_references(dependencies: str, module_name: Optional[str]) -> str:
144
+ """Remove includes that reference the module's own example file.
145
+
146
+ Args:
147
+ dependencies: The dependencies string containing include tags.
148
+ module_name: The module name to filter out self-references for.
149
+
150
+ Returns:
151
+ The dependencies string with self-referential includes removed.
152
+ """
153
+ if not module_name:
154
+ return dependencies
155
+ # Pattern matches: <...><include>context/[subdirs/]{module_name}_example.py</include></...>
156
+ # The (?:[^/]+/)* matches zero or more subdirectory levels (e.g., backend/, frontend/)
157
+ pattern = rf'<[^>]+><include>context/(?:[^/]+/)*{re.escape(module_name)}_example\.py</include></[^>]+>\s*'
158
+ return re.sub(pattern, '', dependencies)
159
+
160
+
161
+ def _fix_malformed_includes(dependencies: str) -> str:
162
+ """Fix malformed [File: ...] patterns to proper <include>...</include> format.
163
+
164
+ The LLM sometimes outputs [File: path] instead of <include>path</include>.
165
+ This function corrects that error.
166
+
167
+ Args:
168
+ dependencies: The dependencies string containing potential malformed includes.
169
+
170
+ Returns:
171
+ The dependencies string with [File:] patterns converted to <include> tags.
172
+ """
173
+ # Pattern: <tag>[File: path]</tag> or <tag>\n[File: path]\n</tag>
174
+ pattern = r'(<[^>]+>)\s*\[File:\s*([^\]]+)\]\s*(</[^>]+>)'
175
+
176
+ def replacer(match: re.Match) -> str:
177
+ opening_tag = match.group(1)
178
+ path = match.group(2).strip() # Strip whitespace from captured path
179
+ closing_tag = match.group(3)
180
+ return f'{opening_tag}<include>{path}</include>{closing_tag}'
181
+
182
+ fixed = re.sub(pattern, replacer, dependencies)
183
+ if fixed != dependencies:
184
+ console.print("[yellow]Warning: Fixed malformed [File:] patterns in dependencies[/yellow]")
185
+ return fixed
186
+
187
+
188
+ def _extract_example_modules(content: str) -> Set[str]:
189
+ """Extract module names from _example.py includes.
190
+
191
+ Args:
192
+ content: The string content to search for include tags.
193
+
194
+ Returns:
195
+ A set of module names extracted from _example.py paths.
196
+ E.g., 'context/agentic_bug_example.py' -> 'agentic_bug'
197
+ """
198
+ pattern = r'<include>(.*?)</include>'
199
+ matches = re.findall(pattern, content, re.DOTALL)
200
+ modules = set()
201
+ for match in matches:
202
+ path = match.strip()
203
+ # Match pattern: context/[subdirs/]module_name_example.py
204
+ example_match = re.search(r'context/(?:[^/]+/)*([^/]+)_example\.py$', path)
205
+ if example_match:
206
+ modules.add(example_match.group(1))
207
+ return modules
208
+
209
+
210
+ def _detect_circular_dependencies(
211
+ current_prompt: str,
212
+ new_dependencies: str,
213
+ prompts_dir: Optional[str] = None
214
+ ) -> List[List[str]]:
215
+ """Detect circular dependencies through example file includes.
216
+
217
+ Detects module-level circular dependencies where:
218
+ - Module A's prompt includes module B's example file
219
+ - Module B's prompt includes module A's example file
220
+
221
+ Args:
222
+ current_prompt: The current prompt file being processed.
223
+ new_dependencies: The new dependencies string to check.
224
+ prompts_dir: Optional base directory for resolving prompt paths.
225
+
226
+ Returns:
227
+ List of cycles found, where each cycle is a list of module names.
228
+ """
229
+ # Extract current module name from prompt filename
230
+ current_module = _extract_module_name(current_prompt)
231
+ if not current_module:
232
+ return []
233
+
234
+ # Extract module names from example includes in new dependencies
235
+ new_dep_modules = _extract_example_modules(new_dependencies)
236
+ if not new_dep_modules:
237
+ return []
238
+
239
+ cycles: List[List[str]] = []
240
+
241
+ # Determine base directory for prompts
242
+ if prompts_dir:
243
+ base_dir = Path(prompts_dir)
244
+ else:
245
+ # Try to find prompts directory relative to current prompt
246
+ current_path = Path(current_prompt)
247
+ if current_path.parent.name == 'prompts' or 'prompts' in str(current_path):
248
+ base_dir = current_path.parent
249
+ else:
250
+ base_dir = Path('prompts')
251
+
252
+ # Extract current prompt filename for cycle reporting
253
+ current_prompt_name = Path(current_prompt).name
254
+
255
+ # For each module we're about to depend on, check if it depends on us
256
+ for dep_module in new_dep_modules:
257
+ # Find the prompt file for this module (try common patterns)
258
+ prompt_patterns = [
259
+ f"{dep_module}_python.prompt",
260
+ f"{dep_module}_LLM.prompt",
261
+ f"{dep_module}.prompt",
262
+ ]
263
+
264
+ for pattern in prompt_patterns:
265
+ prompt_path = base_dir / pattern
266
+ if prompt_path.exists():
267
+ try:
268
+ content = prompt_path.read_text(encoding='utf-8')
269
+ # Check if this prompt includes our example file
270
+ dep_modules = _extract_example_modules(content)
271
+ if current_module in dep_modules:
272
+ # Found circular dependency!
273
+ # Use actual prompt filenames, not hardcoded suffixes
274
+ cycles.append([
275
+ current_prompt_name,
276
+ pattern,
277
+ current_prompt_name
278
+ ])
279
+ except Exception:
280
+ pass
281
+ break
282
+
283
+ return cycles
284
+
285
+
286
+ def _filter_circular_dependencies(dependencies: str, cycles: List[List[str]]) -> str:
287
+ """Remove include tags that would create circular dependencies.
288
+
289
+ Args:
290
+ dependencies: The dependencies string containing include tags.
291
+ cycles: List of cycles, where each cycle is a list of prompt filenames.
292
+
293
+ Returns:
294
+ The dependencies string with circular dependency includes removed.
295
+ """
296
+ if not cycles:
297
+ return dependencies
298
+
299
+ # Extract module names from cycles (e.g., 'agentic_bug_python.prompt' -> 'agentic_bug')
300
+ problematic_modules: Set[str] = set()
301
+ for cycle in cycles:
302
+ for prompt_name in cycle:
303
+ # Extract module name from prompt filename using shared helper
304
+ module_name = _extract_module_name(prompt_name)
305
+ if module_name:
306
+ problematic_modules.add(module_name)
307
+
308
+ if not problematic_modules:
309
+ return dependencies
310
+
311
+ # Pattern to match include tags with _example.py files
312
+ # Matches: <wrapper><include>context/[subdirs/]module_example.py</include></wrapper>
313
+ # Using a simpler approach: find each include and check if it's problematic
314
+ result = dependencies
315
+ for module in problematic_modules:
316
+ # Remove includes for this module's example file
317
+ # Pattern: <wrapper><include>context/[subdirs/]module_example.py</include></wrapper>
318
+ pattern = rf'<[^>]+><include>context/(?:[^/]+/)*{re.escape(module)}_example\.py</include></[^>]+>\s*'
319
+ result = re.sub(pattern, '', result)
320
+
321
+ return result
322
+
323
+
111
324
  def auto_include(
112
325
  input_prompt: str,
113
326
  directory_path: str,
114
327
  csv_file: Optional[str] = None,
328
+ prompt_filename: Optional[str] = None,
115
329
  strength: float = DEFAULT_STRENGTH,
116
330
  temperature: float = 0.0,
117
331
  time: float = DEFAULT_TIME,
118
- verbose: bool = False
332
+ verbose: bool = False,
333
+ progress_callback: Optional[Callable[[int, int], None]] = None
119
334
  ) -> Tuple[str, str, float, str]:
120
335
  """
121
336
  Automatically find and insert proper dependencies into the prompt.
@@ -124,10 +339,14 @@ def auto_include(
124
339
  input_prompt (str): The prompt requiring includes
125
340
  directory_path (str): Directory path of dependencies
126
341
  csv_file (Optional[str]): Contents of existing CSV file
342
+ prompt_filename (Optional[str]): The prompt filename being processed,
343
+ used to filter out self-referential example files
127
344
  strength (float): Strength of LLM model (0-1)
128
345
  temperature (float): Temperature of LLM model (0-1)
129
346
  time (float): Time budget for LLM calls
130
347
  verbose (bool): Whether to print detailed information
348
+ progress_callback (Optional[Callable[[int, int], None]]): Callback for progress updates.
349
+ Called with (current, total) for each file processed.
131
350
 
132
351
  Returns:
133
352
  Tuple[str, str, float, str]: (dependencies, csv_output, total_cost, model_name)
@@ -152,7 +371,7 @@ def auto_include(
152
371
  console.print(Panel("Step 2: Running summarize_directory", style="blue"))
153
372
 
154
373
  csv_output, summary_cost, summary_model = _summarize(
155
- directory_path, csv_file, llm_kwargs
374
+ directory_path, csv_file, llm_kwargs, progress_callback
156
375
  )
157
376
 
158
377
  available_includes = _get_available_includes_from_csv(csv_output)
@@ -167,7 +386,28 @@ def auto_include(
167
386
  available_includes=available_includes,
168
387
  llm_kwargs=llm_kwargs,
169
388
  )
170
-
389
+
390
+ # Filter out self-referential includes (module's own example file)
391
+ module_name = _extract_module_name(prompt_filename)
392
+ dependencies = _filter_self_references(dependencies, module_name)
393
+
394
+ # Fix any malformed [File:] patterns from LLM output
395
+ dependencies = _fix_malformed_includes(dependencies)
396
+
397
+ # Detect and filter circular dependencies in prompt includes
398
+ if prompt_filename:
399
+ cycles = _detect_circular_dependencies(
400
+ current_prompt=prompt_filename,
401
+ new_dependencies=dependencies
402
+ )
403
+ if cycles:
404
+ dependencies = _filter_circular_dependencies(dependencies, cycles)
405
+ for cycle in cycles:
406
+ console.print(
407
+ f"[yellow]Warning: Filtered circular dependency: "
408
+ f"{' -> '.join(cycle)}[/yellow]"
409
+ )
410
+
171
411
  total_cost = summary_cost + llm_cost
172
412
  model_name = llm_model_name or summary_model
173
413