pdd-cli 0.0.90__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 (144) hide show
  1. pdd/__init__.py +38 -6
  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 +521 -786
  7. pdd/agentic_e2e_fix.py +319 -0
  8. pdd/agentic_e2e_fix_orchestrator.py +426 -0
  9. pdd/agentic_fix.py +118 -3
  10. pdd/agentic_update.py +25 -8
  11. pdd/architecture_sync.py +565 -0
  12. pdd/auth_service.py +210 -0
  13. pdd/auto_deps_main.py +63 -53
  14. pdd/auto_include.py +185 -3
  15. pdd/auto_update.py +125 -47
  16. pdd/bug_main.py +195 -23
  17. pdd/cmd_test_main.py +345 -197
  18. pdd/code_generator.py +4 -2
  19. pdd/code_generator_main.py +118 -32
  20. pdd/commands/__init__.py +6 -0
  21. pdd/commands/analysis.py +87 -29
  22. pdd/commands/auth.py +309 -0
  23. pdd/commands/connect.py +290 -0
  24. pdd/commands/fix.py +136 -113
  25. pdd/commands/maintenance.py +3 -2
  26. pdd/commands/misc.py +8 -0
  27. pdd/commands/modify.py +190 -164
  28. pdd/commands/sessions.py +284 -0
  29. pdd/construct_paths.py +334 -32
  30. pdd/context_generator_main.py +167 -170
  31. pdd/continue_generation.py +6 -3
  32. pdd/core/__init__.py +33 -0
  33. pdd/core/cli.py +27 -3
  34. pdd/core/cloud.py +237 -0
  35. pdd/core/errors.py +4 -0
  36. pdd/core/remote_session.py +61 -0
  37. pdd/crash_main.py +219 -23
  38. pdd/data/llm_model.csv +4 -4
  39. pdd/docs/prompting_guide.md +864 -0
  40. pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
  41. pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
  42. pdd/fix_code_loop.py +208 -34
  43. pdd/fix_code_module_errors.py +6 -2
  44. pdd/fix_error_loop.py +291 -38
  45. pdd/fix_main.py +204 -4
  46. pdd/fix_verification_errors_loop.py +235 -26
  47. pdd/fix_verification_main.py +269 -83
  48. pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
  49. pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
  50. pdd/frontend/dist/index.html +376 -0
  51. pdd/frontend/dist/logo.svg +33 -0
  52. pdd/generate_output_paths.py +46 -5
  53. pdd/generate_test.py +212 -151
  54. pdd/get_comment.py +19 -44
  55. pdd/get_extension.py +8 -9
  56. pdd/get_jwt_token.py +309 -20
  57. pdd/get_language.py +8 -7
  58. pdd/get_run_command.py +7 -5
  59. pdd/insert_includes.py +2 -1
  60. pdd/llm_invoke.py +459 -95
  61. pdd/load_prompt_template.py +15 -34
  62. pdd/path_resolution.py +140 -0
  63. pdd/postprocess.py +4 -1
  64. pdd/preprocess.py +68 -12
  65. pdd/preprocess_main.py +33 -1
  66. pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
  67. pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
  68. pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
  69. pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
  70. pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
  71. pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
  72. pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
  73. pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
  74. pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
  75. pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
  76. pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
  77. pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
  78. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
  79. pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
  80. pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
  81. pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
  82. pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
  83. pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
  84. pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
  85. pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
  86. pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
  87. pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
  88. pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
  89. pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
  90. pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
  91. pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
  92. pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
  93. pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
  94. pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
  95. pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
  96. pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
  97. pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
  98. pdd/prompts/agentic_update_LLM.prompt +192 -338
  99. pdd/prompts/auto_include_LLM.prompt +22 -0
  100. pdd/prompts/change_LLM.prompt +3093 -1
  101. pdd/prompts/detect_change_LLM.prompt +571 -14
  102. pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
  103. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
  104. pdd/prompts/generate_test_LLM.prompt +20 -1
  105. pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
  106. pdd/prompts/insert_includes_LLM.prompt +262 -252
  107. pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
  108. pdd/prompts/prompt_diff_LLM.prompt +82 -0
  109. pdd/remote_session.py +876 -0
  110. pdd/server/__init__.py +52 -0
  111. pdd/server/app.py +335 -0
  112. pdd/server/click_executor.py +587 -0
  113. pdd/server/executor.py +338 -0
  114. pdd/server/jobs.py +661 -0
  115. pdd/server/models.py +241 -0
  116. pdd/server/routes/__init__.py +31 -0
  117. pdd/server/routes/architecture.py +451 -0
  118. pdd/server/routes/auth.py +364 -0
  119. pdd/server/routes/commands.py +929 -0
  120. pdd/server/routes/config.py +42 -0
  121. pdd/server/routes/files.py +603 -0
  122. pdd/server/routes/prompts.py +1322 -0
  123. pdd/server/routes/websocket.py +473 -0
  124. pdd/server/security.py +243 -0
  125. pdd/server/terminal_spawner.py +209 -0
  126. pdd/server/token_counter.py +222 -0
  127. pdd/summarize_directory.py +236 -237
  128. pdd/sync_animation.py +8 -4
  129. pdd/sync_determine_operation.py +329 -47
  130. pdd/sync_main.py +272 -28
  131. pdd/sync_orchestration.py +136 -75
  132. pdd/template_expander.py +161 -0
  133. pdd/templates/architecture/architecture_json.prompt +41 -46
  134. pdd/trace.py +1 -1
  135. pdd/track_cost.py +0 -13
  136. pdd/unfinished_prompt.py +2 -1
  137. pdd/update_main.py +23 -5
  138. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +15 -10
  139. pdd_cli-0.0.118.dist-info/RECORD +227 -0
  140. pdd_cli-0.0.90.dist-info/RECORD +0 -153
  141. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
  142. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
  143. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +0 -0
  144. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,7 @@
1
1
  from pathlib import Path
2
- import os
3
2
  from typing import Optional
4
- import sys
5
3
  from rich import print
4
+ from pdd.path_resolution import get_default_resolver
6
5
 
7
6
  def print_formatted(message: str) -> None:
8
7
  """Print message with raw formatting tags for testing compatibility."""
@@ -23,40 +22,22 @@ def load_prompt_template(prompt_name: str) -> Optional[str]:
23
22
  print_formatted("[red]Unexpected error loading prompt template[/red]")
24
23
  return None
25
24
 
26
- # Step 1: Get project path from environment variable (preferred),
27
- # else fall back to auto-detect based on this module's location or CWD.
28
- project_path_env = os.getenv('PDD_PATH')
29
- candidate_paths = []
30
- if project_path_env:
31
- candidate_paths.append(Path(project_path_env))
25
+ resolver = get_default_resolver()
26
+ prompt_path = resolver.resolve_prompt_template(prompt_name)
32
27
 
33
- # Fallback 1: repository root inferred from this module (pdd/ => repo root)
34
- try:
35
- module_root = Path(__file__).resolve().parent # pdd/
36
- repo_root = module_root.parent # repo root
37
- candidate_paths.append(repo_root)
38
- except Exception:
39
- pass
40
-
41
- # Fallback 2: current working directory
42
- candidate_paths.append(Path.cwd())
43
-
44
- # Build candidate prompt paths to try in order
45
- prompt_candidates = []
46
- for cp in candidate_paths:
47
- # Check both <path>/prompts/ and <path>/pdd/prompts/
48
- # The latter handles installed package case where prompts are in pdd/prompts/
49
- prompt_candidates.append(cp / 'prompts' / f"{prompt_name}.prompt")
50
- prompt_candidates.append(cp / 'pdd' / 'prompts' / f"{prompt_name}.prompt")
28
+ if prompt_path is None:
29
+ candidate_roots = []
30
+ if resolver.pdd_path_env is not None:
31
+ candidate_roots.append(resolver.pdd_path_env)
32
+ if resolver.repo_root is not None:
33
+ candidate_roots.append(resolver.repo_root)
34
+ candidate_roots.append(resolver.cwd)
51
35
 
52
- # Step 2: Load and return the prompt template
53
- prompt_path: Optional[Path] = None
54
- for candidate in prompt_candidates:
55
- if candidate.exists():
56
- prompt_path = candidate
57
- break
36
+ prompt_candidates = []
37
+ for root in candidate_roots:
38
+ prompt_candidates.append(root / 'prompts' / f"{prompt_name}.prompt")
39
+ prompt_candidates.append(root / 'pdd' / 'prompts' / f"{prompt_name}.prompt")
58
40
 
59
- if prompt_path is None:
60
41
  tried = "\n".join(str(c) for c in prompt_candidates)
