pdd-cli 0.0.90__py3-none-any.whl → 0.0.121__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pdd/__init__.py +38 -6
- pdd/agentic_bug.py +323 -0
- pdd/agentic_bug_orchestrator.py +506 -0
- pdd/agentic_change.py +231 -0
- pdd/agentic_change_orchestrator.py +537 -0
- pdd/agentic_common.py +533 -770
- pdd/agentic_crash.py +2 -1
- pdd/agentic_e2e_fix.py +319 -0
- pdd/agentic_e2e_fix_orchestrator.py +582 -0
- pdd/agentic_fix.py +118 -3
- pdd/agentic_update.py +27 -9
- pdd/agentic_verify.py +3 -2
- pdd/architecture_sync.py +565 -0
- pdd/auth_service.py +210 -0
- pdd/auto_deps_main.py +63 -53
- pdd/auto_include.py +236 -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 +113 -48
- pdd/commands/auth.py +309 -0
- pdd/commands/connect.py +358 -0
- pdd/commands/fix.py +155 -114
- pdd/commands/generate.py +5 -0
- pdd/commands/maintenance.py +3 -2
- pdd/commands/misc.py +8 -0
- pdd/commands/modify.py +225 -163
- pdd/commands/sessions.py +284 -0
- pdd/commands/utility.py +12 -7
- 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 +44 -7
- pdd/core/cloud.py +237 -0
- pdd/core/dump.py +68 -20
- 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 +208 -6
- 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-CUWd8al1.js +450 -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 +531 -97
- pdd/load_prompt_template.py +15 -34
- pdd/operation_log.py +342 -0
- pdd/path_resolution.py +140 -0
- pdd/postprocess.py +122 -97
- 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 +140 -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 +19 -1
- pdd/prompts/generate_test_from_example_LLM.prompt +366 -0
- pdd/prompts/insert_includes_LLM.prompt +262 -252
- pdd/prompts/prompt_code_diff_LLM.prompt +123 -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 +1347 -0
- pdd/server/routes/websocket.py +473 -0
- pdd/server/security.py +243 -0
- pdd/server/terminal_spawner.py +217 -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 +289 -211
- pdd/sync_order.py +304 -0
- 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 +68 -26
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/METADATA +15 -10
- pdd_cli-0.0.121.dist-info/RECORD +229 -0
- pdd_cli-0.0.90.dist-info/RECORD +0 -153
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/top_level.txt +0 -0
pdd/code_generator_main.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import re
|
|
3
|
-
import asyncio
|
|
4
3
|
import json
|
|
5
4
|
import pathlib
|
|
6
5
|
import shlex
|
|
@@ -21,16 +20,15 @@ from .construct_paths import construct_paths
|
|
|
21
20
|
from .preprocess import preprocess as pdd_preprocess
|
|
22
21
|
from .code_generator import code_generator as local_code_generator_func
|
|
23
22
|
from .incremental_code_generator import incremental_code_generator as incremental_code_generator_func
|
|
24
|
-
from .
|
|
23
|
+
from .core.cloud import CloudConfig
|
|
25
24
|
from .python_env_detector import detect_host_python_executable
|
|
25
|
+
from .architecture_sync import (
|
|
26
|
+
get_architecture_entry_for_prompt,
|
|
27
|
+
has_pdd_tags,
|
|
28
|
+
generate_tags_from_architecture,
|
|
29
|
+
)
|
|
26
30
|
|
|
27
|
-
#
|
|
28
|
-
FIREBASE_API_KEY_ENV_VAR = "NEXT_PUBLIC_FIREBASE_API_KEY"
|
|
29
|
-
GITHUB_CLIENT_ID_ENV_VAR = "GITHUB_CLIENT_ID"
|
|
30
|
-
PDD_APP_NAME = "PDD Code Generator"
|
|
31
|
-
|
|
32
|
-
# Cloud function URL
|
|
33
|
-
CLOUD_GENERATE_URL = "https://us-central1-prompt-driven-development.cloudfunctions.net/generateCode"
|
|
31
|
+
# Cloud request timeout
|
|
34
32
|
CLOUD_REQUEST_TIMEOUT = 400 # seconds
|
|
35
33
|
|
|
36
34
|
console = Console()
|
|
@@ -46,6 +44,13 @@ def _parse_llm_bool(value: str) -> bool:
|
|
|
46
44
|
else:
|
|
47
45
|
return llm_str in {"1", "true", "yes", "on"}
|
|
48
46
|
|
|
47
|
+
def _env_flag_enabled(name: str) -> bool:
|
|
48
|
+
"""Return True when an env var is set to a truthy value."""
|
|
49
|
+
value = os.environ.get(name)
|
|
50
|
+
if value is None:
|
|
51
|
+
return False
|
|
52
|
+
return str(value).strip().lower() in {"1", "true", "yes", "on"}
|
|
53
|
+
|
|
49
54
|
# --- Git Helper Functions ---
|
|
50
55
|
def _run_git_command(command: List[str], cwd: Optional[str] = None) -> Tuple[int, str, str]:
|
|
51
56
|
"""Runs a git command and returns (return_code, stdout, stderr)."""
|
|
@@ -762,7 +767,8 @@ def code_generator_main(
|
|
|
762
767
|
if verbose:
|
|
763
768
|
console.print(Panel("Performing full code generation...", title="[blue]Mode[/blue]", expand=False))
|
|
764
769
|
|
|
765
|
-
|
|
770
|
+
cloud_only = _env_flag_enabled("PDD_CLOUD_ONLY") or _env_flag_enabled("PDD_NO_LOCAL_FALLBACK")
|
|
771
|
+
current_execution_is_local = is_local_execution_preferred and not cloud_only
|
|
766
772
|
|
|
767
773
|
if not current_execution_is_local:
|
|
768
774
|
if verbose: console.print("Attempting cloud code generation...")
|
|
@@ -772,31 +778,22 @@ def code_generator_main(
|
|
|
772
778
|
processed_prompt_for_cloud = pdd_preprocess(processed_prompt_for_cloud, recursive=False, double_curly_brackets=True, exclude_keys=[])
|
|
773
779
|
if verbose: console.print(Panel(Text(processed_prompt_for_cloud, overflow="fold"), title="[cyan]Preprocessed Prompt for Cloud[/cyan]", expand=False))
|
|
774
780
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
jwt_token = asyncio.run(get_jwt_token(
|
|
784
|
-
firebase_api_key=firebase_api_key_val,
|
|
785
|
-
github_client_id=github_client_id_val,
|
|
786
|
-
app_name=PDD_APP_NAME
|
|
787
|
-
))
|
|
788
|
-
except (AuthError, NetworkError, TokenError, UserCancelledError, RateLimitError) as e:
|
|
789
|
-
console.print(f"[yellow]Cloud authentication/token error: {e}. Falling back to local execution.[/yellow]")
|
|
790
|
-
current_execution_is_local = True
|
|
791
|
-
except Exception as e:
|
|
792
|
-
console.print(f"[yellow]Unexpected error during cloud authentication: {e}. Falling back to local execution.[/yellow]")
|
|
781
|
+
# Get JWT token via CloudConfig (handles both injected tokens and device flow)
|
|
782
|
+
jwt_token = CloudConfig.get_jwt_token(verbose=verbose)
|
|
783
|
+
|
|
784
|
+
if not jwt_token:
|
|
785
|
+
if cloud_only:
|
|
786
|
+
console.print("[red]Cloud authentication failed.[/red]")
|
|
787
|
+
raise click.UsageError("Cloud authentication failed")
|
|
788
|
+
console.print("[yellow]Cloud authentication failed. Falling back to local execution.[/yellow]")
|
|
793
789
|
current_execution_is_local = True
|
|
794
790
|
|
|
795
791
|
if jwt_token and not current_execution_is_local:
|
|
796
792
|
payload = {"promptContent": processed_prompt_for_cloud, "language": language, "strength": strength, "temperature": temperature, "verbose": verbose}
|
|
797
793
|
headers = {"Authorization": f"Bearer {jwt_token}", "Content-Type": "application/json"}
|
|
794
|
+
cloud_url = CloudConfig.get_endpoint_url("generateCode")
|
|
798
795
|
try:
|
|
799
|
-
response = requests.post(
|
|
796
|
+
response = requests.post(cloud_url, json=payload, headers=headers, timeout=CLOUD_REQUEST_TIMEOUT)
|
|
800
797
|
response.raise_for_status()
|
|
801
798
|
|
|
802
799
|
response_data = response.json()
|
|
@@ -804,22 +801,71 @@ def code_generator_main(
|
|
|
804
801
|
total_cost = float(response_data.get("totalCost", 0.0))
|
|
805
802
|
model_name = response_data.get("modelName", "cloud_model")
|
|
806
803
|
|
|
804
|
+
# Strip markdown code fences if present (cloud API returns fenced JSON)
|
|
805
|
+
if generated_code_content and isinstance(language, str) and language.strip().lower() == "json":
|
|
806
|
+
cleaned = generated_code_content.strip()
|
|
807
|
+
if cleaned.startswith("```json"):
|
|
808
|
+
cleaned = cleaned[7:]
|
|
809
|
+
elif cleaned.startswith("```"):
|
|
810
|
+
cleaned = cleaned[3:]
|
|
811
|
+
if cleaned.endswith("```"):
|
|
812
|
+
cleaned = cleaned[:-3]
|
|
813
|
+
generated_code_content = cleaned.strip()
|
|
814
|
+
|
|
807
815
|
if not generated_code_content:
|
|
816
|
+
if cloud_only:
|
|
817
|
+
console.print("[red]Cloud execution returned no code.[/red]")
|
|
818
|
+
raise click.UsageError("Cloud execution returned no code")
|
|
808
819
|
console.print("[yellow]Cloud execution returned no code. Falling back to local.[/yellow]")
|
|
809
820
|
current_execution_is_local = True
|
|
810
821
|
elif verbose:
|
|
811
822
|
console.print(Panel(f"Cloud generation successful. Model: {model_name}, Cost: ${total_cost:.6f}", title="[green]Cloud Success[/green]", expand=False))
|
|
812
823
|
except requests.exceptions.Timeout:
|
|
824
|
+
if cloud_only:
|
|
825
|
+
console.print(f"[red]Cloud execution timed out ({CLOUD_REQUEST_TIMEOUT}s).[/red]")
|
|
826
|
+
raise click.UsageError("Cloud execution timed out")
|
|
813
827
|
console.print(f"[yellow]Cloud execution timed out ({CLOUD_REQUEST_TIMEOUT}s). Falling back to local.[/yellow]")
|
|
814
828
|
current_execution_is_local = True
|
|
815
829
|
except requests.exceptions.HTTPError as e:
|
|
830
|
+
status_code = e.response.status_code if e.response else 0
|
|
816
831
|
err_content = e.response.text[:200] if e.response else "No response content"
|
|
817
|
-
|
|
818
|
-
|
|
832
|
+
|
|
833
|
+
# Non-recoverable errors: do NOT fall back to local
|
|
834
|
+
if status_code == 402: # Insufficient credits
|
|
835
|
+
try:
|
|
836
|
+
error_data = e.response.json()
|
|
837
|
+
current_balance = error_data.get("currentBalance", "unknown")
|
|
838
|
+
estimated_cost = error_data.get("estimatedCost", "unknown")
|
|
839
|
+
console.print(f"[red]Insufficient credits. Current balance: {current_balance}, estimated cost: {estimated_cost}[/red]")
|
|
840
|
+
except Exception:
|
|
841
|
+
console.print(f"[red]Insufficient credits: {err_content}[/red]")
|
|
842
|
+
raise click.UsageError("Insufficient credits for cloud code generation")
|
|
843
|
+
elif status_code == 401: # Authentication error
|
|
844
|
+
console.print(f"[red]Authentication failed: {err_content}[/red]")
|
|
845
|
+
raise click.UsageError("Cloud authentication failed")
|
|
846
|
+
elif status_code == 403: # Authorization error (not approved)
|
|
847
|
+
console.print(f"[red]Access denied: {err_content}[/red]")
|
|
848
|
+
raise click.UsageError("Access denied - user not approved")
|
|
849
|
+
elif status_code == 400: # Validation error (e.g., empty prompt)
|
|
850
|
+
console.print(f"[red]Invalid request: {err_content}[/red]")
|
|
851
|
+
raise click.UsageError(f"Invalid request: {err_content}")
|
|
852
|
+
else:
|
|
853
|
+
# Recoverable errors (5xx, unexpected errors): fall back to local
|
|
854
|
+
if cloud_only:
|
|
855
|
+
console.print(f"[red]Cloud HTTP error ({status_code}): {err_content}[/red]")
|
|
856
|
+
raise click.UsageError(f"Cloud HTTP error ({status_code}): {err_content}")
|
|
857
|
+
console.print(f"[yellow]Cloud HTTP error ({status_code}): {err_content}. Falling back to local.[/yellow]")
|
|
858
|
+
current_execution_is_local = True
|
|
819
859
|
except requests.exceptions.RequestException as e:
|
|
860
|
+
if cloud_only:
|
|
861
|
+
console.print(f"[red]Cloud network error: {e}[/red]")
|
|
862
|
+
raise click.UsageError(f"Cloud network error: {e}")
|
|
820
863
|
console.print(f"[yellow]Cloud network error: {e}. Falling back to local.[/yellow]")
|
|
821
864
|
current_execution_is_local = True
|
|
822
865
|
except json.JSONDecodeError:
|
|
866
|
+
if cloud_only:
|
|
867
|
+
console.print("[red]Cloud returned invalid JSON.[/red]")
|
|
868
|
+
raise click.UsageError("Cloud returned invalid JSON")
|
|
823
869
|
console.print("[yellow]Cloud returned invalid JSON. Falling back to local.[/yellow]")
|
|
824
870
|
current_execution_is_local = True
|
|
825
871
|
|
|
@@ -1006,6 +1052,23 @@ def code_generator_main(
|
|
|
1006
1052
|
raise click.UsageError(f"LLM generation failed: {generated_code_content}")
|
|
1007
1053
|
|
|
1008
1054
|
parsed = json.loads(generated_code_content)
|
|
1055
|
+
|
|
1056
|
+
# Fix common LLM mistake: unwrap arrays wrapped in objects
|
|
1057
|
+
# LLMs often return {"items": [...]} or {"type": "array", "items": [...]}
|
|
1058
|
+
# when the schema expects a plain array [...]
|
|
1059
|
+
output_schema = fm_meta.get("output_schema", {})
|
|
1060
|
+
if output_schema.get("type") == "array" and isinstance(parsed, dict):
|
|
1061
|
+
# Check for common wrapper patterns
|
|
1062
|
+
if "items" in parsed and isinstance(parsed["items"], list):
|
|
1063
|
+
parsed = parsed["items"]
|
|
1064
|
+
generated_code_content = json.dumps(parsed, indent=2)
|
|
1065
|
+
elif "data" in parsed and isinstance(parsed["data"], list):
|
|
1066
|
+
parsed = parsed["data"]
|
|
1067
|
+
generated_code_content = json.dumps(parsed, indent=2)
|
|
1068
|
+
elif "results" in parsed and isinstance(parsed["results"], list):
|
|
1069
|
+
parsed = parsed["results"]
|
|
1070
|
+
generated_code_content = json.dumps(parsed, indent=2)
|
|
1071
|
+
|
|
1009
1072
|
if _is_architecture_template(fm_meta):
|
|
1010
1073
|
parsed, repaired = _repair_architecture_interface_types(parsed)
|
|
1011
1074
|
if repaired:
|
|
@@ -1024,7 +1087,30 @@ def code_generator_main(
|
|
|
1024
1087
|
if output_path:
|
|
1025
1088
|
p_output = pathlib.Path(output_path)
|
|
1026
1089
|
p_output.parent.mkdir(parents=True, exist_ok=True)
|
|
1027
|
-
|
|
1090
|
+
|
|
1091
|
+
# Inject architecture metadata tags for .prompt files (reverse sync)
|
|
1092
|
+
final_content = generated_code_content
|
|
1093
|
+
if p_output.suffix == '.prompt':
|
|
1094
|
+
try:
|
|
1095
|
+
# Check if this prompt has an architecture entry
|
|
1096
|
+
arch_entry = get_architecture_entry_for_prompt(p_output.name)
|
|
1097
|
+
|
|
1098
|
+
# Only inject tags if:
|
|
1099
|
+
# 1. Architecture entry exists
|
|
1100
|
+
# 2. Content doesn't already have PDD tags (preserve manual edits)
|
|
1101
|
+
if arch_entry and not has_pdd_tags(generated_code_content):
|
|
1102
|
+
tags = generate_tags_from_architecture(arch_entry)
|
|
1103
|
+
if tags:
|
|
1104
|
+
# Prepend tags to the generated content
|
|
1105
|
+
final_content = tags + '\n\n' + generated_code_content
|
|
1106
|
+
if verbose:
|
|
1107
|
+
console.print("[info]Injected architecture metadata tags from architecture.json[/info]")
|
|
1108
|
+
except Exception as e:
|
|
1109
|
+
# Don't fail generation if tag injection fails
|
|
1110
|
+
if verbose:
|
|
1111
|
+
console.print(f"[yellow]Warning: Could not inject architecture tags: {e}[/yellow]")
|
|
1112
|
+
|
|
1113
|
+
p_output.write_text(final_content, encoding="utf-8")
|
|
1028
1114
|
if verbose or not quiet:
|
|
1029
1115
|
console.print(f"Generated code saved to: [green]{p_output.resolve()}[/green]")
|
|
1030
1116
|
# Safety net: ensure architecture HTML is generated post-write if applicable
|
pdd/commands/__init__.py
CHANGED
|
@@ -8,7 +8,10 @@ from .fix import fix
|
|
|
8
8
|
from .modify import split, change, update
|
|
9
9
|
from .maintenance import sync, auto_deps, setup
|
|
10
10
|
from .analysis import detect_change, conflicts, bug, crash, trace
|
|
11
|
+
from .connect import connect
|
|
12
|
+
from .auth import auth_group
|
|
11
13
|
from .misc import preprocess
|
|
14
|
+
from .sessions import sessions
|
|
12
15
|
from .report import report_core
|
|
13
16
|
from .templates import templates_group
|
|
14
17
|
from .utility import install_completion_cmd, verify
|
|
@@ -40,3 +43,6 @@ def register_commands(cli: click.Group) -> None:
|
|
|
40
43
|
# The original code did: cli.commands["templates"] = templates_group
|
|
41
44
|
# Using add_command is cleaner if it works for the structure.
|
|
42
45
|
cli.add_command(templates_group)
|
|
46
|
+
cli.add_command(connect)
|
|
47
|
+
cli.add_command(auth_group)
|
|
48
|
+
cli.add_command(sessions)
|
pdd/commands/analysis.py
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
"""
|
|
2
4
|
Analysis commands (detect-change, conflicts, bug, crash, trace).
|
|
3
5
|
"""
|
|
6
|
+
import os
|
|
4
7
|
import click
|
|
5
|
-
from typing import Optional, Tuple, List
|
|
8
|
+
from typing import Optional, Tuple, List, Dict, Any
|
|
6
9
|
|
|
7
10
|
from ..detect_change_main import detect_change_main
|
|
8
11
|
from ..conflicts_main import conflicts_main
|
|
9
12
|
from ..bug_main import bug_main
|
|
13
|
+
from ..agentic_bug import run_agentic_bug
|
|
10
14
|
from ..crash_main import crash_main
|
|
11
15
|
from ..trace_main import trace_main
|
|
12
16
|
from ..track_cost import track_cost
|
|
13
17
|
from ..core.errors import handle_error
|
|
18
|
+
from ..operation_log import log_operation
|
|
19
|
+
|
|
20
|
+
def get_context_obj(ctx: click.Context) -> Dict[str, Any]:
|
|
21
|
+
"""Safely retrieve the context object, defaulting to empty dict if None."""
|
|
22
|
+
return ctx.obj or {}
|
|
14
23
|
|
|
15
24
|
@click.command("detect")
|
|
16
25
|
@click.argument("files", nargs=-1, type=click.Path(exists=True, dir_okay=False))
|
|
@@ -24,8 +33,8 @@ from ..core.errors import handle_error
|
|
|
24
33
|
@track_cost
|
|
25
34
|
def detect_change(
|
|
26
35
|
ctx: click.Context,
|
|
27
|
-
files: Tuple[str, ...],
|
|
28
|
-
output: Optional[str],
|
|
36
|
+
files: Tuple[str, ...] = (),
|
|
37
|
+
output: Optional[str] = None,
|
|
29
38
|
) -> Optional[Tuple[List, float, str]]:
|
|
30
39
|
"""Detect if prompts need to be changed based on a description.
|
|
31
40
|
|
|
@@ -46,10 +55,10 @@ def detect_change(
|
|
|
46
55
|
output=output,
|
|
47
56
|
)
|
|
48
57
|
return result, total_cost, model_name
|
|
49
|
-
except click.Abort:
|
|
58
|
+
except (click.Abort, click.ClickException):
|
|
50
59
|
raise
|
|
51
60
|
except Exception as exception:
|
|
52
|
-
handle_error(exception, "detect", ctx.
|
|
61
|
+
handle_error(exception, "detect", get_context_obj(ctx).get("quiet", False))
|
|
53
62
|
return None
|
|
54
63
|
|
|
55
64
|
|
|
@@ -68,7 +77,7 @@ def conflicts(
|
|
|
68
77
|
ctx: click.Context,
|
|
69
78
|
prompt1: str,
|
|
70
79
|
prompt2: str,
|
|
71
|
-
output: Optional[str],
|
|
80
|
+
output: Optional[str] = None,
|
|
72
81
|
) -> Optional[Tuple[List, float, str]]:
|
|
73
82
|
"""Check for conflicts between two prompt files."""
|
|
74
83
|
try:
|
|
@@ -77,22 +86,24 @@ def conflicts(
|
|
|
77
86
|
prompt1=prompt1,
|
|
78
87
|
prompt2=prompt2,
|
|
79
88
|
output=output,
|
|
80
|
-
verbose=ctx.
|
|
89
|
+
verbose=get_context_obj(ctx).get("verbose", False),
|
|
81
90
|
)
|
|
82
91
|
return result, total_cost, model_name
|
|
83
|
-
except click.Abort:
|
|
92
|
+
except (click.Abort, click.ClickException):
|
|
84
93
|
raise
|
|
85
94
|
except Exception as exception:
|
|
86
|
-
handle_error(exception, "conflicts", ctx.
|
|
95
|
+
handle_error(exception, "conflicts", get_context_obj(ctx).get("quiet", False))
|
|
87
96
|
return None
|
|
88
97
|
|
|
89
98
|
|
|
90
99
|
@click.command("bug")
|
|
91
|
-
@click.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
@click.option(
|
|
101
|
+
"--manual",
|
|
102
|
+
is_flag=True,
|
|
103
|
+
default=False,
|
|
104
|
+
help="Run in manual mode requiring 5 positional file arguments.",
|
|
105
|
+
)
|
|
106
|
+
@click.argument("args", nargs=-1)
|
|
96
107
|
@click.option(
|
|
97
108
|
"--output",
|
|
98
109
|
type=click.Path(writable=True),
|
|
@@ -103,37 +114,90 @@ def conflicts(
|
|
|
103
114
|
"--language",
|
|
104
115
|
type=str,
|
|
105
116
|
default="Python",
|
|
106
|
-
help="Programming language for the unit test.",
|
|
117
|
+
help="Programming language for the unit test (Manual mode only).",
|
|
118
|
+
)
|
|
119
|
+
@click.option(
|
|
120
|
+
"--timeout-adder",
|
|
121
|
+
type=float,
|
|
122
|
+
default=0.0,
|
|
123
|
+
help="Additional seconds to add to each step's timeout (agentic mode only).",
|
|
124
|
+
)
|
|
125
|
+
@click.option(
|
|
126
|
+
"--no-github-state",
|
|
127
|
+
is_flag=True,
|
|
128
|
+
default=False,
|
|
129
|
+
help="Disable GitHub state persistence (agentic mode only).",
|
|
107
130
|
)
|
|
108
131
|
@click.pass_context
|
|
109
132
|
@track_cost
|
|
110
133
|
def bug(
|
|
111
134
|
ctx: click.Context,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
language: str,
|
|
135
|
+
manual: bool = False,
|
|
136
|
+
args: Tuple[str, ...] = (),
|
|
137
|
+
output: Optional[str] = None,
|
|
138
|
+
language: str = "Python",
|
|
139
|
+
timeout_adder: float = 0.0,
|
|
140
|
+
no_github_state: bool = False,
|
|
119
141
|
) -> Optional[Tuple[str, float, str]]:
|
|
120
|
-
"""Generate a unit test
|
|
142
|
+
"""Generate a unit test (manual) or investigate a bug (agentic).
|
|
143
|
+
|
|
144
|
+
Agentic Mode (default):
|
|
145
|
+
pdd bug ISSUE_URL
|
|
146
|
+
|
|
147
|
+
Manual Mode:
|
|
148
|
+
pdd bug --manual PROMPT_FILE CODE_FILE PROGRAM_FILE CURRENT_OUTPUT DESIRED_OUTPUT
|
|
149
|
+
"""
|
|
121
150
|
try:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
151
|
+
obj = get_context_obj(ctx)
|
|
152
|
+
if manual:
|
|
153
|
+
if len(args) != 5:
|
|
154
|
+
raise click.UsageError(
|
|
155
|
+
"Manual mode requires 5 arguments: PROMPT_FILE CODE_FILE PROGRAM_FILE CURRENT_OUTPUT DESIRED_OUTPUT"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Validate files exist (replicating click.Path(exists=True))
|
|
159
|
+
for f in args:
|
|
160
|
+
if not os.path.exists(f):
|
|
161
|
+
raise click.UsageError(f"File does not exist: {f}")
|
|
162
|
+
if os.path.isdir(f):
|
|
163
|
+
raise click.UsageError(f"Path is a directory, not a file: {f}")
|
|
164
|
+
|
|
165
|
+
prompt_file, code_file, program_file, current_output, desired_output = args
|
|
166
|
+
|
|
167
|
+
result, total_cost, model_name = bug_main(
|
|
168
|
+
ctx=ctx,
|
|
169
|
+
prompt_file=prompt_file,
|
|
170
|
+
code_file=code_file,
|
|
171
|
+
program_file=program_file,
|
|
172
|
+
current_output=current_output,
|
|
173
|
+
desired_output=desired_output,
|
|
174
|
+
output=output,
|
|
175
|
+
language=language,
|
|
176
|
+
)
|
|
177
|
+
return result, total_cost, model_name
|
|
178
|
+
|
|
179
|
+
else:
|
|
180
|
+
# Agentic mode
|
|
181
|
+
if len(args) != 1:
|
|
182
|
+
raise click.UsageError("Agentic mode requires exactly one argument: the GitHub Issue URL.")
|
|
183
|
+
|
|
184
|
+
issue_url = args[0]
|
|
185
|
+
|
|
186
|
+
success, message, cost, model, changed_files = run_agentic_bug(
|
|
187
|
+
issue_url=issue_url,
|
|
188
|
+
verbose=obj.get("verbose", False),
|
|
189
|
+
quiet=obj.get("quiet", False),
|
|
190
|
+
timeout_adder=timeout_adder,
|
|
191
|
+
use_github_state=not no_github_state,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
result_str = f"Success: {success}\nMessage: {message}\nChanged Files: {changed_files}"
|
|
195
|
+
return result_str, cost, model
|
|
196
|
+
|
|
197
|
+
except (click.Abort, click.ClickException):
|
|
134
198
|
raise
|
|
135
199
|
except Exception as exception:
|
|
136
|
-
handle_error(exception, "bug", ctx.
|
|
200
|
+
handle_error(exception, "bug", get_context_obj(ctx).get("quiet", False))
|
|
137
201
|
return None
|
|
138
202
|
|
|
139
203
|
|
|
@@ -173,6 +237,7 @@ def bug(
|
|
|
173
237
|
help="Maximum cost allowed for the fixing process (default: 5.0).",
|
|
174
238
|
)
|
|
175
239
|
@click.pass_context
|
|
240
|
+
@log_operation("crash", clears_run_report=True)
|
|
176
241
|
@track_cost
|
|
177
242
|
def crash(
|
|
178
243
|
ctx: click.Context,
|
|
@@ -180,15 +245,15 @@ def crash(
|
|
|
180
245
|
code_file: str,
|
|
181
246
|
program_file: str,
|
|
182
247
|
error_file: str,
|
|
183
|
-
output: Optional[str],
|
|
184
|
-
output_program: Optional[str],
|
|
185
|
-
loop: bool,
|
|
186
|
-
max_attempts: Optional[int],
|
|
187
|
-
budget: Optional[float],
|
|
248
|
+
output: Optional[str] = None,
|
|
249
|
+
output_program: Optional[str] = None,
|
|
250
|
+
loop: bool = False,
|
|
251
|
+
max_attempts: Optional[int] = None,
|
|
252
|
+
budget: Optional[float] = None,
|
|
188
253
|
) -> Optional[Tuple[str, float, str]]:
|
|
189
254
|
"""Analyze a crash and fix the code and program."""
|
|
190
255
|
try:
|
|
191
|
-
# crash_main returns: success, final_code, final_program, attempts,
|
|
256
|
+
# crash_main returns: success, final_code, final_program, attempts, total_cost, model_name
|
|
192
257
|
success, final_code, final_program, attempts, total_cost, model_name = crash_main(
|
|
193
258
|
ctx=ctx,
|
|
194
259
|
prompt_file=prompt_file,
|
|
@@ -204,10 +269,10 @@ def crash(
|
|
|
204
269
|
# Return a summary string as the result for track_cost/CLI output
|
|
205
270
|
result = f"Success: {success}, Attempts: {attempts}"
|
|
206
271
|
return result, total_cost, model_name
|
|
207
|
-
except click.Abort:
|
|
272
|
+
except (click.Abort, click.ClickException):
|
|
208
273
|
raise
|
|
209
274
|
except Exception as exception:
|
|
210
|
-
handle_error(exception, "crash", ctx.
|
|
275
|
+
handle_error(exception, "crash", get_context_obj(ctx).get("quiet", False))
|
|
211
276
|
return None
|
|
212
277
|
|
|
213
278
|
|
|
@@ -228,7 +293,7 @@ def trace(
|
|
|
228
293
|
prompt_file: str,
|
|
229
294
|
code_file: str,
|
|
230
295
|
code_line: int,
|
|
231
|
-
output: Optional[str],
|
|
296
|
+
output: Optional[str] = None,
|
|
232
297
|
) -> Optional[Tuple[str, float, str]]:
|
|
233
298
|
"""Trace execution flow back to the prompt."""
|
|
234
299
|
try:
|
|
@@ -241,8 +306,8 @@ def trace(
|
|
|
241
306
|
output=output,
|
|
242
307
|
)
|
|
243
308
|
return str(result), total_cost, model_name
|
|
244
|
-
except click.Abort:
|
|
309
|
+
except (click.Abort, click.ClickException):
|
|
245
310
|
raise
|
|
246
311
|
except Exception as exception:
|
|
247
|
-
handle_error(exception, "trace", ctx.
|
|
248
|
-
return None
|
|
312
|
+
handle_error(exception, "trace", get_context_obj(ctx).get("quiet", False))
|
|
313
|
+
return None
|