pdd-cli 0.0.90__py3-none-any.whl → 0.0.121__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 (151) hide show
  1. pdd/__init__.py +38 -6
  2. pdd/agentic_bug.py +323 -0
  3. pdd/agentic_bug_orchestrator.py +506 -0
  4. pdd/agentic_change.py +231 -0
  5. pdd/agentic_change_orchestrator.py +537 -0
  6. pdd/agentic_common.py +533 -770
  7. pdd/agentic_crash.py +2 -1
  8. pdd/agentic_e2e_fix.py +319 -0
  9. pdd/agentic_e2e_fix_orchestrator.py +582 -0
  10. pdd/agentic_fix.py +118 -3
  11. pdd/agentic_update.py +27 -9
  12. pdd/agentic_verify.py +3 -2
  13. pdd/architecture_sync.py +565 -0
  14. pdd/auth_service.py +210 -0
  15. pdd/auto_deps_main.py +63 -53
  16. pdd/auto_include.py +236 -3
  17. pdd/auto_update.py +125 -47
  18. pdd/bug_main.py +195 -23
  19. pdd/cmd_test_main.py +345 -197
  20. pdd/code_generator.py +4 -2
  21. pdd/code_generator_main.py +118 -32
  22. pdd/commands/__init__.py +6 -0
  23. pdd/commands/analysis.py +113 -48
  24. pdd/commands/auth.py +309 -0
  25. pdd/commands/connect.py +358 -0
  26. pdd/commands/fix.py +155 -114
  27. pdd/commands/generate.py +5 -0
  28. pdd/commands/maintenance.py +3 -2
  29. pdd/commands/misc.py +8 -0
  30. pdd/commands/modify.py +225 -163
  31. pdd/commands/sessions.py +284 -0
  32. pdd/commands/utility.py +12 -7
  33. pdd/construct_paths.py +334 -32
  34. pdd/context_generator_main.py +167 -170
  35. pdd/continue_generation.py +6 -3
  36. pdd/core/__init__.py +33 -0
  37. pdd/core/cli.py +44 -7
  38. pdd/core/cloud.py +237 -0
  39. pdd/core/dump.py +68 -20
  40. pdd/core/errors.py +4 -0
  41. pdd/core/remote_session.py +61 -0
  42. pdd/crash_main.py +219 -23
  43. pdd/data/llm_model.csv +4 -4
  44. pdd/docs/prompting_guide.md +864 -0
  45. pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
  46. pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
  47. pdd/fix_code_loop.py +208 -34
  48. pdd/fix_code_module_errors.py +6 -2
  49. pdd/fix_error_loop.py +291 -38
  50. pdd/fix_main.py +208 -6
  51. pdd/fix_verification_errors_loop.py +235 -26
  52. pdd/fix_verification_main.py +269 -83
  53. pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
  54. pdd/frontend/dist/assets/index-CUWd8al1.js +450 -0
  55. pdd/frontend/dist/index.html +376 -0
  56. pdd/frontend/dist/logo.svg +33 -0
  57. pdd/generate_output_paths.py +46 -5
  58. pdd/generate_test.py +212 -151
  59. pdd/get_comment.py +19 -44
  60. pdd/get_extension.py +8 -9
  61. pdd/get_jwt_token.py +309 -20
  62. pdd/get_language.py +8 -7
  63. pdd/get_run_command.py +7 -5
  64. pdd/insert_includes.py +2 -1
  65. pdd/llm_invoke.py +531 -97
  66. pdd/load_prompt_template.py +15 -34
  67. pdd/operation_log.py +342 -0
  68. pdd/path_resolution.py +140 -0
  69. pdd/postprocess.py +122 -97
  70. pdd/preprocess.py +68 -12
  71. pdd/preprocess_main.py +33 -1
  72. pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
  73. pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
  74. pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
  75. pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
  76. pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
  77. pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
  78. pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
  79. pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
  80. pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
  81. pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
  82. pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
  83. pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
  84. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +140 -0
  85. pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
  86. pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
  87. pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
  88. pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
  89. pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
  90. pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
  91. pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
  92. pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
  93. pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
  94. pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
  95. pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
  96. pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
  97. pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
  98. pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
  99. pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
  100. pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
  101. pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
  102. pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
  103. pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
  104. pdd/prompts/agentic_update_LLM.prompt +192 -338
  105. pdd/prompts/auto_include_LLM.prompt +22 -0
  106. pdd/prompts/change_LLM.prompt +3093 -1
  107. pdd/prompts/detect_change_LLM.prompt +571 -14
  108. pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
  109. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
  110. pdd/prompts/generate_test_LLM.prompt +19 -1
  111. pdd/prompts/generate_test_from_example_LLM.prompt +366 -0
  112. pdd/prompts/insert_includes_LLM.prompt +262 -252
  113. pdd/prompts/prompt_code_diff_LLM.prompt +123 -0
  114. pdd/prompts/prompt_diff_LLM.prompt +82 -0
  115. pdd/remote_session.py +876 -0
  116. pdd/server/__init__.py +52 -0
  117. pdd/server/app.py +335 -0
  118. pdd/server/click_executor.py +587 -0
  119. pdd/server/executor.py +338 -0
  120. pdd/server/jobs.py +661 -0
  121. pdd/server/models.py +241 -0
  122. pdd/server/routes/__init__.py +31 -0
  123. pdd/server/routes/architecture.py +451 -0
  124. pdd/server/routes/auth.py +364 -0
  125. pdd/server/routes/commands.py +929 -0
  126. pdd/server/routes/config.py +42 -0
  127. pdd/server/routes/files.py +603 -0
  128. pdd/server/routes/prompts.py +1347 -0
  129. pdd/server/routes/websocket.py +473 -0
  130. pdd/server/security.py +243 -0
  131. pdd/server/terminal_spawner.py +217 -0
  132. pdd/server/token_counter.py +222 -0
  133. pdd/summarize_directory.py +236 -237
  134. pdd/sync_animation.py +8 -4
  135. pdd/sync_determine_operation.py +329 -47
  136. pdd/sync_main.py +272 -28
  137. pdd/sync_orchestration.py +289 -211
  138. pdd/sync_order.py +304 -0
  139. pdd/template_expander.py +161 -0
  140. pdd/templates/architecture/architecture_json.prompt +41 -46
  141. pdd/trace.py +1 -1
  142. pdd/track_cost.py +0 -13
  143. pdd/unfinished_prompt.py +2 -1
  144. pdd/update_main.py +68 -26
  145. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/METADATA +15 -10
  146. pdd_cli-0.0.121.dist-info/RECORD +229 -0
  147. pdd_cli-0.0.90.dist-info/RECORD +0 -153
  148. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/WHEEL +0 -0
  149. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/entry_points.txt +0 -0
  150. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/licenses/LICENSE +0 -0
  151. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/top_level.txt +0 -0