61
42
  print_formatted(
62
43
  f"[red]Prompt file not found in any candidate locations for '{prompt_name}'. Tried:\n{tried}[/red]"
@@ -82,4 +63,4 @@ if __name__ == "__main__":
82
63
  prompt = load_prompt_template("example_prompt")
83
64
  if prompt:
84
65
  print_formatted("[blue]Loaded prompt template:[/blue]")
85
- print_formatted(prompt)
66
+ print_formatted(prompt)
pdd/path_resolution.py ADDED
@@ -0,0 +1,140 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Literal, Optional
7
+
8
+ IncludeProfile = Literal["cwd_then_package_then_repo"]
9
+ PromptProfile = Literal["pdd_path_then_repo_then_cwd"]
10
+ DataProfile = Literal["pdd_path_only"]
11
+ ProjectRootProfile = Literal["pdd_path_then_marker_then_cwd"]
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class PathResolver:
16
+ cwd: Path
17
+ pdd_path_env: Optional[Path]
18
+ package_root: Path
19
+ repo_root: Optional[Path]
20
+
21
+ def resolve_include(self, rel: str, profile: IncludeProfile = "cwd_then_package_then_repo") -> Path:
22
+ if profile != "cwd_then_package_then_repo":
23
+ raise ValueError(f"Unsupported include profile: {profile}")
24
+
25
+ cwd_path = self.cwd / rel
26
+ if cwd_path.exists():
27
+ return cwd_path
28
+
29
+ pkg_path = self.package_root / rel
30
+ if pkg_path.exists():
31
+ return pkg_path
32
+
33
+ if self.repo_root is not None:
34
+ repo_path = self.repo_root / rel
35
+ if repo_path.exists():
36
+ return repo_path
37
+
38
+ return cwd_path
39
+
40
+ def resolve_prompt_template(
41
+ self,
42
+ name: str,
43
+ profile: PromptProfile = "pdd_path_then_repo_then_cwd",
44
+ ) -> Optional[Path]:
45
+ if profile != "pdd_path_then_repo_then_cwd":
46
+ raise ValueError(f"Unsupported prompt profile: {profile}")
47
+
48
+ roots = []
49
+ if self.pdd_path_env is not None:
50
+ roots.append(self.pdd_path_env)
51
+ if self.repo_root is not None:
52
+ roots.append(self.repo_root)
53
+ roots.append(self.cwd)
54
+
55
+ prompt_file = f"{name}.prompt"
56
+ for root in roots:
57
+ candidate = root / "prompts" / prompt_file
58
+ if candidate.exists():
59
+ return candidate
60
+ candidate = root / "pdd" / "prompts" / prompt_file
61
+ if candidate.exists():
62
+ return candidate
63
+
64
+ return None
65
+
66
+ def resolve_data_file(self, rel: str, profile: DataProfile = "pdd_path_only") -> Path:
67
+ if profile != "pdd_path_only":
68
+ raise ValueError(f"Unsupported data profile: {profile}")
69
+ if self.pdd_path_env is None:
70
+ raise ValueError("PDD_PATH environment variable is not set.")
71
+ return self.pdd_path_env / rel
72
+
73
+ def resolve_project_root(
74
+ self,
75
+ profile: ProjectRootProfile = "pdd_path_then_marker_then_cwd",
76
+ max_levels: int = 5,
77
+ ) -> Path:
78
+ if profile != "pdd_path_then_marker_then_cwd":
79
+ raise ValueError(f"Unsupported project root profile: {profile}")
80
+
81
+ if (
82
+ self.pdd_path_env is not None
83
+ and self.pdd_path_env.is_dir()
84
+ and not _is_within(self.pdd_path_env, self.package_root)
85
+ ):
86
+ return self.pdd_path_env
87
+
88
+ current = self.cwd
89
+ for _ in range(max_levels):
90
+ if _has_project_marker(current):
91
+ return current
92
+ parent = current.parent
93
+ if parent == current:
94
+ break
95
+ current = parent
96
+
97
+ return self.cwd
98
+
99
+
100
+ def get_default_resolver() -> PathResolver:
101
+ cwd = Path.cwd().resolve()
102
+
103
+ pdd_path_env = None
104
+ env_value = os.getenv("PDD_PATH")
105
+ if env_value:
106
+ pdd_path_env = Path(env_value).expanduser().resolve()
107
+
108
+ package_root = Path(__file__).resolve().parent
109
+ repo_root = package_root.parent
110
+
111
+ return PathResolver(
112
+ cwd=cwd,
113
+ pdd_path_env=pdd_path_env,
114
+ package_root=package_root,
115
+ repo_root=repo_root,
116
+ )
117
+
118
+
119
+ def _has_project_marker(path: Path) -> bool:
120
+ return (
121
+ (path / ".git").exists()
122
+ or (path / "pyproject.toml").exists()
123
+ or (path / "data").is_dir()
124
+ or (path / ".env").exists()
125
+ )
126
+
127
+
128
+ def _is_within(path: Path, parent: Path) -> bool:
129
+ try:
130
+ resolved_path = path.resolve()
131
+ resolved_parent = parent.resolve()
132
+ except Exception:
133
+ return False
134
+
135
+ if resolved_path == resolved_parent:
136
+ return True
137
+ parent_str = str(resolved_parent)
138
+ if not parent_str.endswith(os.sep):
139
+ parent_str = parent_str + os.sep
140
+ return str(resolved_path).startswith(parent_str)
pdd/postprocess.py CHANGED
@@ -7,6 +7,8 @@ from . import DEFAULT_TIME, DEFAULT_STRENGTH
7
7
 
8
8
  class ExtractedCode(BaseModel):
9
9
  """Pydantic model for the extracted code."""
10
+ focus: str = Field(default="", description="The focus of the generation")
11
+ explanation: str = Field(default="", description="Explanation of the extraction")
10
12
  extracted_code: str = Field(description="The extracted code from the LLM output")
11
13
 
12
14
  def postprocess_0(text: str) -> str:
@@ -93,7 +95,8 @@ def postprocess(
93
95
  temperature=temperature,
94
96
  time=time,
95
97
  verbose=verbose,
96
- output_pydantic=ExtractedCode
98
+ output_pydantic=ExtractedCode,
99
+ language=language,
97
100
  )
98
101
 
99
102
  if not response or 'result' not in response:
pdd/preprocess.py CHANGED
@@ -4,10 +4,12 @@ import base64
4
4
  import subprocess
5
5
  from typing import List, Optional, Tuple
6
6
  import traceback
7
+ from pathlib import Path
7
8
  from rich.console import Console
8
9
  from rich.panel import Panel
9
10
  from rich.markup import escape
10
11
  from rich.traceback import install
12
+ from pdd.path_resolution import get_default_resolver
11
13
 
12
14
  install()
13
15
  console = Console()
@@ -37,24 +39,51 @@ def _write_debug_report() -> None:
37
39
  console.print("[dim]Debug mode enabled but PDD_PREPROCESS_DEBUG_FILE not set (output shown in console only)[/dim]")
38
40
 
39
41
  def _extract_fence_spans(text: str) -> List[Tuple[int, int]]:
40
- """Return list of (start, end) spans for fenced code blocks ```...```.
42
+ """Return list of (start, end) spans for fenced code blocks (``` or ~~~).
41
43
 
42
44
  The spans are [start, end) indices in the original text.
43
45
  """
44
46
  spans: List[Tuple[int, int]] = []
45
47
  try:
46
- for m in re.finditer(r"```[\w\s]*\n[\s\S]*?```", text):
48
+ fence_re = re.compile(
49
+ r"(?m)^[ \t]*([`~]{3,})[^\n]*\n[\s\S]*?\n[ \t]*\1[ \t]*(?:\n|$)"
50
+ )
51
+ for m in fence_re.finditer(text):
47
52
  spans.append((m.start(), m.end()))
