pdd-cli 0.0.42__py3-none-any.whl → 0.0.90__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. pdd/__init__.py +4 -4
  2. pdd/agentic_common.py +863 -0
  3. pdd/agentic_crash.py +534 -0
  4. pdd/agentic_fix.py +1179 -0
  5. pdd/agentic_langtest.py +162 -0
  6. pdd/agentic_update.py +370 -0
  7. pdd/agentic_verify.py +183 -0
  8. pdd/auto_deps_main.py +15 -5
  9. pdd/auto_include.py +63 -5
  10. pdd/bug_main.py +3 -2
  11. pdd/bug_to_unit_test.py +2 -0
  12. pdd/change_main.py +11 -4
  13. pdd/cli.py +22 -1181
  14. pdd/cmd_test_main.py +80 -19
  15. pdd/code_generator.py +58 -18
  16. pdd/code_generator_main.py +672 -25
  17. pdd/commands/__init__.py +42 -0
  18. pdd/commands/analysis.py +248 -0
  19. pdd/commands/fix.py +140 -0
  20. pdd/commands/generate.py +257 -0
  21. pdd/commands/maintenance.py +174 -0
  22. pdd/commands/misc.py +79 -0
  23. pdd/commands/modify.py +230 -0
  24. pdd/commands/report.py +144 -0
  25. pdd/commands/templates.py +215 -0
  26. pdd/commands/utility.py +110 -0
  27. pdd/config_resolution.py +58 -0
  28. pdd/conflicts_main.py +8 -3
  29. pdd/construct_paths.py +281 -81
  30. pdd/context_generator.py +10 -2
  31. pdd/context_generator_main.py +113 -11
  32. pdd/continue_generation.py +47 -7
  33. pdd/core/__init__.py +0 -0
  34. pdd/core/cli.py +503 -0
  35. pdd/core/dump.py +554 -0
  36. pdd/core/errors.py +63 -0
  37. pdd/core/utils.py +90 -0
  38. pdd/crash_main.py +44 -11
  39. pdd/data/language_format.csv +71 -62
  40. pdd/data/llm_model.csv +20 -18
  41. pdd/detect_change_main.py +5 -4
  42. pdd/fix_code_loop.py +331 -77
  43. pdd/fix_error_loop.py +209 -60
  44. pdd/fix_errors_from_unit_tests.py +4 -3
  45. pdd/fix_main.py +75 -18
  46. pdd/fix_verification_errors.py +12 -100
  47. pdd/fix_verification_errors_loop.py +319 -272
  48. pdd/fix_verification_main.py +57 -17
  49. pdd/generate_output_paths.py +93 -10
  50. pdd/generate_test.py +16 -5
  51. pdd/get_jwt_token.py +48 -9
  52. pdd/get_run_command.py +73 -0
  53. pdd/get_test_command.py +68 -0
  54. pdd/git_update.py +70 -19
  55. pdd/increase_tests.py +7 -0
  56. pdd/incremental_code_generator.py +2 -2
  57. pdd/insert_includes.py +11 -3
  58. pdd/llm_invoke.py +1278 -110
  59. pdd/load_prompt_template.py +36 -10
  60. pdd/pdd_completion.fish +25 -2
  61. pdd/pdd_completion.sh +30 -4
  62. pdd/pdd_completion.zsh +79 -4
  63. pdd/postprocess.py +10 -3
  64. pdd/preprocess.py +228 -15
  65. pdd/preprocess_main.py +8 -5
  66. pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
  67. pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
  68. pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
  69. pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
  70. pdd/prompts/agentic_update_LLM.prompt +1071 -0
  71. pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
  72. pdd/prompts/auto_include_LLM.prompt +98 -101
  73. pdd/prompts/change_LLM.prompt +1 -3
  74. pdd/prompts/detect_change_LLM.prompt +562 -3
  75. pdd/prompts/example_generator_LLM.prompt +22 -1
  76. pdd/prompts/extract_code_LLM.prompt +5 -1
  77. pdd/prompts/extract_program_code_fix_LLM.prompt +14 -2
  78. pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
  79. pdd/prompts/extract_promptline_LLM.prompt +17 -11
  80. pdd/prompts/find_verification_errors_LLM.prompt +6 -0
  81. pdd/prompts/fix_code_module_errors_LLM.prompt +16 -4
  82. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +6 -41
  83. pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
  84. pdd/prompts/generate_test_LLM.prompt +21 -6
  85. pdd/prompts/increase_tests_LLM.prompt +1 -2
  86. pdd/prompts/insert_includes_LLM.prompt +1181 -6
  87. pdd/prompts/split_LLM.prompt +1 -62
  88. pdd/prompts/trace_LLM.prompt +25 -22
  89. pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
  90. pdd/prompts/update_prompt_LLM.prompt +22 -1
  91. pdd/prompts/xml_convertor_LLM.prompt +3246 -7
  92. pdd/pytest_output.py +188 -21
  93. pdd/python_env_detector.py +151 -0
  94. pdd/render_mermaid.py +236 -0
  95. pdd/setup_tool.py +648 -0
  96. pdd/simple_math.py +2 -0
  97. pdd/split_main.py +3 -2
  98. pdd/summarize_directory.py +56 -7
  99. pdd/sync_determine_operation.py +918 -186
  100. pdd/sync_main.py +82 -32
  101. pdd/sync_orchestration.py +1456 -453
  102. pdd/sync_tui.py +848 -0
  103. pdd/template_registry.py +264 -0
  104. pdd/templates/architecture/architecture_json.prompt +242 -0
  105. pdd/templates/generic/generate_prompt.prompt +174 -0
  106. pdd/trace.py +168 -12
  107. pdd/trace_main.py +4 -3
  108. pdd/track_cost.py +151 -61
  109. pdd/unfinished_prompt.py +49 -3
  110. pdd/update_main.py +549 -67
  111. pdd/update_model_costs.py +2 -2
  112. pdd/update_prompt.py +19 -4
  113. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/METADATA +20 -7
  114. pdd_cli-0.0.90.dist-info/RECORD +153 -0
  115. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/licenses/LICENSE +1 -1
  116. pdd_cli-0.0.42.dist-info/RECORD +0 -115
  117. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/WHEEL +0 -0
  118. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/entry_points.txt +0 -0
  119. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/top_level.txt +0 -0