pdd/sync_main.py CHANGED
@@ -1,3 +1,4 @@
1
+ import fnmatch
1
2
  import re
2
3
  import time
3
4
  from pathlib import Path
@@ -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
@@ -118,9 +332,10 @@ def sync_main(
118
332
  local = ctx.obj.get("local", False)
119
333
  context_override = ctx.obj.get("context", None)
120
334
 
121
- # Default values for max_attempts and budget when not specified via CLI or .pddrc
335
+ # Default values for max_attempts, budget, target_coverage when not specified via CLI or .pddrc
122
336
  DEFAULT_MAX_ATTEMPTS = 3
123
337
  DEFAULT_BUDGET = 20.0
338
+ DEFAULT_TARGET_COVERAGE = 90.0
124
339
 
125
340
  # 2. Validate inputs (basename only - budget/max_attempts validated after config resolution)
126
341
  _validate_basename(basename)
@@ -132,23 +347,47 @@ def sync_main(
132
347
  if max_attempts is not None and max_attempts < 0:
133
348
  raise click.BadParameter("Max attempts must be a non-negative integer.", param_hint="--max-attempts")
134
349
 
135
- # 3. Use construct_paths in 'discovery' mode to find the prompts directory.
136
- try:
137
- initial_config, _, _, _ = construct_paths(
138
- input_file_paths={},
139
- force=False,
140
- quiet=True,
141
- command="sync",
142
- command_options={"basename": basename},
143
- context_override=context_override,
144
- )
145
- prompts_dir = Path(initial_config.get("prompts_dir", "prompts"))
146
- except Exception as e:
147
- rprint(f"[bold red]Error initializing PDD paths:[/bold red] {e}")
148
- raise click.Abort()
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]")
367
+
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()
149
388
 
150
- # 4. Detect all languages for the given basename
151
- 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)
152
391
  if not languages:
153
392
  raise click.UsageError(
154
393
  f"No prompt files found for basename '{basename}' in directory '{prompts_dir}'.\n"
@@ -274,10 +513,15 @@ def sync_main(
274
513
  # Priority: CLI value > .pddrc value > hardcoded default
275
514
  final_strength = resolved_config.get("strength", strength)
276
515
  final_temp = resolved_config.get("temperature", temperature)
277
- final_target_coverage = resolved_config.get("target_coverage", target_coverage)
278
516
 
279
- # For max_attempts and budget: CLI > .pddrc > hardcoded default
517
+ # For target_coverage, max_attempts and budget: CLI > .pddrc > hardcoded default
280
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
+
281
525
  if max_attempts is not None:
282
526
  final_max_attempts = max_attempts
283
527
  else: