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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. pdd/__init__.py +38 -6
  2. pdd/agentic_bug.py +323 -0
  3. pdd/agentic_bug_orchestrator.py +497 -0
  4. pdd/agentic_change.py +231 -0
  5. pdd/agentic_change_orchestrator.py +526 -0
  6. pdd/agentic_common.py +521 -786
  7. pdd/agentic_e2e_fix.py +319 -0
  8. pdd/agentic_e2e_fix_orchestrator.py +426 -0
  9. pdd/agentic_fix.py +118 -3
  10. pdd/agentic_update.py +25 -8
  11. pdd/architecture_sync.py +565 -0
  12. pdd/auth_service.py +210 -0
  13. pdd/auto_deps_main.py +63 -53
  14. pdd/auto_include.py +185 -3
  15. pdd/auto_update.py +125 -47
  16. pdd/bug_main.py +195 -23
  17. pdd/cmd_test_main.py +345 -197
  18. pdd/code_generator.py +4 -2
  19. pdd/code_generator_main.py +118 -32
  20. pdd/commands/__init__.py +6 -0
  21. pdd/commands/analysis.py +87 -29
  22. pdd/commands/auth.py +309 -0
  23. pdd/commands/connect.py +290 -0
  24. pdd/commands/fix.py +136 -113
  25. pdd/commands/maintenance.py +3 -2
  26. pdd/commands/misc.py +8 -0
  27. pdd/commands/modify.py +190 -164
  28. pdd/commands/sessions.py +284 -0
  29. pdd/construct_paths.py +334 -32
  30. pdd/context_generator_main.py +167 -170
  31. pdd/continue_generation.py +6 -3
  32. pdd/core/__init__.py +33 -0
  33. pdd/core/cli.py +27 -3
  34. pdd/core/cloud.py +237 -0
  35. pdd/core/errors.py +4 -0
  36. pdd/core/remote_session.py +61 -0
  37. pdd/crash_main.py +219 -23
  38. pdd/data/llm_model.csv +4 -4
  39. pdd/docs/prompting_guide.md +864 -0
  40. pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
  41. pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
  42. pdd/fix_code_loop.py +208 -34
  43. pdd/fix_code_module_errors.py +6 -2
  44. pdd/fix_error_loop.py +291 -38
  45. pdd/fix_main.py +204 -4
  46. pdd/fix_verification_errors_loop.py +235 -26
  47. pdd/fix_verification_main.py +269 -83
  48. pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
  49. pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
  50. pdd/frontend/dist/index.html +376 -0
  51. pdd/frontend/dist/logo.svg +33 -0
  52. pdd/generate_output_paths.py +46 -5
  53. pdd/generate_test.py +212 -151
  54. pdd/get_comment.py +19 -44
  55. pdd/get_extension.py +8 -9
  56. pdd/get_jwt_token.py +309 -20
  57. pdd/get_language.py +8 -7
  58. pdd/get_run_command.py +7 -5
  59. pdd/insert_includes.py +2 -1
  60. pdd/llm_invoke.py +459 -95
  61. pdd/load_prompt_template.py +15 -34
  62. pdd/path_resolution.py +140 -0
  63. pdd/postprocess.py +4 -1
  64. pdd/preprocess.py +68 -12
  65. pdd/preprocess_main.py +33 -1
  66. pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
  67. pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
  68. pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
  69. pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
  70. pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
  71. pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
  72. pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
  73. pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
  74. pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
  75. pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
  76. pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
  77. pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
  78. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
  79. pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
  80. pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
  81. pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
  82. pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
  83. pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
  84. pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
  85. pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
  86. pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
  87. pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
  88. pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
  89. pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
  90. pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
  91. pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
  92. pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
  93. pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
  94. pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
  95. pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
  96. pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
  97. pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
  98. pdd/prompts/agentic_update_LLM.prompt +192 -338
  99. pdd/prompts/auto_include_LLM.prompt +22 -0
  100. pdd/prompts/change_LLM.prompt +3093 -1
  101. pdd/prompts/detect_change_LLM.prompt +571 -14
  102. pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
  103. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
  104. pdd/prompts/generate_test_LLM.prompt +20 -1
  105. pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
  106. pdd/prompts/insert_includes_LLM.prompt +262 -252
  107. pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
  108. pdd/prompts/prompt_diff_LLM.prompt +82 -0
  109. pdd/remote_session.py +876 -0
  110. pdd/server/__init__.py +52 -0
  111. pdd/server/app.py +335 -0
  112. pdd/server/click_executor.py +587 -0
  113. pdd/server/executor.py +338 -0
  114. pdd/server/jobs.py +661 -0
  115. pdd/server/models.py +241 -0
  116. pdd/server/routes/__init__.py +31 -0
  117. pdd/server/routes/architecture.py +451 -0
  118. pdd/server/routes/auth.py +364 -0
  119. pdd/server/routes/commands.py +929 -0
  120. pdd/server/routes/config.py +42 -0
  121. pdd/server/routes/files.py +603 -0
  122. pdd/server/routes/prompts.py +1322 -0
  123. pdd/server/routes/websocket.py +473 -0
  124. pdd/server/security.py +243 -0
  125. pdd/server/terminal_spawner.py +209 -0
  126. pdd/server/token_counter.py +222 -0
  127. pdd/summarize_directory.py +236 -237
  128. pdd/sync_animation.py +8 -4
  129. pdd/sync_determine_operation.py +329 -47
  130. pdd/sync_main.py +272 -28
  131. pdd/sync_orchestration.py +136 -75
  132. pdd/template_expander.py +161 -0
  133. pdd/templates/architecture/architecture_json.prompt +41 -46
  134. pdd/trace.py +1 -1
  135. pdd/track_cost.py +0 -13
  136. pdd/unfinished_prompt.py +2 -1
  137. pdd/update_main.py +23 -5
  138. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +15 -10
  139. pdd_cli-0.0.118.dist-info/RECORD +227 -0
  140. pdd_cli-0.0.90.dist-info/RECORD +0 -153
  141. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
  142. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
  143. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +0 -0
  144. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/top_level.txt +0 -0