pdd/preprocess.py CHANGED
@@ -1,7 +1,8 @@
1
1
  import os
2
2
  import re
3
+ import base64
3
4
  import subprocess
4
- from typing import List, Optional
5
+ from typing import List, Optional, Tuple
5
6
  import traceback
6
7
  from rich.console import Console
7
8
  from rich.panel import Panel
@@ -11,22 +12,110 @@ from rich.traceback import install
11
12
  install()
12
13
  console = Console()
13
14
 
15
+ # Debug/Instrumentation controls
16
+ _DEBUG_PREPROCESS = str(os.getenv("PDD_PREPROCESS_DEBUG", "")).lower() in ("1", "true", "yes", "on")
17
+ _DEBUG_OUTPUT_FILE = os.getenv("PDD_PREPROCESS_DEBUG_FILE") # Optional path to write a debug report
18
+ _DEBUG_EVENTS: List[str] = []
19
+
20
+ def _dbg(msg: str) -> None:
21
+ if _DEBUG_PREPROCESS:
22
+ console.print(f"[dim][PPD][preprocess][/dim] {escape(msg)}")
23
+ _DEBUG_EVENTS.append(msg)
24
+
25
+ def _write_debug_report() -> None:
26
+ if _DEBUG_PREPROCESS and _DEBUG_OUTPUT_FILE:
27
+ try:
28
+ with open(_DEBUG_OUTPUT_FILE, "w", encoding="utf-8") as fh:
29
+ fh.write("Preprocess Debug Report\n\n")
30
+ for line in _DEBUG_EVENTS:
31
+ fh.write(line + "\n")
32
+ console.print(f"[green]Debug report written to:[/green] {_DEBUG_OUTPUT_FILE}")
33
+ except Exception as e:
34
+ # Report the error so users know why the log file wasn't written
35
+ console.print(f"[yellow]Warning: Could not write debug report to {_DEBUG_OUTPUT_FILE}: {e}[/yellow]")
36
+ elif _DEBUG_PREPROCESS and not _DEBUG_OUTPUT_FILE:
37
+ console.print("[dim]Debug mode enabled but PDD_PREPROCESS_DEBUG_FILE not set (output shown in console only)[/dim]")
38
+
39
+ def _extract_fence_spans(text: str) -> List[Tuple[int, int]]:
40
+ """Return list of (start, end) spans for fenced code blocks ```...```.
41
+
42
+ The spans are [start, end) indices in the original text.
43
+ """
44
+ spans: List[Tuple[int, int]] = []
45
+ try:
46
+ for m in re.finditer(r"```[\w\s]*\n[\s\S]*?```", text):
47
+ spans.append((m.start(), m.end()))
48
+ except Exception:
49
+ pass
50
+ return spans
51
+
52
+ def _is_inside_any_span(idx: int, spans: List[Tuple[int, int]]) -> bool:
53
+ for s, e in spans:
54
+ if s <= idx < e:
55
+ return True
56
+ return False
57
+
58
+ def _scan_risky_placeholders(text: str) -> Tuple[List[Tuple[int, str]], List[Tuple[int, str]]]:
59
+ """Scan for risky placeholders outside code fences.
60
+
61
+ Returns two lists of (line_no, snippet):
62
+ - single_brace: matches like {name} not doubled and not part of {{...}}
63
+ - template_brace: `${...}` occurrences (which include single { ... })
64
+ """
65
+ single_brace: List[Tuple[int, str]] = []
66
+ template_brace: List[Tuple[int, str]] = []
67
+ try:
68
+ fence_spans = _extract_fence_spans(text)
69
+ # Single-brace variable placeholders (avoid matching {{ or }})
70
+ for m in re.finditer(r"(?<!\{)\{([A-Za-z_][A-Za-z0-9_]*)\}(?!\})", text):
71
+ if not _is_inside_any_span(m.start(), fence_spans):
72
+ line_no = text.count("\n", 0, m.start()) + 1
73
+ single_brace.append((line_no, m.group(0)))
74
+ # JavaScript template placeholders like ${...}
75
+ for m in re.finditer(r"\$\{[^\}]+\}", text):
76
+ if not _is_inside_any_span(m.start(), fence_spans):
77
+ line_no = text.count("\n", 0, m.start()) + 1
78
+ template_brace.append((line_no, m.group(0)))
79
+ except Exception:
80
+ pass
81
+ return single_brace, template_brace
82
+
14
83
  def preprocess(prompt: str, recursive: bool = False, double_curly_brackets: bool = True, exclude_keys: Optional[List[str]] = None) -> str:
15
84
  try:
16
85
  if not prompt:
17
86
  console.print("[bold red]Error:[/bold red] Empty prompt provided")
18
87
  return ""
88
+ _DEBUG_EVENTS.clear()
89
+ _dbg(f"Start preprocess(recursive={recursive}, double_curly={double_curly_brackets}, exclude_keys={exclude_keys})")
90
+ _dbg(f"Initial length: {len(prompt)} characters")
19
91
  console.print(Panel("Starting prompt preprocessing", style="bold blue"))
20
92
  prompt = process_backtick_includes(prompt, recursive)
93
+ _dbg("After backtick includes processed")
21
94
  prompt = process_xml_tags(prompt, recursive)
95
+ _dbg("After XML-like tags processed")
22
96
  if double_curly_brackets:
23
97
  prompt = double_curly(prompt, exclude_keys)
98
+ _dbg("After double_curly execution")
99
+ # Scan for risky placeholders remaining outside code fences
100
+ singles, templates = _scan_risky_placeholders(prompt)
101
+ if singles:
102
+ _dbg(f"WARNING: Found {len(singles)} single-brace placeholders outside code fences (examples):")
103
+ for ln, frag in singles[:5]:
104
+ _dbg(f" line {ln}: {frag}")
105
+ if templates:
106
+ _dbg(f"INFO: Found {len(templates)} template literals ${'{...'} outside code fences (examples):")
107
+ for ln, frag in templates[:5]:
108
+ _dbg(f" line {ln}: {frag}")
24
109
  # Don't trim whitespace that might be significant for the tests
25
110
  console.print(Panel("Preprocessing complete", style="bold green"))
111
+ _dbg(f"Final length: {len(prompt)} characters")
112
+ _write_debug_report()
26
113
  return prompt
27
114
  except Exception as e:
28
115
  console.print(f"[bold red]Error during preprocessing:[/bold red] {str(e)}")
29
116
  console.print(Panel(traceback.format_exc(), title="Error Details", style="red"))
117
+ _dbg(f"Exception: {str(e)}")
118
+ _write_debug_report()
30
119
  return prompt
31
120
 
32
121
  def get_file_path(file_name: str) -> str:
@@ -45,12 +134,17 @@ def process_backtick_includes(text: str, recursive: bool) -> str:
45
134
  content = file.read()
46
135
  if recursive:
47
136
  content = preprocess(content, recursive=True, double_curly_brackets=False)
137
+ _dbg(f"Included via backticks: {file_path} (len={len(content)})")
48
138
  return f"```{content}```"
49
139
  except FileNotFoundError:
50
140
  console.print(f"[bold red]Warning:[/bold red] File not found: {file_path}")
51
- return match.group(0)
141
+ _dbg(f"Missing backtick include: {file_path}")
142
+ # First pass (recursive=True): leave the tag so a later env expansion can resolve it
143
+ # Second pass (recursive=False): replace with a visible placeholder
144
+ return match.group(0) if recursive else f"```[File not found: {file_path}]```"
52
145
  except Exception as e:
53
146
  console.print(f"[bold red]Error processing include:[/bold red] {str(e)}")
147
+ _dbg(f"Error processing backtick include {file_path}: {e}")
54
148
  return f"```[Error processing include: {file_path}]```"
55
149
  prev_text = ""
56
150
  current_text = text
@@ -62,9 +156,9 @@ def process_backtick_includes(text: str, recursive: bool) -> str:
62
156
  def process_xml_tags(text: str, recursive: bool) -> str:
63
157
  text = process_pdd_tags(text)
64
158
  text = process_include_tags(text, recursive)
65
-
66
- text = process_shell_tags(text)
67
- text = process_web_tags(text)
159
+ text = process_include_many_tags(text, recursive)
160
+ text = process_shell_tags(text, recursive)
161
+ text = process_web_tags(text, recursive)
68
162
  return text
69
163
 
70
164
  def process_include_tags(text: str, recursive: bool) -> str:
@@ -73,17 +167,63 @@ def process_include_tags(text: str, recursive: bool) -> str:
73
167
  file_path = match.group(1).strip()