48
53
  except Exception:
49
54
  pass
50
55
  return spans
51
56
 
57
+
58
+ def _extract_inline_code_spans(text: str) -> List[Tuple[int, int]]:
59
+ """Return list of (start, end) spans for inline code (backticks)."""
60
+ spans: List[Tuple[int, int]] = []
61
+ try:
62
+ for m in re.finditer(r"(?<!`)(`+)([^\n]*?)\1", text):
63
+ spans.append((m.start(), m.end()))
64
+ except Exception:
65
+ pass
66
+ return spans
67
+
68
+
69
+ def _extract_code_spans(text: str) -> List[Tuple[int, int]]:
70
+ spans = _extract_fence_spans(text)
71
+ spans.extend(_extract_inline_code_spans(text))
72
+ return sorted(spans, key=lambda s: s[0])
73
+
52
74
  def _is_inside_any_span(idx: int, spans: List[Tuple[int, int]]) -> bool:
53
75
  for s, e in spans:
54
76
  if s <= idx < e:
55
77
  return True
56
78
  return False
57
79
 
80
+
81
+ def _intersects_any_span(start: int, end: int, spans: List[Tuple[int, int]]) -> bool:
82
+ for s, e in spans:
83
+ if start < e and end > s:
84
+ return True
85
+ return False
86
+
58
87
  def _scan_risky_placeholders(text: str) -> Tuple[List[Tuple[int, str]], List[Tuple[int, str]]]:
59
88
  """Scan for risky placeholders outside code fences.
60
89
 
@@ -119,8 +148,11 @@ def preprocess(prompt: str, recursive: bool = False, double_curly_brackets: bool
119
148
  return prompt
120
149
 
121
150
  def get_file_path(file_name: str) -> str:
122
- base_path = './'
123
- return os.path.join(base_path, file_name)
151
+ resolver = get_default_resolver()
152
+ resolved = resolver.resolve_include(file_name)
153
+ if not Path(file_name).is_absolute() and resolved == resolver.cwd / file_name:
154
+ return os.path.join("./", file_name)
155
+ return str(resolved)
124
156
 
125
157
  def process_backtick_includes(text: str, recursive: bool) -> str:
126
158
  # More specific pattern that doesn't match nested > characters
@@ -229,7 +261,12 @@ def process_include_tags(text: str, recursive: bool) -> str:
229
261
  current_text = text
230
262
  while prev_text != current_text:
231
263
  prev_text = current_text
232
- current_text = re.sub(pattern, replace_include, current_text, flags=re.DOTALL)
264
+ code_spans = _extract_code_spans(current_text)
265
+ def replace_include_with_spans(match):
266
+ if _intersects_any_span(match.start(), match.end(), code_spans):
267
+ return match.group(0)
268
+ return replace_include(match)
269
+ current_text = re.sub(pattern, replace_include_with_spans, current_text, flags=re.DOTALL)
233
270
  return current_text
234
271
 
235
272
  def process_pdd_tags(text: str) -> str:
@@ -262,7 +299,12 @@ def process_shell_tags(text: str, recursive: bool) -> str:
262
299
  console.print(f"[bold red]Error executing shell command:[/bold red] {str(e)}")
263
300
  _dbg(f"Shell execution exception: {e}")
264
301
  return f"[Shell execution error: {str(e)}]"
265
- return re.sub(pattern, replace_shell, text, flags=re.DOTALL)
302
+ code_spans = _extract_code_spans(text)
303
+ def replace_shell_with_spans(match):
304
+ if _intersects_any_span(match.start(), match.end(), code_spans):
305
+ return match.group(0)
306
+ return replace_shell(match)
307
+ return re.sub(pattern, replace_shell_with_spans, text, flags=re.DOTALL)
266
308
 
267
309
  def process_web_tags(text: str, recursive: bool) -> str:
268
310
  pattern = r'<web>(.*?)</web>'
@@ -275,7 +317,7 @@ def process_web_tags(text: str, recursive: bool) -> str:
275
317
  _dbg(f"Web tag URL: {url}")
276
318
  try:
277
319
  try:
278
- from firecrawl import FirecrawlApp
320
+ from firecrawl import Firecrawl
279
321
  except ImportError:
280
322
  _dbg("firecrawl import failed; package not installed")
281
323
  return f"[Error: firecrawl-py package not installed. Cannot scrape {url}]"
@@ -284,9 +326,13 @@ def process_web_tags(text: str, recursive: bool) -> str:
284
326
  console.print("[bold yellow]Warning:[/bold yellow] FIRECRAWL_API_KEY not found in environment")
285
327
  _dbg("FIRECRAWL_API_KEY not set")
286
328
  return f"[Error: FIRECRAWL_API_KEY not set. Cannot scrape {url}]"
287
- app = FirecrawlApp(api_key=api_key)
288
- response = app.scrape_url(url, formats=['markdown'])
289
- if hasattr(response, 'markdown'):
329
+ app = Firecrawl(api_key=api_key)
330
+ response = app.scrape(url, formats=['markdown'])
331
+ # Handle both dict response (new API) and object response (legacy)
332
+ if isinstance(response, dict) and 'markdown' in response:
333
+ _dbg(f"Web scrape returned markdown (len={len(response['markdown'])})")
334
+ return response['markdown']
335
+ elif hasattr(response, 'markdown'):
290
336
  _dbg(f"Web scrape returned markdown (len={len(response.markdown)})")
291
337
  return response.markdown
292
338
  else:
@@ -297,7 +343,12 @@ def process_web_tags(text: str, recursive: bool) -> str:
297
343
  console.print(f"[bold red]Error scraping web content:[/bold red] {str(e)}")
298
344
  _dbg(f"Web scraping exception: {e}")
299
345
  return f"[Web scraping error: {str(e)}]"
300
- return re.sub(pattern, replace_web, text, flags=re.DOTALL)
346
+ code_spans = _extract_code_spans(text)
347
+ def replace_web_with_spans(match):
348
+ if _intersects_any_span(match.start(), match.end(), code_spans):
349
+ return match.group(0)
350
+ return replace_web(match)
351
+ return re.sub(pattern, replace_web_with_spans, text, flags=re.DOTALL)
301
352
 
302
353
  def process_include_many_tags(text: str, recursive: bool) -> str:
303
354
  """Process <include-many> blocks whose inner content is a comma- or newline-separated