@@ -1,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 .get_jwt_token import get_jwt_token, AuthError, NetworkError, TokenError, UserCancelledError, RateLimitError
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
- # Environment variable names for Firebase/GitHub auth
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
- current_execution_is_local = is_local_execution_preferred
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
- jwt_token: Optional[str] = None
776
- try:
777
- firebase_api_key_val = os.environ.get(FIREBASE_API_KEY_ENV_VAR)
778
- github_client_id_val = os.environ.get(GITHUB_CLIENT_ID_ENV_VAR)
779
-
780
- if not firebase_api_key_val: raise AuthError(f"{FIREBASE_API_KEY_ENV_VAR} not set.")
781
- if not github_client_id_val: raise AuthError(f"{GITHUB_CLIENT_ID_ENV_VAR} not set.")
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(CLOUD_GENERATE_URL, json=payload, headers=headers, timeout=CLOUD_REQUEST_TIMEOUT)
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
- console.print(f"[yellow]Cloud HTTP error ({e.response.status_code}): {err_content}. Falling back to local.[/yellow]")
818
- current_execution_is_local = True
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
- p_output.write_text(generated_code_content, encoding="utf-8")
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,12 +1,16 @@
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
8
  from typing import Optional, Tuple, List
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
@@ -46,7 +50,7 @@ def detect_change(
46
50
  output=output,
47
51
  )
48
52
  return result, total_cost, model_name
49
- except click.Abort:
53
+ except (click.Abort, click.ClickException):
50
54
  raise
51
55
  except Exception as exception:
52
56
  handle_error(exception, "detect", ctx.obj.get("quiet", False))
@@ -80,7 +84,7 @@ def conflicts(
80
84
  verbose=ctx.obj.get("verbose", False),
81
85
  )
82
86
  return result, total_cost, model_name
83
- except click.Abort:
87
+ except (click.Abort, click.ClickException):
84
88
  raise
85
89
  except Exception as exception:
86
90
  handle_error(exception, "conflicts", ctx.obj.get("quiet", False))
