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