@@ -328,7 +379,12 @@ def process_include_many_tags(text: str, recursive: bool) -> str:
328
379
  _dbg(f"Error processing include-many {p}: {e}")
329
380
  contents.append(f"[Error processing include: {p}]")
330
381
  return "\n".join(contents)
331
- return re.sub(pattern, replace_many, text, flags=re.DOTALL)
382
+ code_spans = _extract_code_spans(text)
383
+ def replace_many_with_spans(match):
384
+ if _intersects_any_span(match.start(), match.end(), code_spans):
385
+ return match.group(0)
386
+ return replace_many(match)
387
+ return re.sub(pattern, replace_many_with_spans, text, flags=re.DOTALL)
332
388
 
333
389
  def double_curly(text: str, exclude_keys: Optional[List[str]] = None) -> str:
334
390
  if exclude_keys is None:
pdd/preprocess_main.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import csv
2
2
  import sys
3
+ from pathlib import Path
3
4
  from typing import Tuple, Optional
4
5
  import click
5
6
  from rich import print as rprint
@@ -8,8 +9,15 @@ from .config_resolution import resolve_effective_config
8
9
  from .construct_paths import construct_paths
9
10
  from .preprocess import preprocess
10
11
  from .xml_tagger import xml_tagger
12
+ from .architecture_sync import (
13
+ get_architecture_entry_for_prompt,
14
+ generate_tags_from_architecture,
15
+ has_pdd_tags,
16
+ )
17
+
18
+
11
19
  def preprocess_main(
12
- ctx: click.Context, prompt_file: str, output: Optional[str], xml: bool, recursive: bool, double: bool, exclude: list
20
+ ctx: click.Context, prompt_file: str, output: Optional[str], xml: bool, recursive: bool, double: bool, exclude: list, pdd_tags: bool = False
13
21
  ) -> Tuple[str, float, str]:
14
22
  """
15
23
  CLI wrapper for preprocessing prompts.
@@ -22,6 +30,7 @@ def preprocess_main(
22
30
  :param double: If True, curly brackets will be doubled.
23
31
  :param exclude: List of keys to exclude from curly bracket doubling.
24
32
  :return: Tuple containing the preprocessed prompt, total cost, and model name used.
33
+ :param pdd_tags: If True, inject PDD metadata tags from architecture.json.
25
34
  """
26
35
  try:
27
36
  # Construct file paths