@@ -88,11 +92,13 @@ def conflicts(
88
92
 
89
93
 
90
94
  @click.command("bug")
91
- @click.argument("prompt_file", type=click.Path(exists=True, dir_okay=False))
92
- @click.argument("code_file", type=click.Path(exists=True, dir_okay=False))
93
- @click.argument("program_file", type=click.Path(exists=True, dir_okay=False))
94
- @click.argument("current_output", type=click.Path(exists=True, dir_okay=False))
95
- @click.argument("desired_output", type=click.Path(exists=True, dir_okay=False))
95
+ @click.option(
96
+ "--manual",
97
+ is_flag=True,
98
+ default=False,
99
+ help="Run in manual mode requiring 5 positional file arguments.",
100
+ )
101
+ @click.argument("args", nargs=-1)
96
102
  @click.option(
97
103
  "--output",
98
104
  type=click.Path(writable=True),
@@ -103,34 +109,86 @@ def conflicts(
103
109
  "--language",
104
110
  type=str,
105
111
  default="Python",
106
- help="Programming language for the unit test.",
112
+ help="Programming language for the unit test (Manual mode only).",
113
+ )
114
+ @click.option(
115
+ "--timeout-adder",
116
+ type=float,
117
+ default=0.0,
118
+ help="Additional seconds to add to each step's timeout (agentic mode only).",
119
+ )
120
+ @click.option(
121
+ "--no-github-state",
122
+ is_flag=True,
123
+ default=False,
124
+ help="Disable GitHub state persistence (agentic mode only).",
107
125
  )
108
126
  @click.pass_context
109
127
  @track_cost
110
128
  def bug(
111
129
  ctx: click.Context,
112
- prompt_file: str,
113
- code_file: str,
114
- program_file: str,
115
- current_output: str,
116
- desired_output: str,
130
+ manual: bool,
131
+ args: Tuple[str, ...],
117
132
  output: Optional[str],
118
133
  language: str,
134
+ timeout_adder: float,
135
+ no_github_state: bool,
119
136
  ) -> Optional[Tuple[str, float, str]]:
120
- """Generate a unit test reproducing a bug from inputs and outputs."""
137
+ """Generate a unit test (manual) or investigate a bug (agentic).
138
+
139
+ Agentic Mode (default):
140
+ pdd bug ISSUE_URL
141
+
142
+ Manual Mode:
143
+ pdd bug --manual PROMPT_FILE CODE_FILE PROGRAM_FILE CURRENT_OUTPUT DESIRED_OUTPUT
144
+ """
121
145
  try:
122
- result, total_cost, model_name = bug_main(
123
- ctx=ctx,
124
- prompt_file=prompt_file,
125
- code_file=code_file,
126
- program_file=program_file,
127
- current_output=current_output,
128
- desired_output=desired_output,
129
- output=output,
130
- language=language,
131
- )
132
- return result, total_cost, model_name
133
- except click.Abort:
146
+ if manual:
147
+ if len(args) != 5:
148
+ raise click.UsageError(
149
+ "Manual mode requires 5 arguments: PROMPT_FILE CODE_FILE PROGRAM_FILE CURRENT_OUTPUT DESIRED_OUTPUT"
150
+ )
151
+
152
+ # Validate files exist (replicating click.Path(exists=True))
153
+ for f in args:
154
+ if not os.path.exists(f):
155
+ raise click.UsageError(f"File does not exist: {f}")
156
+ if os.path.isdir(f):
157
+ raise click.UsageError(f"Path is a directory, not a file: {f}")
158
+
159
+ prompt_file, code_file, program_file, current_output, desired_output = args
160
+
161
+ result, total_cost, model_name = bug_main(
162
+ ctx=ctx,
163
+ prompt_file=prompt_file,
164
+ code_file=code_file,
165
+ program_file=program_file,
166
+ current_output=current_output,
167
+ desired_output=desired_output,
168
+ output=output,
169
+ language=language,
170
+ )
171
+ return result, total_cost, model_name
172
+
173
+ else:
174
+ # Agentic mode
175
+ if len(args) != 1:
176
+ raise click.UsageError("Agentic mode requires exactly one argument: the GitHub Issue URL.")
177
+
178
+ issue_url = args[0]
179
+
180
+ success, message, cost, model, changed_files = run_agentic_bug(
181
+ issue_url=issue_url,
182
+ verbose=ctx.obj.get("verbose", False),
183
+ quiet=ctx.obj.get("quiet", False),
184
+ timeout_adder=timeout_adder,
185
+ use_github_state=not no_github_state,
186
+ )
187
+
188
+ result_str = f"Success: {success}\nMessage: {message}\nChanged Files: {changed_files}"
189
+ return result_str, cost, model
190
+
191
+ except (click.Abort, click.ClickException):
134
192
  raise
135
193
  except Exception as exception:
136
194
  handle_error(exception, "bug", ctx.obj.get("quiet", False))
@@ -188,7 +246,7 @@ def crash(
188
246
  ) -> Optional[Tuple[str, float, str]]:
189
247
  """Analyze a crash and fix the code and program."""
190
248
  try:
191
- # crash_main returns: success, final_code, final_program, attempts, cost, model
249
+ # crash_main returns: success, final_code, final_program, attempts, total_cost, model_name
192
250
  success, final_code, final_program, attempts, total_cost, model_name = crash_main(
193
251
  ctx=ctx,
194
252
  prompt_file=prompt_file,
@@ -204,7 +262,7 @@ def crash(
204
262
  # Return a summary string as the result for track_cost/CLI output
205
263
  result = f"Success: {success}, Attempts: {attempts}"
206
264
  return result, total_cost, model_name
207
- except click.Abort:
265
+ except (click.Abort, click.ClickException):
208
266
  raise
209
267
  except Exception as exception:
210
268
  handle_error(exception, "crash", ctx.obj.get("quiet", False))
@@ -241,7 +299,7 @@ def trace(
241
299
  output=output,
242
300
  )
243
301
  return str(result), total_cost, model_name
244
- except click.Abort:
302
+ except (click.Abort, click.ClickException):
245
303
  raise
246
304
  except Exception as exception:
247
305
  handle_error(exception, "trace", ctx.obj.get("quiet", False))