74
168
  try:
75
169
  full_path = get_file_path(file_path)
76
- console.print(f"Processing XML include: [cyan]{full_path}[/cyan]")
77
- with open(full_path, 'r', encoding='utf-8') as file:
78
- content = file.read()
79
- if recursive:
80
- content = preprocess(content, recursive=True, double_curly_brackets=False)
81
- return content
170
+ ext = os.path.splitext(file_path)[1].lower()
171
+ image_extensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.heic']
172
+
173
+ if ext in image_extensions:
174
+ console.print(f"Processing image include: [cyan]{full_path}[/cyan]")
175
+ from PIL import Image
176
+ import io
177
+ import pillow_heif
178
+
179
+ pillow_heif.register_heif_opener()
180
+
181
+ MAX_DIMENSION = 1024
182
+ with open(full_path, 'rb') as file:
183
+ img = Image.open(file)
184
+ img.load() # Force loading the image data before the file closes
185
+
186
+ if img.width > MAX_DIMENSION or img.height > MAX_DIMENSION:
187
+ img.thumbnail((MAX_DIMENSION, MAX_DIMENSION))
188
+ console.print(f"Image resized to {img.size}")
189
+
190
+ # Handle GIFs: convert to a static PNG of the first frame
191
+ if ext == '.gif':
192
+ img.seek(0)
193
+ img = img.convert("RGB")
194
+ img_format = 'PNG'
195
+ mime_type = 'image/png'
196
+ elif ext == '.heic':
197
+ img_format = 'JPEG'
198
+ mime_type = 'image/jpeg'
199
+ else:
200
+ img_format = 'JPEG' if ext in ['.jpg', '.jpeg'] else 'PNG'
201
+ mime_type = f'image/{img_format.lower()}'
202
+
203
+ # Save the (potentially resized and converted) image to an in-memory buffer
204
+ buffer = io.BytesIO()
205
+ img.save(buffer, format=img_format)
206
+ content = buffer.getvalue()
207
+
208
+ encoded_string = base64.b64encode(content).decode('utf-8')
209
+ return f"data:{mime_type};base64,{encoded_string}"
210
+ else:
211
+ console.print(f"Processing XML include: [cyan]{full_path}[/cyan]")
212
+ with open(full_path, 'r', encoding='utf-8') as file:
213
+ content = file.read()
214
+ if recursive:
215
+ content = preprocess(content, recursive=True, double_curly_brackets=False)
216
+ _dbg(f"Included via XML tag: {file_path} (len={len(content)})")
217
+ return content
82
218
  except FileNotFoundError:
83
219
  console.print(f"[bold red]Warning:[/bold red] File not found: {file_path}")
84
- return f"[File not found: {file_path}]"
220
+ _dbg(f"Missing XML include: {file_path}")
221
+ # First pass (recursive=True): leave the tag so a later env expansion can resolve it
222
+ # Second pass (recursive=False): replace with a visible placeholder
223
+ return match.group(0) if recursive else f"[File not found: {file_path}]"
85
224
  except Exception as e:
86
225
  console.print(f"[bold red]Error processing include:[/bold red] {str(e)}")
226
+ _dbg(f"Error processing XML include {file_path}: {e}")
87
227
  return f"[Error processing include: {file_path}]"
88
228
  prev_text = ""
89
229
  current_text = text
@@ -101,54 +241,101 @@ def process_pdd_tags(text: str) -> str:
101
241
  return "This is a test "
102
242
  return processed
103
243
 
104
- def process_shell_tags(text: str) -> str:
244
+ def process_shell_tags(text: str, recursive: bool) -> str:
105
245
  pattern = r'<shell>(.*?)</shell>'
106
246
  def replace_shell(match):
107
247
  command = match.group(1).strip()
248
+ if recursive:
249
+ # Defer execution until after env var expansion
250
+ return match.group(0)
108
251
  console.print(f"Executing shell command: [cyan]{escape(command)}[/cyan]")
252
+ _dbg(f"Shell tag command: {command}")
109
253
  try:
110
254
  result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
111
255
  return result.stdout
112
256
  except subprocess.CalledProcessError as e:
113
257
  error_msg = f"Command '{command}' returned non-zero exit status {e.returncode}."
114
258
  console.print(f"[bold red]Error:[/bold red] {error_msg}")
259
+ _dbg(f"Shell command error: {error_msg}")
115
260
  return f"Error: {error_msg}"
116
261
  except Exception as e:
117
262
  console.print(f"[bold red]Error executing shell command:[/bold red] {str(e)}")
263
+ _dbg(f"Shell execution exception: {e}")
118
264
  return f"[Shell execution error: {str(e)}]"
119
265
  return re.sub(pattern, replace_shell, text, flags=re.DOTALL)
120
266
 
121
- def process_web_tags(text: str) -> str:
267
+ def process_web_tags(text: str, recursive: bool) -> str:
122
268
  pattern = r'<web>(.*?)</web>'
123
269
  def replace_web(match):
