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/sync_main.py CHANGED
@@ -1,7 +1,8 @@
1
+ import fnmatch
1
2
  import re
2
3
  import time
3
4
  from pathlib import Path
4
- from typing import Any, Dict, List, Tuple
5
+ from typing import Any, Dict, List, Optional, Tuple
5
6
 
6
7
  import click
7
8
  from rich.console import Console
@@ -12,17 +13,26 @@ from rich import print as rprint
12
13
  # Relative imports from the pdd package
13
14
  from . import DEFAULT_STRENGTH, DEFAULT_TIME
14
15
  from .construct_paths import (
15
- _is_known_language,
16
+ _is_known_language,
16
17
  construct_paths,
17
18
  _find_pddrc_file,
19
+ _get_relative_basename,
18
20
  _load_pddrc_config,
19
21
  _detect_context,
20
- _get_context_config
22
+ _get_context_config,
23
+ get_extension
21
24
  )
22
25
  from .sync_orchestration import sync_orchestration
26
+ from .template_expander import expand_template
23
27
 
24
- # A simple regex for basename validation to prevent path traversal or other injection
25
- VALID_BASENAME_CHARS = re.compile(r"^[a-zA-Z0-9_-]+$")
28
+ # Regex for basename validation supporting subdirectory paths (e.g., 'core/cloud')
29
+ # Allows: alphanumeric, underscore, hyphen, and forward slash for subdirectory paths
30
+ # Structure inherently prevents:
31
+ # - Path traversal (..) - dot not in character class
32
+ # - Leading slash (/abs) - must start with [a-zA-Z0-9_-]+
33
+ # - Trailing slash (path/) - must end with [a-zA-Z0-9_-]+
34
+ # - Double slash (a//b) - requires characters between slashes
35
+ VALID_BASENAME_CHARS = re.compile(r"^[a-zA-Z0-9_-]+(/[a-zA-Z0-9_-]+)*$")
26
36
 
27
37
 
28
38
  def _validate_basename(basename: str) -> None:
@@ -32,27 +42,231 @@ def _validate_basename(basename: str) -> None:
32
42
  if not VALID_BASENAME_CHARS.match(basename):
33
43
  raise click.UsageError(
34
44
  f"Basename '{basename}' contains invalid characters. "
35
- "Only alphanumeric, underscore, and hyphen are allowed."
45
+ "Only alphanumeric, underscore, hyphen, and forward slash (for subdirectories) are allowed."
36
46
  )
37
47
 
38
48
 