@@ -39,6 +48,27 @@ def preprocess_main(
39
48
  # Load prompt file
40
49
  prompt = input_strings["prompt_file"]
41
50
 
51
+ # Inject PDD metadata tags from architecture.json if requested
52
+ pdd_tags_injected = False
53
+ if pdd_tags:
54
+ prompt_filename = Path(prompt_file).name
55
+ arch_entry = get_architecture_entry_for_prompt(prompt_filename)
56
+
57
+ if arch_entry:
58
+ if has_pdd_tags(prompt):
59
+ if not ctx.obj.get("quiet", False):
60
+ rprint(f"[yellow]Prompt already has PDD tags, skipping injection.[/yellow]")
61
+ else:
62
+ generated_tags = generate_tags_from_architecture(arch_entry)
63
+ if generated_tags:
64
+ prompt = generated_tags + '\n\n' + prompt
65
+ pdd_tags_injected = True
66
+ if not ctx.obj.get("quiet", False):
67
+ rprint(f"[green]Injected PDD tags from architecture.json[/green]")
68
+ else:
69
+ if not ctx.obj.get("quiet", False):
70
+ rprint(f"[yellow]No architecture entry found for '{prompt_filename}', skipping PDD tags.[/yellow]")
71
+
42
72
  if xml:
43
73
  # Use xml_tagger to add XML delimiters
44
74
  # Use centralized config resolution with proper priority: CLI > pddrc > defaults
@@ -67,6 +97,8 @@ def preprocess_main(
67
97
  # Provide user feedback
68
98
  if not ctx.obj.get("quiet", False):
69
99
  rprint("[bold green]Prompt preprocessing completed successfully.[/bold green]")
100
+ if pdd_tags_injected:
101
+ rprint("[bold]PDD metadata tags: injected from architecture.json[/bold]")
70
102
  if xml:
71
103
  rprint(f"[bold]XML Tagging used: {model_name}[/bold]")
72
104
  else:
@@ -0,0 +1,182 @@
1
+ % You are an expert software engineer investigating a bug report. Your task is to create a draft pull request with the failing tests and link it to the issue.
2
+
3
+ % Context
4
+
5
+ You are working on step 10 of 10 (final step) in an agentic bug investigation workflow. Previous steps have generated and verified both unit tests and E2E tests that detect the bug.
6
+
7
+ % Inputs
8
+
9
+ - GitHub Issue URL: {issue_url}
10
+ - Repository: {repo_owner}/{repo_name}
11
+ - Issue Number: {issue_number}
12
+
13
+ % Issue Content
14
+ <issue_content>
15
+ {issue_content}
16
+ </issue_content>
17
+
18
+ % Previous Steps Output
19
+ <step1_output>
20
+ {step1_output}
21
+ </step1_output>
22
+
23
+ <step2_output>
24
+ {step2_output}
25
+ </step2_output>
26
+
27
+ <step3_output>
28
+ {step3_output}
29
+ </step3_output>
30
+
31
+ <step4_output>
32
+ {step4_output}
33
+ </step4_output>
34
+
35
+ <step5_output>
36
+ {step5_output}
37
+ </step5_output>
38
+
39
+ <step6_output>
40
+ {step6_output}
41
+ </step6_output>
42
+
43
+ <step7_output>
44
+ {step7_output}
45
+ </step7_output>
46
+
47
+ <step8_output>
48
+ {step8_output}
49
+ </step8_output>
50
+
51
+ <step9_output>
52
+ {step9_output}
53
+ </step9_output>
54
+
55
+ % Worktree Information
56
+
57
+ You are operating in an isolated git worktree at: {worktree_path}
58
+ This worktree is already checked out to branch `fix/issue-{issue_number}`.
59
+ Do NOT create a new branch - just stage, commit, and push.
60
+
61
+ % Files to Stage
62
+
63
+ **IMPORTANT: Only stage these specific files:**
64
+ {files_to_stage}
65
+
66
+ % Your Task
67
+
68
+ 1. **Prepare the commit**
69
+ - You are already on branch `fix/issue-{issue_number}` in an isolated worktree
70
+ - **CRITICAL: Stage ONLY the test file(s) created in Steps 7 and 9**
71
+ - Get the exact file paths from:
72
+ - Step 7's `FILES_CREATED:` or `FILES_MODIFIED:` output (unit tests)
73
+ - Step 9's `E2E_FILES_CREATED:` or `E2E_FILES_MODIFIED:` output (E2E tests)
74
+ - Stage each file individually: `git add <exact_file_path>`
75
+ - **DO NOT use `git add .` or `git add -A`** - these will stage unrelated files and pollute the PR
76
+ - Verify only the intended files are staged: `git status --short` (should show only the test file(s))
77
+ - Commit with a descriptive message referencing the issue
78
+
79
+ 2. **Create the draft PR**
80
+ - Push the branch to origin
81
+ - Create a draft pull request using `gh pr create --draft`
82
+ - Link to the issue using "Fixes #{issue_number}" in the PR body
83
+
84
+ 3. **Post final summary**
85
+ - Comment on the issue with PR link and next steps for the fix
86
+
87
+ 4. **Include PDD fix command**
88
+ - Extract code file path from Step 5's `**Location:**` field (strip the `:line_number` suffix)
89
+ - Use test file path from Step 7's `FILES_CREATED:` or test file section
90
+ - Search repo for matching prompt file: `find . -name "*.prompt" -type f`
91
+ - Derive module name from code file (e.g., `pdd/foo.py` -> `foo`)
92
+ - Use verification program: `context/{{module_name}}_example.py`
93
+ - Use error log path: `fix-issue-{issue_number}.log` for the fix command output
94
+ - Include a ready-to-run `pdd fix` command in your GitHub comment
95
+ - If no prompt file or verification program exists, include a note that they must be created first
96
+
97
+ % PR Creation Command
98
+
99
+ ```bash
100
+ gh pr create --draft --title "Add failing tests for #{issue_number}" --body "$(cat <<'EOF'
101
+ ## Summary
102
+ Adds failing tests that detect the bug reported in #{issue_number}.
103
+
104
+ ## Test Files
105
+ - Unit test: `{{unit_test_file_path}}`
106
+ - E2E test: `{{e2e_test_file_path}}` (if applicable)
107
+
108
+ ## What This PR Contains
109
+ - Failing unit test that reproduces the reported bug
110
+ - Failing E2E test that verifies the bug at integration level (if applicable)
111
+ - Tests are verified to fail on current code and will pass once the bug is fixed
112
+
113
+ ## Root Cause
114
+ {{root_cause_summary}}
115
+
116
+ ## Next Steps
117
+ 1. [ ] Implement the fix at the identified location
118
+ 2. [ ] Verify the unit test passes
119
+ 3. [ ] Verify the E2E test passes
120
+ 4. [ ] Run full test suite
121
+ 5. [ ] Mark PR as ready for review
122
+
123
+ Fixes #{issue_number}
124
+
125
+ ---
126
+ *Generated by PDD agentic bug workflow*
127
+ EOF
128
+ )"
129
+ ```
130
+
131
+ % Output
132
+
133
+ After creating the PR, use `gh issue comment` to post your final report to issue #{issue_number}:
134
+
135
+ ```
136
+ gh issue comment {issue_number} --repo {repo_owner}/{repo_name} --body "..."
137
+ ```
138
+
139
+ Your comment should follow this format:
140
+
141
+ ```markdown
142
+ ## Step 10: Draft PR Created
143
+
144
+ ### Pull Request
145
+ **PR #{{pr_number}}:** [{{pr_title}}]({{pr_url}})
146
+
147
+ ### Branch
148
+ `fix/issue-{issue_number}`
149
+
150
+ ### What's Included
151
+ - Failing unit test at `{{unit_test_file_path}}`
152
+ - Failing E2E test at `{{e2e_test_file_path}}` (if applicable)
153
+ - Commits: {{commit_count}}
154
+
155
+ ### Next Steps for Maintainers
156
+ 1. Review the failing tests to understand the expected behavior
157
+ 2. Implement the fix at the identified location
158
+ 3. Verify both unit and E2E tests pass with your fix
159
+ 4. Run full test suite to check for regressions
160
+ 5. Mark the PR as ready for review
161
+
162
+ ### PDD Fix Command
163
+
164
+ To auto-fix this bug using PDD:
165
+
166
+ ```bash
167
+ cd {{worktree_path}}
168
+ pdd --force fix --loop --max-attempts 5 --verification-program context/{{module_name}}_example.py {{prompt_file}} {{code_file_path}} {{test_file_path}} fix-issue-{{issue_number}}.log
169
+ ```
170
+
171
+ ---
172
+ *Investigation complete. A draft PR with failing tests has been created and linked to this issue.*
173
+ ```
174
+
175
+ % Important
176
+
177
+ - Create a DRAFT PR (not ready for review) since it only contains the failing tests
178
+ - The PR should clearly state that a fix is still needed
179
+ - Use "Fixes #{issue_number}" to auto-link the PR to the issue
180
+ - Do NOT create a new branch - you are already on the correct branch in the worktree
181
+ - Include both unit test files (Step 7) and E2E test files (Step 9) if both exist
182
+ - Always post your findings as a GitHub comment before completing