124
270
  url = match.group(1).strip()
271
+ if recursive:
272
+ # Defer network operations until after env var expansion
273
+ return match.group(0)
125
274
  console.print(f"Scraping web content from: [cyan]{url}[/cyan]")
275
+ _dbg(f"Web tag URL: {url}")
126
276
  try:
127
277
  try:
128
278
  from firecrawl import FirecrawlApp
129
279
  except ImportError:
280
+ _dbg("firecrawl import failed; package not installed")
130
281
  return f"[Error: firecrawl-py package not installed. Cannot scrape {url}]"
131
282
  api_key = os.environ.get('FIRECRAWL_API_KEY')
132
283
  if not api_key:
133
284
  console.print("[bold yellow]Warning:[/bold yellow] FIRECRAWL_API_KEY not found in environment")
285
+ _dbg("FIRECRAWL_API_KEY not set")
134
286
  return f"[Error: FIRECRAWL_API_KEY not set. Cannot scrape {url}]"
135
287
  app = FirecrawlApp(api_key=api_key)
136
288
  response = app.scrape_url(url, formats=['markdown'])
137
289
  if hasattr(response, 'markdown'):
290
+ _dbg(f"Web scrape returned markdown (len={len(response.markdown)})")
138
291
  return response.markdown
139
292
  else:
140
293
  console.print(f"[bold yellow]Warning:[/bold yellow] No markdown content returned for {url}")
294
+ _dbg("Web scrape returned no markdown content")
141
295
  return f"[No content available for {url}]"
142
296
  except Exception as e:
143
297
  console.print(f"[bold red]Error scraping web content:[/bold red] {str(e)}")
298
+ _dbg(f"Web scraping exception: {e}")
144
299
  return f"[Web scraping error: {str(e)}]"
145
300
  return re.sub(pattern, replace_web, text, flags=re.DOTALL)
146
301
 
302
+ def process_include_many_tags(text: str, recursive: bool) -> str:
303
+ """Process <include-many> blocks whose inner content is a comma- or newline-separated
304
+ list of file paths (typically provided via variables after env expansion)."""
305
+ pattern = r'<include-many>(.*?)</include-many>'
306
+ def replace_many(match):
307
+ inner = match.group(1)
308
+ if recursive:
309
+ # Wait for env expansion to materialize the list
310
+ return match.group(0)
311
+ # Split by newlines or commas
312
+ raw_items = [s.strip() for part in inner.split('\n') for s in part.split(',')]
313
+ paths = [p for p in raw_items if p]
314
+ contents: list[str] = []
315
+ for p in paths:
316
+ try:
317
+ full_path = get_file_path(p)
318
+ console.print(f"Including (many): [cyan]{full_path}[/cyan]")
319
+ with open(full_path, 'r', encoding='utf-8') as fh:
320
+ contents.append(fh.read())
321
+ _dbg(f"Included (many): {p}")
322
+ except FileNotFoundError:
323
+ console.print(f"[bold red]Warning:[/bold red] File not found: {p}")
324
+ _dbg(f"Missing include-many: {p}")
325
+ contents.append(f"[File not found: {p}]")
326
+ except Exception as e:
327
+ console.print(f"[bold red]Error processing include-many:[/bold red] {str(e)}")
328
+ _dbg(f"Error processing include-many {p}: {e}")
329
+ contents.append(f"[Error processing include: {p}]")
330
+ return "\n".join(contents)
331
+ return re.sub(pattern, replace_many, text, flags=re.DOTALL)
332
+
147
333
  def double_curly(text: str, exclude_keys: Optional[List[str]] = None) -> str:
148
334
  if exclude_keys is None:
149
335
  exclude_keys = []
150
336
 
151
337
  console.print("Doubling curly brackets...")
338
+ _dbg("double_curly invoked")
152
339
 
153
340
  # Special case handling for specific test patterns
154
341
  if "Mix of {excluded{inner}} nesting" in text and "excluded" in exclude_keys:
@@ -172,6 +359,14 @@ def double_curly(text: str, exclude_keys: Optional[List[str]] = None) -> str:
172
359
  "2": {{"id": "2", "name": "Resource Two"}}
