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.
Files changed (195) hide show
  1. pdd/__init__.py +40 -8
  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 +598 -0
  7. pdd/agentic_crash.py +534 -0
  8. pdd/agentic_e2e_fix.py +319 -0
  9. pdd/agentic_e2e_fix_orchestrator.py +426 -0
  10. pdd/agentic_fix.py +1294 -0
  11. pdd/agentic_langtest.py +162 -0
  12. pdd/agentic_update.py +387 -0
  13. pdd/agentic_verify.py +183 -0
  14. pdd/architecture_sync.py +565 -0
  15. pdd/auth_service.py +210 -0
  16. pdd/auto_deps_main.py +71 -51
  17. pdd/auto_include.py +245 -5
  18. pdd/auto_update.py +125 -47
  19. pdd/bug_main.py +196 -23
  20. pdd/bug_to_unit_test.py +2 -0
  21. pdd/change_main.py +11 -4
  22. pdd/cli.py +22 -1181
  23. pdd/cmd_test_main.py +350 -150
  24. pdd/code_generator.py +60 -18
  25. pdd/code_generator_main.py +790 -57
  26. pdd/commands/__init__.py +48 -0
  27. pdd/commands/analysis.py +306 -0
  28. pdd/commands/auth.py +309 -0
  29. pdd/commands/connect.py +290 -0
  30. pdd/commands/fix.py +163 -0
  31. pdd/commands/generate.py +257 -0
  32. pdd/commands/maintenance.py +175 -0
  33. pdd/commands/misc.py +87 -0
  34. pdd/commands/modify.py +256 -0
  35. pdd/commands/report.py +144 -0
  36. pdd/commands/sessions.py +284 -0
  37. pdd/commands/templates.py +215 -0
  38. pdd/commands/utility.py +110 -0
  39. pdd/config_resolution.py +58 -0
  40. pdd/conflicts_main.py +8 -3
  41. pdd/construct_paths.py +589 -111
  42. pdd/context_generator.py +10 -2
  43. pdd/context_generator_main.py +175 -76
  44. pdd/continue_generation.py +53 -10
  45. pdd/core/__init__.py +33 -0
  46. pdd/core/cli.py +527 -0
  47. pdd/core/cloud.py +237 -0
  48. pdd/core/dump.py +554 -0
  49. pdd/core/errors.py +67 -0
  50. pdd/core/remote_session.py +61 -0
  51. pdd/core/utils.py +90 -0
  52. pdd/crash_main.py +262 -33
  53. pdd/data/language_format.csv +71 -63
  54. pdd/data/llm_model.csv +20 -18
  55. pdd/detect_change_main.py +5 -4
  56. pdd/docs/prompting_guide.md +864 -0
  57. pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
  58. pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
  59. pdd/fix_code_loop.py +523 -95
  60. pdd/fix_code_module_errors.py +6 -2
  61. pdd/fix_error_loop.py +491 -92
  62. pdd/fix_errors_from_unit_tests.py +4 -3
  63. pdd/fix_main.py +278 -21
  64. pdd/fix_verification_errors.py +12 -100
  65. pdd/fix_verification_errors_loop.py +529 -286
  66. pdd/fix_verification_main.py +294 -89
  67. pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
  68. pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
  69. pdd/frontend/dist/index.html +376 -0
  70. pdd/frontend/dist/logo.svg +33 -0
  71. pdd/generate_output_paths.py +139 -15
  72. pdd/generate_test.py +218 -146
  73. pdd/get_comment.py +19 -44
  74. pdd/get_extension.py +8 -9
  75. pdd/get_jwt_token.py +318 -22
  76. pdd/get_language.py +8 -7
  77. pdd/get_run_command.py +75 -0
  78. pdd/get_test_command.py +68 -0
  79. pdd/git_update.py +70 -19
  80. pdd/incremental_code_generator.py +2 -2
  81. pdd/insert_includes.py +13 -4
  82. pdd/llm_invoke.py +1711 -181
  83. pdd/load_prompt_template.py +19 -12
  84. pdd/path_resolution.py +140 -0
  85. pdd/pdd_completion.fish +25 -2
  86. pdd/pdd_completion.sh +30 -4
  87. pdd/pdd_completion.zsh +79 -4
  88. pdd/postprocess.py +14 -4
  89. pdd/preprocess.py +293 -24
  90. pdd/preprocess_main.py +41 -6
  91. pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
  92. pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
  93. pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
  94. pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
  95. pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
  96. pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
  97. pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
  98. pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
  99. pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
  100. pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
  101. pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
  102. pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
  103. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
  104. pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
  105. pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
  106. pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
  107. pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
  108. pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
  109. pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
  110. pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
  111. pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
  112. pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
  113. pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
  114. pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
  115. pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
  116. pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
  117. pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
  118. pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
  119. pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
  120. pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
  121. pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
  122. pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
  123. pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
  124. pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
  125. pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
  126. pdd/prompts/agentic_update_LLM.prompt +925 -0
  127. pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
  128. pdd/prompts/auto_include_LLM.prompt +122 -905
  129. pdd/prompts/change_LLM.prompt +3093 -1
  130. pdd/prompts/detect_change_LLM.prompt +686 -27
  131. pdd/prompts/example_generator_LLM.prompt +22 -1
  132. pdd/prompts/extract_code_LLM.prompt +5 -1
  133. pdd/prompts/extract_program_code_fix_LLM.prompt +7 -1
  134. pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
  135. pdd/prompts/extract_promptline_LLM.prompt +17 -11
  136. pdd/prompts/find_verification_errors_LLM.prompt +6 -0
  137. pdd/prompts/fix_code_module_errors_LLM.prompt +12 -2
  138. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +9 -0
  139. pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
  140. pdd/prompts/generate_test_LLM.prompt +41 -7
  141. pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
  142. pdd/prompts/increase_tests_LLM.prompt +1 -5
  143. pdd/prompts/insert_includes_LLM.prompt +316 -186
  144. pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
  145. pdd/prompts/prompt_diff_LLM.prompt +82 -0
  146. pdd/prompts/trace_LLM.prompt +25 -22
  147. pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
  148. pdd/prompts/update_prompt_LLM.prompt +22 -1
  149. pdd/pytest_output.py +127 -12
  150. pdd/remote_session.py +876 -0
  151. pdd/render_mermaid.py +236 -0
  152. pdd/server/__init__.py +52 -0
  153. pdd/server/app.py +335 -0
  154. pdd/server/click_executor.py +587 -0
  155. pdd/server/executor.py +338 -0
  156. pdd/server/jobs.py +661 -0
  157. pdd/server/models.py +241 -0
  158. pdd/server/routes/__init__.py +31 -0
  159. pdd/server/routes/architecture.py +451 -0
  160. pdd/server/routes/auth.py +364 -0
  161. pdd/server/routes/commands.py +929 -0
  162. pdd/server/routes/config.py +42 -0
  163. pdd/server/routes/files.py +603 -0
  164. pdd/server/routes/prompts.py +1322 -0
  165. pdd/server/routes/websocket.py +473 -0
  166. pdd/server/security.py +243 -0
  167. pdd/server/terminal_spawner.py +209 -0
  168. pdd/server/token_counter.py +222 -0
  169. pdd/setup_tool.py +648 -0
  170. pdd/simple_math.py +2 -0
  171. pdd/split_main.py +3 -2
  172. pdd/summarize_directory.py +237 -195
  173. pdd/sync_animation.py +8 -4
  174. pdd/sync_determine_operation.py +839 -112
  175. pdd/sync_main.py +351 -57
  176. pdd/sync_orchestration.py +1400 -756
  177. pdd/sync_tui.py +848 -0
  178. pdd/template_expander.py +161 -0
  179. pdd/template_registry.py +264 -0
  180. pdd/templates/architecture/architecture_json.prompt +237 -0
  181. pdd/templates/generic/generate_prompt.prompt +174 -0
  182. pdd/trace.py +168 -12
  183. pdd/trace_main.py +4 -3
  184. pdd/track_cost.py +140 -63
  185. pdd/unfinished_prompt.py +51 -4
  186. pdd/update_main.py +567 -67
  187. pdd/update_model_costs.py +2 -2
  188. pdd/update_prompt.py +19 -4
  189. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +29 -11
  190. pdd_cli-0.0.118.dist-info/RECORD +227 -0
  191. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +1 -1
  192. pdd_cli-0.0.45.dist-info/RECORD +0 -116
  193. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
  194. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
  195. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/top_level.txt +0 -0
pdd/agentic_crash.py ADDED
@@ -0,0 +1,534 @@
1
+ # pdd/agentic_crash.py
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import subprocess
6
+ from pathlib import Path
7
+ from typing import Any, Iterable, Mapping
8
+
9
+ from rich.console import Console
10
+
11
+ from .agentic_common import get_available_agents, run_agentic_task
12
+ from .get_run_command import get_run_command_for_file
13
+ from .load_prompt_template import load_prompt_template
14
+
15
+ console = Console()
16
+
17
+
18
+ def _require_existing_file(path: str | Path, description: str) -> Path:
19
+ """
20
+ Normalize and validate a file path.
21
+
22
+ Args:
23
+ path: Path-like or string pointing to a file.
24
+ description: Human-readable description used in error messages.
25
+
26
+ Returns:
27
+ The resolved Path instance.
28
+
29
+ Raises:
30
+ ValueError: If the path string is empty.
31
+ FileNotFoundError: If the file does not exist.
32
+ FileNotFoundError: If the path exists but is not a file.
33
+ TypeError: If `path` is not a str or Path.
34
+ """
35
+ if isinstance(path, str):
36
+ if not path.strip():
37
+ raise ValueError(f"{description} path is empty.")
38
+ p = Path(path.strip()).expanduser()
39
+ elif isinstance(path, Path):
40
+ p = path.expanduser()
41
+ else:
42
+ raise TypeError(
43
+ f"{description} must be a str or Path, got {type(path).__name__}."
44
+ )
45
+
46
+ if not p.exists():
47
+ raise FileNotFoundError(f"{description} does not exist: {p}")
48
+ if not p.is_file():
49
+ raise FileNotFoundError(f"{description} is not a file: {p}")
50
+
51
+ return p.resolve()
52
+
53
+
54
+ def _snapshot_mtimes(root: Path) -> dict[Path, float]:
55
+ """
56
+ Take a snapshot of modification times for all files under a root directory.
57
+
58
+ Args:
59
+ root: Directory to scan.
60
+
61
+ Returns:
62
+ Mapping from absolute file paths to their modification times.
63
+ """
64
+ mtimes: dict[Path, float] = {}
65
+ try:
66
+ for path in root.rglob("*"):
67
+ try:
68
+ if path.is_file():
69
+ mtimes[path.resolve()] = path.stat().st_mtime
70
+ except OSError:
71
+ # Ignore files we cannot stat
72
+ continue
73
+ except OSError:
74
+ # If the root cannot be traversed, just return empty
75
+ return {}
76
+ return mtimes
77
+
78
+
79
+ def _detect_changed_paths(
80
+ before: Mapping[Path, float],
81
+ after: Mapping[Path, float],
82
+ ) -> list[Path]:
83
+ """
84
+ Compute which files changed between two mtime snapshots.
85
+
86
+ A file is considered changed if:
87
+ - It existed before and its mtime changed, or
88
+ - It is new in the `after` snapshot, or
89
+ - It existed before but is missing in `after` (deleted).
90
+
91
+ Args:
92
+ before: Snapshot taken before the agent runs.
93
+ after: Snapshot taken after the agent runs.
94
+
95
+ Returns:
96
+ List of Paths that changed.
97
+ """
98
+ changed: list[Path] = []
99
+
100
+ # Modified or deleted files
101
+ for path, before_mtime in before.items():
102
+ after_mtime = after.get(path)
103
+ if after_mtime is None:
104
+ # Deleted
105
+ changed.append(path)
106
+ elif after_mtime != before_mtime:
107
+ # Modified
108
+ changed.append(path)
109
+
110
+ # New files
111
+ new_paths = set(after.keys()) - set(before.keys())
112
+ changed.extend(new_paths)
113
+
114
+ return changed
115
+
116
+
117
+ def _paths_to_relative_strings(paths: Iterable[Path], root: Path) -> list[str]:
118
+ """
119
+ Convert absolute paths to strings, relative to a root when possible.
120
+
121
+ Args:
122
+ paths: Iterable of Paths.
123
+ root: Root directory for relativization.
124
+
125
+ Returns:
126
+ List of string paths.
127
+ """
128
+ results: list[str] = []
129
+ for path in paths:
130
+ try:
131
+ rel = path.relative_to(root)
132
+ results.append(str(rel))
133
+ except ValueError:
134
+ # Path is outside root; return absolute
135
+ results.append(str(path))
136
+ return results
137
+
138
+
139
+ def _ensure_float(value: Any, default: float) -> float:
140
+ """
141
+ Safely convert a value to float with a default.
142
+
143
+ Args:
144
+ value: Value to convert.
145
+ default: Default value if conversion fails.
146
+
147
+ Returns:
148
+ Float value or the default.
149
+ """
150
+ try:
151
+ return float(value) # type: ignore[arg-type]
152
+ except (TypeError, ValueError):
153
+ return default
154
+
155
+
156
+ def _parse_agent_json(
157
+ raw_output: str,
158
+ *,
159
+ fallback_success: bool,
160
+ fallback_cost: float,
161
+ fallback_model: str | None,
162
+ verbose: bool,
163
+ quiet: bool,
164
+ ) -> tuple[bool, str, float, str | None, list[str]]:
165
+ """
166
+ Parse the JSON emitted by the agentic CLI.
167
+
168
+ The agent is expected to output a JSON string with fields:
169
+ - success: bool
170
+ - message: str
171
+ - cost: float
172
+ - model: str
173
+ - changed_files: list[str]
174
+
175
+ If parsing fails, fall back to the provided defaults.
176
+
177
+ Args:
178
+ raw_output: Raw stdout from the agent.
179
+ fallback_success: Success flag reported by run_agentic_task.
180
+ fallback_cost: Cost reported by run_agentic_task.
181
+ fallback_model: Provider/model reported by run_agentic_task.
182
+ verbose: Whether to log debug information.
183
+ quiet: Whether to suppress console output.
184
+
185
+ Returns:
186
+ Tuple of (success, message, cost, model, changed_files).
187
+ """
188
+ success: bool = fallback_success
189
+ message: str = raw_output.strip() or "Agentic CLI produced no output."
190
+ cost: float = fallback_cost
191
+ model: str | None = fallback_model
192
+ changed_files: list[str] = []
193
+
194
+ if not raw_output.strip():
195
+ return success, message, cost, model, changed_files
196
+
197
+ text = raw_output.strip()
198
+ data: Any | None = None
199
+
200
+ try:
201
+ data = json.loads(text)
202
+ except json.JSONDecodeError:
203
+ # Try to extract a JSON object substring if the output is mixed.
204
+ start = text.find("{")
205
+ end = text.rfind("}")
206
+ if start != -1 and end != -1 and end > start:
207
+ try:
208
+ data = json.loads(text[start : end + 1])
209
+ except json.JSONDecodeError:
210
+ data = None
211
+
212
+ if not isinstance(data, dict):
213
+ if verbose and not quiet:
214
+ console.print(
215
+ "[yellow]Warning:[/yellow] Agent output was not valid JSON; "
216
+ "using raw output as message."
217
+ )
218
+ return success, message, cost, model, changed_files
219
+
220
+ # success
221
+ if "success" in data:
222
+ success = bool(data["success"])
223
+
224
+ # message
225
+ if "message" in data and isinstance(data["message"], str):
226
+ message = data["message"].strip() or message
227
+
228
+ # cost
229
+ if "cost" in data:
230
+ cost = _ensure_float(data["cost"], cost)
231
+
232
+ # model
233
+ if "model" in data and data["model"] is not None:
234
+ model = str(data["model"])
235
+
236
+ # changed_files
237
+ raw_changed = data.get("changed_files")
238
+ if isinstance(raw_changed, list):
239
+ changed_files = [str(item) for item in raw_changed]
240
+
241
+ return success, message, cost, model, changed_files
242
+
243
+
244
+ def _run_program_file(
245
+ program_path: Path,
246
+ project_root: Path,
247
+ *,
248
+ verbose: bool,
249
+ quiet: bool,
250
+ ) -> tuple[bool, str]:
251
+ """
252
+ Run the program file to verify that the crash has been fixed.
253
+
254
+ Uses pdd.get_run_command.get_run_command_for_file to determine the
255
+ appropriate run command.
256
+
257
+ Args:
258
+ program_path: Path to the program file that previously crashed.
259
+ project_root: Directory to use as the working directory.
260
+ verbose: Whether to log detailed execution information.
261
+ quiet: Whether to suppress console output.
262
+
263
+ Returns:
264
+ Tuple (success, output_or_error_message).
265
+ """
266
+ command = get_run_command_for_file(str(program_path))
267
+
268
+ if not command:
269
+ msg = (
270
+ f"No run command configured for program file '{program_path}'. "
271
+ "Unable to verify crash fix."
272
+ )
273
+ if not quiet:
274
+ console.print(f"[red]{msg}[/red]")
275
+ return False, msg
276
+
277
+ if verbose and not quiet:
278
+ console.print(
279
+ "[cyan]Verifying crash fix by running:[/cyan] "
280
+ f"[white]{command}[/white] (cwd={project_root})"
281
+ )
282
+
283
+ try:
284
+ result = subprocess.run(
285
+ command,
286
+ shell=True,
287
+ cwd=project_root,
288
+ capture_output=True,
289
+ text=True,
290
+ )
291
+ except OSError as exc:
292
+ msg = f"Failed to execute program '{program_path}': {exc}"
293
+ if not quiet:
294
+ console.print(f"[red]{msg}[/red]")
295
+ return False, msg
296
+
297
+ stdout = result.stdout or ""
298
+ stderr = result.stderr or ""
299
+ combined_output = (stdout + ("\n" + stderr if stderr else "")).strip()
300
+
301
+ if result.returncode == 0:
302
+ if verbose and not quiet:
303
+ console.print("[green]Program run succeeded without crashing.[/green]")
304
+ if combined_output:
305
+ console.print("[green]Program output:[/green]")
306
+ console.print(combined_output)
307
+ return True, combined_output or "Program completed successfully."
308
+ else:
309
+ msg = (
310
+ f"Program '{program_path}' exited with status {result.returncode}.\n"
311
+ f"{combined_output}"
312
+ ).strip()
313
+ if not quiet:
314
+ console.print("[red]Verification run failed.[/red]")
315
+ if verbose and combined_output:
316
+ console.print(combined_output)
317
+ return False, msg
318
+
319
+
320
+ def run_agentic_crash(
321
+ prompt_file: str | Path,
322
+ code_file: str | Path,
323
+ program_file: str | Path,
324
+ crash_log_file: str | Path,
325
+ *,
326
+ verbose: bool = False,
327
+ quiet: bool = False,
328
+ ) -> tuple[bool, str, float, str | None, list[str]]:
329
+ """
330
+ Agentic fallback for the PDD crash command.
331
+
332
+ When the normal LLM-based crash loop fails, this function delegates to an
333
+ agentic CLI with full codebase exploration capability. It runs in a
334
+ single-pass "explore" mode and then re-runs the program file to verify
335
+ whether the crash has been fixed.
336
+
337
+ The function:
338
+
339
+ 1. Loads the `agentic_crash_explore_LLM` prompt template.
340
+ 2. Records file mtimes under the project root before running the agent.
341
+ 3. Invokes the agent in explore mode (single pass).
342
+ 4. Parses the agent's JSON output:
343
+ - success (bool)
344
+ - message (str)
345
+ - cost (float)
346
+ - model (str)
347
+ - changed_files (list[str])
348
+ 5. Detects actual file changes based on mtimes and merges them with the
349
+ agent-reported changed_files.
350
+ 6. Runs the program file to verify the fix.
351
+ 7. Returns a 5-tuple:
352
+ (success, message, cost, model, changed_files)
353
+
354
+ Args:
355
+ prompt_file: Path to the prompt file (source of truth).
356
+ code_file: Path to the generated code file that may need fixing.
357
+ program_file: Path to the program that crashed.
358
+ crash_log_file: Path to a log containing the crash traceback and
359
+ previous fix attempts.
360
+ verbose: If True, emit detailed logs.
361
+ quiet: If True, suppress console output (overrides `verbose`).
362
+
363
+ Returns:
364
+ Tuple (success, message, cost, model, changed_files):
365
+ success: Overall success after verification (agent + program run).
366
+ message: Human-readable summary including any error details.
367
+ cost: Estimated LLM cost reported by the agent (or 0.0 on failure).
368
+ model: Model/provider name used by the agent, if available.
369
+ changed_files: Unique list of changed files (relative to project
370
+ root when possible).
371
+ """
372
+ # Normalize and validate inputs
373
+ try:
374
+ prompt_path = _require_existing_file(prompt_file, "Prompt file")
375
+ code_path = _require_existing_file(code_file, "Code file")
376
+ program_path = _require_existing_file(program_file, "Program file")
377
+ except (ValueError, FileNotFoundError, TypeError) as exc:
378
+ msg = f"Agentic crash fallback aborted due to invalid input: {exc}"
379
+ if not quiet:
380
+ console.print(f"[red]{msg}[/red]")
381
+ return False, msg, 0.0, None, []
382
+
383
+ crash_log_text: str = ""
384
+ crash_log_path = Path(crash_log_file).expanduser()
385
+ try:
386
+ if crash_log_path.exists() and crash_log_path.is_file():
387
+ crash_log_text = crash_log_path.read_text(encoding="utf-8")
388
+ elif verbose and not quiet:
389
+ console.print(
390
+ f"[yellow]Warning:[/yellow] Crash log file '{crash_log_path}' "
391
+ "does not exist or is not a regular file."
392
+ )
393
+ except OSError as exc:
394
+ if verbose and not quiet:
395
+ console.print(
396
+ f"[yellow]Warning:[/yellow] Could not read crash log file "
397
+ f"'{crash_log_path}': {exc}"
398
+ )
399
+
400
+ project_root = prompt_path.parent
401
+
402
+ if verbose and not quiet:
403
+ console.print("[cyan]Starting agentic crash fallback (explore mode)...[/cyan]")
404
+ console.print(f"Prompt file : [white]{prompt_path}[/white]")
405
+ console.print(f"Code file : [white]{code_path}[/white]")
406
+ console.print(f"Program file: [white]{program_path}[/white]")
407
+ console.print(f"Project root: [white]{project_root}[/white]")
408
+
409
+ # Check for available agent providers
410
+ agents = get_available_agents()
411
+ if not agents:
412
+ msg = (
413
+ "No agentic CLI providers detected. "
414
+ "Ensure the appropriate CLI tools are installed and API keys are set."
415
+ )
416
+ if not quiet:
417
+ console.print(f"[red]{msg}[/red]")
418
+ return False, msg, 0.0, None, []
419
+
420
+ if verbose and not quiet:
421
+ console.print(
422
+ "[green]Available agent providers:[/green] "
423
+ + ", ".join(str(a) for a in agents)
424
+ )
425
+
426
+ # Load prompt template
427
+ template = load_prompt_template("agentic_crash_explore_LLM")
428
+ if not template:
429
+ msg = "Failed to load prompt template 'agentic_crash_explore_LLM'."
430
+ if not quiet:
431
+ console.print(f"[red]{msg}[/red]")
432
+ return False, msg, 0.0, None, []
433
+
434
+ # Prepare instruction for the agent (explore mode only).
435
+ previous_attempts = crash_log_text.strip() or "No previous attempts available."
436
+ try:
437
+ instruction = template.format(
438
+ prompt_path=str(prompt_path),
439
+ code_path=str(code_path),
440
+ program_path=str(program_path),
441
+ project_root=str(project_root),
442
+ previous_attempts=previous_attempts,
443
+ )
444
+ except Exception as exc:
445
+ msg = f"Error formatting agent prompt template: {exc}"
446
+ if not quiet:
447
+ console.print(f"[red]{msg}[/red]")
448
+ return False, msg, 0.0, None, []
449
+
450
+ # 1) Snapshot file mtimes before the agent runs
451
+ before_mtimes = _snapshot_mtimes(project_root)
452
+
453
+ # 2) Run the agent (single-pass, explore mode implied by the prompt)
454
+ try:
455
+ agent_cli_success, raw_output, base_cost, provider_used = run_agentic_task(
456
+ instruction=instruction,
457
+ cwd=project_root,
458
+ verbose=verbose,
459
+ quiet=quiet,
460
+ label="agentic_crash_explore",
461
+ )
462
+ except Exception as exc: # noqa: BLE001
463
+ msg = f"Agentic CLI invocation failed: {exc}"
464
+ if not quiet:
465
+ console.print(f"[red]{msg}[/red]")
466
+ # No JSON to parse; no changes we can reliably detect beyond this point.
467
+ return False, msg, 0.0, None, []
468
+
469
+ # 3) Snapshot mtimes after the agent completes
470
+ after_mtimes = _snapshot_mtimes(project_root)
471
+ changed_paths_by_mtime = _detect_changed_paths(before_mtimes, after_mtimes)
472
+ changed_files_from_fs = _paths_to_relative_strings(changed_paths_by_mtime, project_root)
473
+
474
+ # 4) Parse JSON emitted by the agent to get structured information
475
+ agent_success, agent_message, agent_cost, agent_model, agent_changed_files = (
476
+ _parse_agent_json(
477
+ raw_output,
478
+ fallback_success=agent_cli_success,
479
+ fallback_cost=base_cost,
480
+ fallback_model=provider_used,
481
+ verbose=verbose,
482
+ quiet=quiet,
483
+ )
484
+ )
485
+
486
+ # Merge changed_files from JSON and filesystem mtimes
487
+ all_changed_files_set = set(agent_changed_files)
488
+ all_changed_files_set.update(changed_files_from_fs)
489
+ all_changed_files = sorted(all_changed_files_set)
490
+
491
+ # 5) Run the program file after the agent's fix attempt to verify the fix
492
+ program_success, program_message = _run_program_file(
493
+ program_path=program_path,
494
+ project_root=project_root,
495
+ verbose=verbose,
496
+ quiet=quiet,
497
+ )
498
+
499
+ # Combine agent's view of success with verification result
500
+ overall_success = bool(agent_success) and bool(program_success)
501
+
502
+ if program_success:
503
+ # Verification succeeded
504
+ if agent_message:
505
+ combined_message = agent_message
506
+ else:
507
+ combined_message = "Agentic crash fix appears successful; program ran without crashing."
508
+ else:
509
+ # Verification failed; append details
510
+ verification_info = (
511
+ "Verification run failed; the program still crashes or exits with an error."
512
+ )
513
+ if agent_message:
514
+ combined_message = (
515
+ agent_message.rstrip()
516
+ + "\n\n"
517
+ + verification_info
518
+ + "\n\nVerification details:\n"
519
+ + program_message
520
+ )
521
+ else:
522
+ combined_message = verification_info + "\n\n" + program_message
523
+
524
+ if verbose and not quiet:
525
+ status_color = "green" if overall_success else "red"
526
+ console.print(
527
+ f"[{status_color}]Agentic crash fallback completed. "
528
+ f"Success={overall_success}[/]"
529
+ )
530
+
531
+ # If the agent did not provide a model name, fall back to provider_used
532
+ final_model = agent_model or provider_used
533
+
534
+ return overall_success, combined_message, agent_cost, final_model, all_changed_files