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.
- pdd/__init__.py +38 -6
- pdd/agentic_bug.py +323 -0
- pdd/agentic_bug_orchestrator.py +497 -0
- pdd/agentic_change.py +231 -0
- pdd/agentic_change_orchestrator.py +526 -0
- pdd/agentic_common.py +521 -786
- pdd/agentic_e2e_fix.py +319 -0
- pdd/agentic_e2e_fix_orchestrator.py +426 -0
- pdd/agentic_fix.py +118 -3
- pdd/agentic_update.py +25 -8
- pdd/architecture_sync.py +565 -0
- pdd/auth_service.py +210 -0
- pdd/auto_deps_main.py +63 -53
- pdd/auto_include.py +185 -3
- pdd/auto_update.py +125 -47
- pdd/bug_main.py +195 -23
- pdd/cmd_test_main.py +345 -197
- pdd/code_generator.py +4 -2
- pdd/code_generator_main.py +118 -32
- pdd/commands/__init__.py +6 -0
- pdd/commands/analysis.py +87 -29
- pdd/commands/auth.py +309 -0
- pdd/commands/connect.py +290 -0
- pdd/commands/fix.py +136 -113
- pdd/commands/maintenance.py +3 -2
- pdd/commands/misc.py +8 -0
- pdd/commands/modify.py +190 -164
- pdd/commands/sessions.py +284 -0
- pdd/construct_paths.py +334 -32
- pdd/context_generator_main.py +167 -170
- pdd/continue_generation.py +6 -3
- pdd/core/__init__.py +33 -0
- pdd/core/cli.py +27 -3
- pdd/core/cloud.py +237 -0
- pdd/core/errors.py +4 -0
- pdd/core/remote_session.py +61 -0
- pdd/crash_main.py +219 -23
- pdd/data/llm_model.csv +4 -4
- pdd/docs/prompting_guide.md +864 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
- pdd/fix_code_loop.py +208 -34
- pdd/fix_code_module_errors.py +6 -2
- pdd/fix_error_loop.py +291 -38
- pdd/fix_main.py +204 -4
- pdd/fix_verification_errors_loop.py +235 -26
- pdd/fix_verification_main.py +269 -83
- pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
- pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
- pdd/frontend/dist/index.html +376 -0
- pdd/frontend/dist/logo.svg +33 -0
- pdd/generate_output_paths.py +46 -5
- pdd/generate_test.py +212 -151
- pdd/get_comment.py +19 -44
- pdd/get_extension.py +8 -9
- pdd/get_jwt_token.py +309 -20
- pdd/get_language.py +8 -7
- pdd/get_run_command.py +7 -5
- pdd/insert_includes.py +2 -1
- pdd/llm_invoke.py +459 -95
- pdd/load_prompt_template.py +15 -34
- pdd/path_resolution.py +140 -0
- pdd/postprocess.py +4 -1
- pdd/preprocess.py +68 -12
- pdd/preprocess_main.py +33 -1
- pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
- pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
- pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
- pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
- pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
- pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
- pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
- pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
- pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
- pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
- pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
- pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
- pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
- pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
- pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
- pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
- pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
- pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
- pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
- pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
- pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
- pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
- pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
- pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
- pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
- pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
- pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
- pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
- pdd/prompts/agentic_update_LLM.prompt +192 -338
- pdd/prompts/auto_include_LLM.prompt +22 -0
- pdd/prompts/change_LLM.prompt +3093 -1
- pdd/prompts/detect_change_LLM.prompt +571 -14
- pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
- pdd/prompts/generate_test_LLM.prompt +20 -1
- pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
- pdd/prompts/insert_includes_LLM.prompt +262 -252
- pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
- pdd/prompts/prompt_diff_LLM.prompt +82 -0
- pdd/remote_session.py +876 -0
- pdd/server/__init__.py +52 -0
- pdd/server/app.py +335 -0
- pdd/server/click_executor.py +587 -0
- pdd/server/executor.py +338 -0
- pdd/server/jobs.py +661 -0
- pdd/server/models.py +241 -0
- pdd/server/routes/__init__.py +31 -0
- pdd/server/routes/architecture.py +451 -0
- pdd/server/routes/auth.py +364 -0
- pdd/server/routes/commands.py +929 -0
- pdd/server/routes/config.py +42 -0
- pdd/server/routes/files.py +603 -0
- pdd/server/routes/prompts.py +1322 -0
- pdd/server/routes/websocket.py +473 -0
- pdd/server/security.py +243 -0
- pdd/server/terminal_spawner.py +209 -0
- pdd/server/token_counter.py +222 -0
- pdd/summarize_directory.py +236 -237
- pdd/sync_animation.py +8 -4
- pdd/sync_determine_operation.py +329 -47
- pdd/sync_main.py +272 -28
- pdd/sync_orchestration.py +136 -75
- pdd/template_expander.py +161 -0
- pdd/templates/architecture/architecture_json.prompt +41 -46
- pdd/trace.py +1 -1
- pdd/track_cost.py +0 -13
- pdd/unfinished_prompt.py +2 -1
- pdd/update_main.py +23 -5
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +15 -10
- pdd_cli-0.0.118.dist-info/RECORD +227 -0
- pdd_cli-0.0.90.dist-info/RECORD +0 -153
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/top_level.txt +0 -0
pdd/load_prompt_template.py
CHANGED
|
@@ -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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
288
|
-
response = app.
|
|
289
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|