173
360
  }}"""
174
361
 
362
+ # Protect ${IDENT} placeholders so we can safely double braces, then restore
363
+ # them as ${{IDENT}} to avoid PromptTemplate interpreting {IDENT}.
364
+ protected_vars: List[str] = []
365
+ def _protect_var(m):
366
+ protected_vars.append(m.group(0))
367
+ return f"__PDD_VAR_{len(protected_vars)-1}__"
368
+ text = re.sub(r"\$\{[A-Za-z_][A-Za-z0-9_]*\}", _protect_var, text)
369
+
175
370
  # First, protect any existing double curly braces
176
371
  text = re.sub(r'\{\{([^{}]*)\}\}', r'__ALREADY_DOUBLED__\1__END_ALREADY__', text)
177
372
 
@@ -188,6 +383,24 @@ def double_curly(text: str, exclude_keys: Optional[List[str]] = None) -> str:
188
383
 
189
384
  # Restore already doubled brackets
190
385
  text = re.sub(r'__ALREADY_DOUBLED__(.*?)__END_ALREADY__', r'{{\1}}', text)
386
+
387
+ # Restore protected ${IDENT} placeholders as ${{IDENT}} so single braces
388
+ # don't leak into PromptTemplate formatting. This is safe for JS template
389
+ # literals and prevents missing-key errors in later formatting steps.
390
+ def _restore_var(m):
391
+ idx = int(m.group(1))
392
+ if 0 <= idx < len(protected_vars):
393
+ original = protected_vars[idx] # e.g., ${FOO}
394
+ try:
395
+ inner = re.match(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}", original)
396
+ if inner:
397
+ # Build as concatenation to avoid f-string brace escaping confusion
398
+ return "${{" + inner.group(1) + "}}" # -> ${{FOO}}
399
+ except Exception:
400
+ pass
401
+ return original
402
+ return m.group(0)
403
+ text = re.sub(r"__PDD_VAR_(\d+)__", _restore_var, text)
191
404
 
192
405
  # Special handling for code blocks
193
406
  code_block_pattern = r'```([\w\s]*)\n([\s\S]*?)```'
@@ -213,4 +426,4 @@ def double_curly(text: str, exclude_keys: Optional[List[str]] = None) -> str:
213
426
  # Process code blocks
214
427
  text = re.sub(code_block_pattern, process_code_block, text, flags=re.DOTALL)
215
428
 
216
- return text
429
+ return text
pdd/preprocess_main.py CHANGED
@@ -4,10 +4,10 @@ from typing import Tuple, Optional
4
4
  import click
5
5
  from rich import print as rprint
6
6
 
7
+ from .config_resolution import resolve_effective_config
7
8
  from .construct_paths import construct_paths
8
9
  from .preprocess import preprocess
9
10
  from .xml_tagger import xml_tagger
10
- from . import DEFAULT_TIME, DEFAULT_STRENGTH
11
11
  def preprocess_main(
12
12
  ctx: click.Context, prompt_file: str, output: Optional[str], xml: bool, recursive: bool, double: bool, exclude: list
13
13
  ) -> Tuple[str, float, str]:
@@ -33,6 +33,7 @@ def preprocess_main(
33
33
  quiet=ctx.obj.get("quiet", False),
34
34
  command="preprocess",
35
35
  command_options=command_options,
36
+ context_override=ctx.obj.get('context')
36
37
  )
37
38
 
38
39
  # Load prompt file
@@ -40,10 +41,12 @@ def preprocess_main(
40
41
 
41
42
  if xml:
42
43
  # Use xml_tagger to add XML delimiters
43
- strength = ctx.obj.get("strength", DEFAULT_STRENGTH)
44
- temperature = ctx.obj.get("temperature", 0.0)
44
+ # Use centralized config resolution with proper priority: CLI > pddrc > defaults
45
+ effective_config = resolve_effective_config(ctx, resolved_config)
46
+ strength = effective_config["strength"]
47
+ temperature = effective_config["temperature"]
48
+ time = effective_config["time"]
45
49
  verbose = ctx.obj.get("verbose", False)
46
- time = ctx.obj.get("time", DEFAULT_TIME)
47
50
  xml_tagged, total_cost, model_name = xml_tagger(
48
51
  prompt,
49
52
  strength,
@@ -76,4 +79,4 @@ def preprocess_main(
76
79
  except Exception as e:
77
80
  if not ctx.obj.get("quiet", False):
78
81
  rprint(f"[bold red]Error during preprocessing:[/bold red] {e}")
79
- sys.exit(1)
82
+ sys.exit(1)
@@ -0,0 +1,49 @@
1
+ You are fixing a crash in a PDD (Prompt-Driven Development) project.
2
+ You are running as FALLBACK after PDD's normal crash loop failed multiple times. This loop was only allowed to change the code and/or program file.
3
+ The error(s) is likely outside of these files.
4
+
5
+ ## PDD Principle
6
+ The PROMPT FILE is the source of truth. Code is a generated artifact.
7
+ The PROGRAM FILE calls the code and crashed. Both files may need fixes.
8
+
9
+ ## Files (you have full read/write access)
10
+ - Prompt file (THE SPEC): {prompt_path}
11
+ - Code file: {code_path}
12
+ - Program file: {program_path}
13
+ - Project root: {project_root}
14
+
15
+ ## Previous Fix Attempts
16
+ The following shows what PDD's normal crash loop already tried.
17
+ DO NOT repeat these approaches - try something different.
18
+
19
+ <previous_attempts>
20
+ {previous_attempts}
21
+ </previous_attempts>
22
+
23
+ ## Your Task
24
+ 1. Read the prompt file to understand the intended behavior
25
+ 2. Read the code and program files
26
+ 3. Analyze the crash traceback to identify the root cause
27
+ 4. Explore related files (imports, dependencies, conftest.py) if needed
28
+ 5. Determine what needs fixing:
29
+ - Code has a bug -> fix the code
30
+ - Program calls code incorrectly -> fix the program
31
+ - Both have issues -> fix both
32
+ - Issue requires changes to other files -> make those changes
33
+ 6. Make ALL necessary changes to stop the crash including other files if needed
34
+ 7. Run the program file to verify the fix
35
+ 8. Repeat steps 4-7 until the program output aligns with the prompt's intent
36
+ 9. Output a JSON string with the following fields:
37
+ - success: bool
38
+ - message: str
39
+ - cost: float
40
+ - model: str
41
+ - changed_files: list[str]
42
+
43
+ ## Critical Rules
44
+ - The prompt file defines what's correct - code should conform to it
45
+ - DO NOT repeat approaches from the fix history above
46
+ - You MAY modify BOTH the code file AND the program file
47
+ - IMPORTANT: Read actual source files before assuming what functions/classes exist
48
+ - Do NOT guess at imports or API names
49
+ - Explore the codebase to understand actual exports
@@ -0,0 +1,45 @@
1
+ You are fixing a test failure in a PDD (Prompt-Driven Development) project.
2
+ You are running as FALLBACK after PDD's normal fix loop failed multiple times. This loop was only allowed to change the code and/or test file.
3
+
4
+ ## PDD Principle
5
+ The PROMPT FILE is the source of truth. Code and tests are generated artifacts.
6
+ If tests expect behavior not defined in the prompt, the TESTS may be wrong.
7
+
8
+ ## Files (you have full read/write access)
9
+ - Prompt file (THE SPEC): {prompt_path}
10
+ - Code file: {code_path}
11
+ - Test file: {test_path}
12
+ - Example program file: {example_program_path}
13
+ - Project root: {project_root}
14
+
15
+ ## Previous Fix Attempts
16
+ The following shows what PDD's normal fix loop already tried.
17
+ DO NOT repeat these approaches - try something different.
18
+
19
+ {error_content}
20
+
21
+ ## Your Task
22
+ 1. Read the prompt file to understand the intended behavior
23
+ 2. Read the code and test files
24
+ 3. Run test file to get the error(s)
25
+ 4. Explore related files (helpers, fixtures, etc.) if needed
26
+ 5. Determine what needs fixing:
27
+ - Code doesn't match the prompt spec -> fix the code
28
+ - Tests don't match the prompt spec -> fix the tests
29
+ - Tests have implementation issues (mocking, isolation) -> fix test implementation
30
+ - Issue requires changes to other files -> make those changes
31
+ 5. Make ALL necessary changes to fix the tests
32
+ 6. Run the example program file to verify the fix didn't break the program
33
+ 7. Repeat steps 4-6 until the program output aligns with the prompt's intent
34
+ 8. Output a JSON string with the following fields:
35
+ - success: bool
36
+ - message: str
37
+ - cost: float
38
+ - model: str
39
+ - changed_files: list[str]
40
+
41
+ ## Critical Rules
42
+ - The prompt file defines what's correct - code and tests should conform to it
43
+ - DO NOT repeat approaches from the fix history above
44
+ - You may modify existing files or create new ones
45
+ - If the error involves mocking/test isolation, focus on the TEST implementation
@@ -0,0 +1,48 @@
1
+ % You are a strictly constrained code emitter. Your goal is to make {test_abs} run without errors. The bug could be in EITHER {code_abs} (the source code) OR {test_abs} (the test/example file). Analyze the error carefully to determine which file needs fixing. Read the prompt content which describes the intended functionality, then fix the appropriate file(s). Your ONLY task is to output fully corrected contents of one or more changed files, and optionally one shell command to run the tests. Wrap outputs between the provided BEGIN/END markers. No commentary or extra text.
2
+
3
+ % IMPORTANT: Analyze the error traceback carefully:
4
+ - If the error is in how the test/example USES the code (wrong exception caught, wrong API usage), fix {test_abs}
5
+ - If the error is in the code's IMPLEMENTATION (wrong behavior, missing functionality), fix {code_abs}
6
+ - You may need to fix BOTH files in some cases
7
+
8
+ % IMPORTANT: If you see ModuleNotFoundError or ImportError:
9
+ - For external packages: include "pip install <package> &&" before the test command in TESTCMD
10
+ - For local imports: fix the sys.path or import statement to correctly locate {code_abs}
11
+ - The code file is at: {code_abs} - ensure imports can find this path
12
+
13
+ <inputs>
14
+ <paths>
15
+ <begin_marker>{begin}</begin_marker>
16
+ <end_marker>{end}</end_marker>
17
+ <code_file>{code_abs}</code_file>
18
+ </paths>
19
+
20
+ <context>
21
+ <prompt_content>
22
+ {prompt_content}
23
+ </prompt_content>
24
+ <relevant_error>
25
+ {error_content}
26
+ </relevant_error>
27
+ </context>
28
+ </inputs>
29
+
30
+ % Follow these instructions:
31
+
32
+ 1) Output ALL files you changed that are needed to make tests pass (source files, tests, or small support files).
33
+ Use one block per file, with this exact wrapping:
34
+ <<<BEGIN_FILE:{code_abs}>>>
35
+ <FULL CORRECTED FILE CONTENT>
36
+ <<<END_FILE:{code_abs}>>>
37
+
38
+ If you also modify the test file:
39
+ <<<BEGIN_FILE:{test_abs}>>>
40
+ <FULL CORRECTED FILE CONTENT>
41
+ <<<END_FILE:{test_abs}>>>
42
+
43
+ 2) If you cannot run tests, ALSO print a single block containing the exact shell command to run tests such that it returns 0 on success:
44
+ <<<BEGIN_TESTCMD>>>
45
+ python {test_abs}
46
+ <<<END_TESTCMD>>>
47
+
48
+ 3) Print nothing else. No code fences, no comments, no prose.
@@ -0,0 +1,85 @@
1
+ % YOU ARE A DEBUGGING AGENT with full file system access.
2
+
3
+ % TASK: Fix the failing test at {test_abs}
4
+
5
+ % APPROACH:
6
+ 1. Read the error traceback carefully to understand what's failing
7
+ 2. Explore the relevant files to understand the codebase structure
8
+ 3. Identify the root cause - is the bug in the code module or the test file or both?
9
+ 4. Use your file editing tools to make minimal, targeted fixes
10
+ 5. After fixing, output the test command to verify your changes
11
+
12
+ % FILES YOU CAN READ AND EDIT:
13
+ <code_module>
14
+ {code_abs}
15
+ </code_module>
16
+ <test_file>
17
+ {test_abs}
18
+ </test_file>
19
+
20
+
21
+ % ORIGINAL SPECIFICATION:
22
+ <proompt_content>
23
+ {prompt_content}
24
+ </proompt_content>
25
+
26
+
27
+ % ERROR LOG:
28
+ <error_content>
29
+ {error_content}
30
+ </error_content>
31
+
32
+
33
+ % DEBUGGING GUIDELINES:
34
+ - Analyze the traceback to find WHERE the error occurs and WHY
35
+ - The bug could be in EITHER file - don't assume it's always in the code
36
+ - If the error is in how the test USES the code → fix the test
37
+ - If the error is in the code's IMPLEMENTATION → fix the code
38
+ - You may need to fix BOTH files in some cases
39
+
40
+ % COMMON ERROR TYPES AND FIXES:
41
+ - ImportError/ModuleNotFoundError for LOCAL modules: The import statement may be wrong.
42
+ FIX: Change the import to use the correct module name (look at what modules exist).
43
+ DO NOT create new modules to match a wrong import - fix the import instead!
44
+ - ImportError/ModuleNotFoundError for EXTERNAL packages (pip packages like toml, requests, humanize, etc.):
45
+ PREFERRED: Install the missing package using TESTCMD:
46
+ <<<BEGIN_TESTCMD>>>
47
+ pip install <package_name> && python -m pytest "{test_abs}" -q
48
+ <<<END_TESTCMD>>>
49
+
50
+ DO NOT rewrite the code to remove or replace the dependency unless the specification
51
+ explicitly says the dependency is optional. If the code uses a library, INSTALL IT.
52
+
53
+ ONLY use try/except fallback if the specification says the feature is optional:
54
+ ```python
55
+ try:
56
+ import toml
57
+ except ImportError:
58
+ toml = None # Only if spec says toml is optional
59
+ ```
60
+ - TypeError/AttributeError: Check function signatures and method names
61
+ - AssertionError: Check if the test expectation or the code logic is wrong
62
+ - ZeroDivisionError/ValueError: Add proper error handling
63
+ - SyntaxError (unterminated string literal / unexpected character):
64
+ This often means the file has garbage appended at the end (common LLM extraction bug).
65
+ FIX: Read the end of the file and look for JSON-like metadata patterns such as:
66
+ - Lines starting with `"explanation":`, `"focus":`, `"description":`
67
+ - Lines with only `}}` or `]`
68
+ - Code lines ending with `",` followed by JSON keys
69
+ SOLUTION: Delete all the garbage lines at the end of the file to restore valid Python.
70
+
71
+ % EDIT POLICY:
72
+ - Keep changes minimal and directly related to the failure
73
+ - Prefer fixing import statements over creating new files
74
+ - Prefer fixing implementation bugs over weakening tests
75
+ - You MAY create new files if truly needed (e.g., __init__.py for packages)
76
+
77
+ % AFTER FIXING, OUTPUT VERIFICATION COMMAND:
78
+ <<<BEGIN_TESTCMD>>>
79
+ python -m pytest "{test_abs}" -q
80
+ <<<END_TESTCMD>>>
81
+
82
+ % IMPORTANT:
83
+ - Use your file tools to directly read and modify the files
84
+ - Do NOT output the full file contents - just make targeted edits
85
+ - The test command will be run automatically to verify your fix worked