49
+ def _get_extension_safe(language: str) -> str:
50
+ """Get file extension with fallback for when PDD_PATH is not set."""
51
+ try:
52
+ return get_extension(language)
53
+ except (ValueError, FileNotFoundError):
54
+ # Fallback to built-in mapping
55
+ builtin_ext_map = {
56
+ 'python': 'py', 'javascript': 'js', 'typescript': 'ts', 'java': 'java',
57
+ 'typescriptreact': 'tsx', 'javascriptreact': 'jsx',
58
+ 'cpp': 'cpp', 'c': 'c', 'go': 'go', 'ruby': 'rb', 'rust': 'rs',
59
+ }
60
+ return builtin_ext_map.get(language.lower(), '')
61
+
62
+
63
+ def _relative_basename_for_context(basename: str, context_config: Dict[str, Any]) -> str:
64
+ """Return basename relative to a context's most specific path or prompt prefix."""
65
+ matches = []
66
+
67
+ for path_pattern in context_config.get('paths', []):
68
+ pattern_base = path_pattern.rstrip('/**').rstrip('/*')
69
+ if fnmatch.fnmatch(basename, path_pattern) or \
70
+ basename.startswith(pattern_base + '/') or \
71
+ basename == pattern_base:
72
+ relative = _get_relative_basename(basename, path_pattern)
73
+ matches.append((len(pattern_base), relative))
74
+
75
+ defaults = context_config.get('defaults', {})
76
+ prompts_dir = defaults.get('prompts_dir', '')
77
+ if prompts_dir:
78
+ normalized = prompts_dir.rstrip('/')
79
+ prefix = normalized
80
+ if normalized == 'prompts':
81
+ prefix = ''
82
+ elif normalized.startswith('prompts/'):
83
+ prefix = normalized[len('prompts/'):]
84
+
85
+ if prefix and (basename == prefix or basename.startswith(prefix + '/')):
86
+ relative = basename[len(prefix) + 1 :] if basename != prefix else basename.split('/')[-1]
87
+ matches.append((len(prefix), relative))
88
+
89
+ if not matches:
90
+ return basename
91
+
92
+ matches.sort(key=lambda item: item[0], reverse=True)
93
+ return matches[0][1]
94
+
95
+
96
+ def _normalize_prompts_root(prompts_dir: Path) -> Path:
97
+ """
98
+ Resolve prompts_dir to an absolute path relative to the project root.
99
+
100
+ This function takes a potentially relative prompts_dir path (e.g., "prompts/backend")
101
+ and resolves it to an absolute path using the .pddrc location as the project root.
102
+
103
+ Note: This function previously stripped subdirectories after "prompts" which was
104
+ incorrect for context-specific prompts_dir values. Fixed in Issue #253.
105
+ """
106
+ prompts_root = Path(prompts_dir)
107
+ pddrc_path = _find_pddrc_file()
108
+ if pddrc_path and not prompts_root.is_absolute():
109
+ prompts_root = pddrc_path.parent / prompts_root
110
+
111
+ return prompts_root
112
+
113
+
114
+ def _find_prompt_in_contexts(basename: str) -> Optional[Tuple[str, Path, str]]:
115
+ """
116
+ Search for a prompt file across all contexts using outputs.prompt.path templates.
117
+
118
+ This enables finding prompts when the basename alone doesn't match context path patterns.
119
+ For example, 'credit_helpers' can find 'prompts/backend/utils/credit_helpers_python.prompt'
120
+ if the backend-utils context has outputs.prompt.path configured.
121
+
122
+ Args:
123
+ basename: The base name for the prompt file (e.g., 'credit_helpers')
124
+
125
+ Returns:
126
+ Tuple of (context_name, prompt_path, language) if found, None otherwise
127
+ """
128
+ pddrc_path = _find_pddrc_file()
129
+ if not pddrc_path:
130
+ return None
131
+
132
+ try:
133
+ config = _load_pddrc_config(pddrc_path)
134
+ except Exception:
135
+ return None
136
+
137
+ # Resolve paths relative to .pddrc location, not CWD
138
+ pddrc_parent = pddrc_path.parent
139
+
140
+ contexts = config.get('contexts', {})
141
+
142
+ # Common languages to try
143
+ languages_to_try = ['python', 'typescript', 'javascript', 'typescriptreact', 'go', 'rust', 'java']
144
+
145
+ for context_name, context_config in contexts.items():
146
+ if context_name == 'default':
147
+ continue
148
+
149
+ defaults = context_config.get('defaults', {})
150
+ outputs = defaults.get('outputs', {})
151
+ prompt_config = outputs.get('prompt', {})
152
+ prompt_template = prompt_config.get('path')
153
+
154
+ if not prompt_template:
155
+ continue
156
+
157
+ context_basename = _relative_basename_for_context(basename, context_config)
158
+ parts = context_basename.split('/') if context_basename else ['']
159
+ name_part = parts[-1]
160
+ category = '/'.join(parts[:-1]) if len(parts) > 1 else ''
161
+ dir_prefix = f"{category}/" if category else ''
162
+
163
+ # Try each language
164
+ for lang in languages_to_try:
165
+ ext = _get_extension_safe(lang)
166
+ template_context = {
167
+ 'name': name_part,
168
+ 'category': category,
169
+ 'dir_prefix': dir_prefix,
170
+ 'ext': ext,
171
+ 'language': lang,
172
+ }
173
+
174
+ expanded_path = expand_template(prompt_template, template_context)
175
+ # Resolve relative to .pddrc location, not CWD
176
+ prompt_path = pddrc_parent / expanded_path
177
+
178
+ if prompt_path.exists():
179
+ return (context_name, prompt_path, lang)
180
+
181
+ return None
182
+
183
+
184
+ def _detect_languages_with_context(basename: str, prompts_dir: Path, context_name: Optional[str] = None) -> List[str]:
185
+ """
186
+ Detects all available languages for a given basename, optionally using context config.
187
+
188
+ If context_name is provided and has outputs.prompt.path configured, uses template-based
189
+ discovery. Otherwise falls back to directory scanning.
190
+ """
191
+ if context_name:
192
+ pddrc_path = _find_pddrc_file()
193
+ if pddrc_path:
194
+ try:
195
+ config = _load_pddrc_config(pddrc_path)
196
+ # Resolve paths relative to .pddrc location, not CWD
197
+ pddrc_parent = pddrc_path.parent
198
+ contexts = config.get('contexts', {})
199
+ context_config = contexts.get(context_name, {})
200
+ defaults = context_config.get('defaults', {})
201
+ outputs = defaults.get('outputs', {})
202
+ prompt_config = outputs.get('prompt', {})
203
+ prompt_template = prompt_config.get('path')
204
+
205
+ if prompt_template:
206
+ context_basename = _relative_basename_for_context(basename, context_config)
207
+ parts = context_basename.split('/') if context_basename else ['']
208
+ name_part = parts[-1]
209
+ category = '/'.join(parts[:-1]) if len(parts) > 1 else ''
210
+ dir_prefix = f"{category}/" if category else ''
211
+
212
+ # Try all known languages
213
+ languages_to_try = ['python', 'typescript', 'javascript', 'typescriptreact', 'go', 'rust', 'java']
214
+ found_languages = []
215
+
216
+ for lang in languages_to_try:
217
+ ext = _get_extension_safe(lang)
218
+ template_context = {
219
+ 'name': name_part,
220
+ 'category': category,
221
+ 'dir_prefix': dir_prefix,
222
+ 'ext': ext,
223
+ 'language': lang,
224
+ }
225
+ expanded_path = expand_template(prompt_template, template_context)
226
+ # Resolve relative to .pddrc location, not CWD
227
+ if (pddrc_parent / expanded_path).exists():
228
+ found_languages.append(lang)
229
+
230
+ if found_languages:
231
+ # Return with Python first if present
232
+ if 'python' in found_languages:
233
+ other = sorted([l for l in found_languages if l != 'python'])
234
+ return ['python'] + other
235
+ return sorted(found_languages)
236
+ except Exception:
237
+ pass
238
+
239
+ # Fallback to original directory scanning
240
+ return _detect_languages(basename, prompts_dir)
241
+
242
+
39
243
  def _detect_languages(basename: str, prompts_dir: Path) -> List[str]:
40
244
  """
41
245
  Detects all available languages for a given basename by finding
42
246
  matching prompt files in the prompts directory.
43
247
  Excludes runtime languages (LLM) as they cannot form valid development units.
248
+
249
+ Supports subdirectory basenames like 'core/cloud':
250
+ - For basename 'core/cloud', searches in prompts/core/ for cloud_*.prompt files
251
+ - The stem comparison only uses the filename part ('cloud'), not the path ('core/cloud')
44
252
  """
45
253
  development_languages = []
46
254
  if not prompts_dir.is_dir():
47
255
  return []
48
256
 
257
+ # For subdirectory basenames, extract just the name part for stem comparison
258
+ if '/' in basename:
259
+ name_part = basename.rsplit('/', 1)[1] # 'cloud' from 'core/cloud'
260
+ else:
261
+ name_part = basename
262
+
49
263
  pattern = f"{basename}_*.prompt"
50
264
  for prompt_file in prompts_dir.glob(pattern):
51
- # stem is 'basename_language'
265
+ # stem is the filename without extension (e.g., 'cloud_python')
52
266
  stem = prompt_file.stem
53
- # Ensure the file starts with the exact basename followed by an underscore
54
- if stem.startswith(f"{basename}_"):
55
- potential_language = stem[len(basename) + 1 :]
267
+ # Ensure the file starts with the exact name part followed by an underscore
268
+ if stem.startswith(f"{name_part}_"):
269
+ potential_language = stem[len(name_part) + 1 :]
56
270
  try:
57
271
  if _is_known_language(potential_language):
58
272
  # Exclude runtime languages (LLM) as they cannot form valid development units
@@ -79,12 +293,12 @@ def _detect_languages(basename: str, prompts_dir: Path) -> List[str]:
79
293
  def sync_main(
80
294
  ctx: click.Context,
81
295
  basename: str,
82
- max_attempts: int,
83
- budget: float,
296
+ max_attempts: Optional[int],
297
+ budget: Optional[float],
84
298
  skip_verify: bool,
85
299
  skip_tests: bool,
86
300
  target_coverage: float,
87
- log: bool,
301
+ dry_run: bool,
88
302
  ) -> Tuple[Dict[str, Any], float, str]:
89
303
  """
90
304
  CLI wrapper for the sync command. Handles parameter validation, path construction,
@@ -93,12 +307,12 @@ def sync_main(
93
307
  Args:
94
308
  ctx: The Click context object.
95
309
  basename: The base name for the prompt file.
96
- max_attempts: Maximum number of fix attempts.
97
- budget: Maximum total cost for the sync process.
310
+ max_attempts: Maximum number of fix attempts. If None, uses .pddrc value or default (3).
311
+ budget: Maximum total cost for the sync process. If None, uses .pddrc value or default (20.0).
98
312
  skip_verify: Skip the functional verification step.
99
313
  skip_tests: Skip unit test generation and fixing.
100
314
  target_coverage: Desired code coverage percentage.
101
- log: If True, display sync logs instead of running the sync.
315
+ dry_run: If True, analyze sync state without executing operations.
102
316
 
103
317
  Returns:
104
318
  A tuple containing the results dictionary, total cost, and primary model name.
@@ -118,43 +332,72 @@ def sync_main(
118
332
  local = ctx.obj.get("local", False)
119
333
  context_override = ctx.obj.get("context", None)
120
334
 
121
- # 2. Validate inputs
335
+ # Default values for max_attempts, budget, target_coverage when not specified via CLI or .pddrc
336
+ DEFAULT_MAX_ATTEMPTS = 3
337
+ DEFAULT_BUDGET = 20.0
338
+ DEFAULT_TARGET_COVERAGE = 90.0
339
+
340
+ # 2. Validate inputs (basename only - budget/max_attempts validated after config resolution)
122
341
  _validate_basename(basename)
123
- if budget <= 0:
124
- raise click.BadParameter("Budget must be a positive number.", param_hint="--budget")
125
- if max_attempts <= 0:
126
- raise click.BadParameter("Max attempts must be a positive integer.", param_hint="--max-attempts")
127
342
 
128
- if not quiet and budget < 1.0:
129
- console.log(f"[yellow]Warning:[/] Budget of ${budget:.2f} is low. Complex operations may exceed this limit.")
343
+ # Validate CLI-specified values if provided (not None)
344
+ # Note: max_attempts=0 is valid (skips LLM loop, goes straight to agentic mode)
345
+ if budget is not None and budget <= 0:
346
+ raise click.BadParameter("Budget must be a positive number.", param_hint="--budget")
347
+ if max_attempts is not None and max_attempts < 0:
348
+ raise click.BadParameter("Max attempts must be a non-negative integer.", param_hint="--max-attempts")
349
+
350
+ # 3. Try template-based prompt discovery first (uses outputs.prompt.path from .pddrc)
351
+ template_result = _find_prompt_in_contexts(basename)
352
+ discovered_context = None
353
+
354
+ if template_result:
355
+ discovered_context, discovered_prompt_path, first_lang = template_result
356
+ prompts_dir_raw = discovered_prompt_path.parent
357
+ pddrc_path = _find_pddrc_file()
358
+ if pddrc_path and not prompts_dir_raw.is_absolute():
359
+ prompts_dir = pddrc_path.parent / prompts_dir_raw
360
+ else:
361
+ prompts_dir = prompts_dir_raw
362
+ # Use context override if not already set
363
+ if not context_override:
364
+ context_override = discovered_context
365
+ if not quiet:
366
+ rprint(f"[dim]Found prompt via template in context: {discovered_context}[/dim]")
130
367
 
131
- # 3. Use construct_paths in 'discovery' mode to find the prompts directory.
132
- try:
133
- initial_config, _, _, _ = construct_paths(
134
- input_file_paths={},
135
- force=False,
136
- quiet=True,
137
- command="sync",
138
- command_options={"basename": basename},
139
- context_override=context_override,
140
- )
141
- prompts_dir = Path(initial_config.get("prompts_dir", "prompts"))
142
- except Exception as e:
143
- rprint(f"[bold red]Error initializing PDD paths:[/bold red] {e}")
144
- raise click.Abort()
368
+ # 4. Fallback: Use construct_paths in 'discovery' mode to find the prompts directory.
369
+ if not template_result:
370
+ try:
371
+ initial_config, _, _, _ = construct_paths(
372
+ input_file_paths={},
373
+ force=False,
374
+ quiet=True,
375
+ command="sync",
376
+ command_options={"basename": basename},
377
+ context_override=context_override,
378
+ )
379
+ prompts_dir_raw = initial_config.get("prompts_dir", "prompts")
380
+ pddrc_path = _find_pddrc_file()
381
+ if pddrc_path and not Path(prompts_dir_raw).is_absolute():
382
+ prompts_dir = pddrc_path.parent / prompts_dir_raw
383
+ else:
384
+ prompts_dir = Path(prompts_dir_raw)
385
+ except Exception as e:
386
+ rprint(f"[bold red]Error initializing PDD paths:[/bold red] {e}")
387
+ raise click.Abort()
145
388
 
146
- # 4. Detect all languages for the given basename
147
- languages = _detect_languages(basename, prompts_dir)
389
+ # 5. Detect all languages for the given basename
390
+ languages = _detect_languages_with_context(basename, prompts_dir, context_name=discovered_context)
148
391
  if not languages:
149
392
  raise click.UsageError(
150
393
  f"No prompt files found for basename '{basename}' in directory '{prompts_dir}'.\n"
151
394
  f"Expected files with format: '{basename}_<language>.prompt'"
152
395
  )
153
396
 
154
- # 5. Handle --log mode separately
155
- if log:
397
+ # 5. Handle --dry-run mode separately
398
+ if dry_run:
156
399
  if not quiet:
157
- rprint(Panel(f"Displaying sync logs for [bold cyan]{basename}[/bold cyan]", title="PDD Sync Log", expand=False))
400
+ rprint(Panel(f"Displaying sync analysis for [bold cyan]{basename}[/bold cyan]", title="PDD Sync Dry Run", expand=False))
158
401
 
159
402
  for lang in languages:
160
403
  if not quiet:
@@ -189,19 +432,27 @@ def sync_main(
189
432
  code_dir=str(code_dir),
190
433
  examples_dir=str(examples_dir),
191
434
  tests_dir=str(tests_dir),
192
- log=True,
435
+ dry_run=True,
193
436
  verbose=verbose,
194
437
  quiet=quiet,
438
+ context_override=context_override,
195
439
  )
196
440
  return {}, 0.0, ""
197
441
 
198
442
  # 6. Main Sync Workflow
443
+ # Determine display values for summary panel (use CLI values or defaults for display)
444
+ display_budget = budget if budget is not None else DEFAULT_BUDGET
445
+ display_max_attempts = max_attempts if max_attempts is not None else DEFAULT_MAX_ATTEMPTS
446
+
447
+ if not quiet and display_budget < 1.0:
448
+ console.log(f"[yellow]Warning:[/] Budget of ${display_budget:.2f} is low. Complex operations may exceed this limit.")
449
+
199
450
  if not quiet:
200
451
  summary_panel = Panel(
201
452
  f"Basename: [bold cyan]{basename}[/bold cyan]\n"
202
453
  f"Languages: [bold green]{', '.join(languages)}[/bold green]\n"
203
- f"Budget: [bold yellow]${budget:.2f}[/bold yellow]\n"
204
- f"Max Attempts: [bold blue]{max_attempts}[/bold blue]",
454
+ f"Budget: [bold yellow]${display_budget:.2f}[/bold yellow]\n"
455
+ f"Max Attempts: [bold blue]{display_max_attempts}[/bold blue]",
205
456
  title="PDD Sync Starting",
206
457
  expand=False,
207
458
  )
@@ -211,13 +462,15 @@ def sync_main(
211
462
  total_cost = 0.0
212
463
  primary_model = ""
213
464
  overall_success = True
214
- remaining_budget = budget
465
+ # remaining_budget will be set from resolved config on first language iteration
466
+ remaining_budget: Optional[float] = None
215
467
 
216
468
  for lang in languages:
217
469
  if not quiet:
218
470
  rprint(f"\n[bold]🚀 Syncing for language: [green]{lang}[/green]...[/bold]")
219
471
 
220
- if remaining_budget <= 0:
472
+ # Check budget exhaustion (after first iteration when remaining_budget is set)
473
+ if remaining_budget is not None and remaining_budget <= 0:
221
474
  if not quiet:
222
475
  rprint(f"[yellow]Budget exhausted. Skipping sync for '{lang}'.[/yellow]")
223
476
  overall_success = False
@@ -231,17 +484,25 @@ def sync_main(
231
484
  command_options = {
232
485
  "basename": basename,
233
486
  "language": lang,
234
- "max_attempts": max_attempts,
235
- "budget": budget,
236
487
  "target_coverage": target_coverage,
237
- "strength": strength,
238
- "temperature": temperature,
239
488
  "time": time_param,
240
489
  }
241
-
490
+ # Only pass values if explicitly set by user (not CLI defaults)
491
+ # This allows .pddrc values to take precedence when user doesn't pass CLI flags
492
+ if max_attempts is not None:
493
+ command_options["max_attempts"] = max_attempts
494
+ if budget is not None:
495
+ command_options["budget"] = budget
496
+ if strength != DEFAULT_STRENGTH:
497
+ command_options["strength"] = strength
498
+ if temperature != 0.0: # 0.0 is the CLI default for temperature
499
+ command_options["temperature"] = temperature
500
+
501
+ # Use force=True for path discovery - actual file writes happen in sync_orchestration
502
+ # which will handle confirmations via the TUI's confirm_callback
242
503
  resolved_config, _, _, resolved_language = construct_paths(
243
504
  input_file_paths={"prompt_file": str(prompt_file_path)},
244
- force=force,
505
+ force=True, # Always force during path discovery
245
506
  quiet=True,
246
507
  command="sync",
247
508
  command_options=command_options,
@@ -249,11 +510,43 @@ def sync_main(
249
510
  )
250
511
 
251
512
  # Extract all parameters directly from the resolved configuration
513
+ # Priority: CLI value > .pddrc value > hardcoded default
252
514
  final_strength = resolved_config.get("strength", strength)
253
515
  final_temp = resolved_config.get("temperature", temperature)
254
- final_max_attempts = resolved_config.get("max_attempts", max_attempts)
255
- final_target_coverage = resolved_config.get("target_coverage", target_coverage)
256
-
516
+
517
+ # For target_coverage, max_attempts and budget: CLI > .pddrc > hardcoded default
518
+ # If CLI value is provided (not None), use it. Otherwise, use .pddrc or default.
519
+ # Issue #194: target_coverage was not being handled consistently with the others
520
+ if target_coverage is not None:
521
+ final_target_coverage = target_coverage
522
+ else:
523
+ final_target_coverage = resolved_config.get("target_coverage") or DEFAULT_TARGET_COVERAGE
524
+
525
+ if max_attempts is not None:
526
+ final_max_attempts = max_attempts
527
+ else:
528
+ final_max_attempts = resolved_config.get("max_attempts") or DEFAULT_MAX_ATTEMPTS
529
+
530
+ if budget is not None:
531
+ final_budget = budget
532
+ else:
533
+ final_budget = resolved_config.get("budget") or DEFAULT_BUDGET
534
+
535
+ # Validate the resolved values
536
+ # Note: max_attempts=0 is valid (skips LLM loop, goes straight to agentic mode)
537
+ if final_budget <= 0:
538
+ raise click.BadParameter("Budget must be a positive number.", param_hint="--budget")
539
+ if final_max_attempts < 0:
540
+ raise click.BadParameter("Max attempts must be a non-negative integer.", param_hint="--max-attempts")
541
+
542
+ # Initialize remaining_budget from first resolved config if not set yet
543
+ if remaining_budget is None:
544
+ remaining_budget = final_budget
545
+
546
+ # Update ctx.obj with resolved values so sub-commands inherit them
547
+ ctx.obj["strength"] = final_strength
548
+ ctx.obj["temperature"] = final_temp
549
+
257
550
  code_dir = resolved_config.get("code_dir", "src")
258
551
  tests_dir = resolved_config.get("tests_dir", "tests")
259
552
  examples_dir = resolved_config.get("examples_dir", "examples")
@@ -280,6 +573,7 @@ def sync_main(
280
573
  review_examples=review_examples,
281
574
  local=local,
282
575
  context_config=resolved_config,
576
+ context_override=context_override,
283
577
  )
284
578
 
285
579
  lang_cost = sync_result.get("total_cost", 0.0)
@@ -330,4 +624,4 @@ def sync_main(
330
624
  aggregated_results["total_cost"] = total_cost
331
625
  aggregated_results["primary_model"] = primary_model
332
626
 
333
- return aggregated_results, total_cost, primary_model
627
+ return aggregated_results, total_cost, primary_model