pdd-cli 0.0.42__py3-none-any.whl → 0.0.90__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 (119) hide show
  1. pdd/__init__.py +4 -4
  2. pdd/agentic_common.py +863 -0
  3. pdd/agentic_crash.py +534 -0
  4. pdd/agentic_fix.py +1179 -0
  5. pdd/agentic_langtest.py +162 -0
  6. pdd/agentic_update.py +370 -0
  7. pdd/agentic_verify.py +183 -0
  8. pdd/auto_deps_main.py +15 -5
  9. pdd/auto_include.py +63 -5
  10. pdd/bug_main.py +3 -2
  11. pdd/bug_to_unit_test.py +2 -0
  12. pdd/change_main.py +11 -4
  13. pdd/cli.py +22 -1181
  14. pdd/cmd_test_main.py +80 -19
  15. pdd/code_generator.py +58 -18
  16. pdd/code_generator_main.py +672 -25
  17. pdd/commands/__init__.py +42 -0
  18. pdd/commands/analysis.py +248 -0
  19. pdd/commands/fix.py +140 -0
  20. pdd/commands/generate.py +257 -0
  21. pdd/commands/maintenance.py +174 -0
  22. pdd/commands/misc.py +79 -0
  23. pdd/commands/modify.py +230 -0
  24. pdd/commands/report.py +144 -0
  25. pdd/commands/templates.py +215 -0
  26. pdd/commands/utility.py +110 -0
  27. pdd/config_resolution.py +58 -0
  28. pdd/conflicts_main.py +8 -3
  29. pdd/construct_paths.py +281 -81
  30. pdd/context_generator.py +10 -2
  31. pdd/context_generator_main.py +113 -11
  32. pdd/continue_generation.py +47 -7
  33. pdd/core/__init__.py +0 -0
  34. pdd/core/cli.py +503 -0
  35. pdd/core/dump.py +554 -0
  36. pdd/core/errors.py +63 -0
  37. pdd/core/utils.py +90 -0
  38. pdd/crash_main.py +44 -11
  39. pdd/data/language_format.csv +71 -62
  40. pdd/data/llm_model.csv +20 -18
  41. pdd/detect_change_main.py +5 -4
  42. pdd/fix_code_loop.py +331 -77
  43. pdd/fix_error_loop.py +209 -60
  44. pdd/fix_errors_from_unit_tests.py +4 -3
  45. pdd/fix_main.py +75 -18
  46. pdd/fix_verification_errors.py +12 -100
  47. pdd/fix_verification_errors_loop.py +319 -272
  48. pdd/fix_verification_main.py +57 -17
  49. pdd/generate_output_paths.py +93 -10
  50. pdd/generate_test.py +16 -5
  51. pdd/get_jwt_token.py +48 -9
  52. pdd/get_run_command.py +73 -0
  53. pdd/get_test_command.py +68 -0
  54. pdd/git_update.py +70 -19
  55. pdd/increase_tests.py +7 -0
  56. pdd/incremental_code_generator.py +2 -2
  57. pdd/insert_includes.py +11 -3
  58. pdd/llm_invoke.py +1278 -110
  59. pdd/load_prompt_template.py +36 -10
  60. pdd/pdd_completion.fish +25 -2
  61. pdd/pdd_completion.sh +30 -4
  62. pdd/pdd_completion.zsh +79 -4
  63. pdd/postprocess.py +10 -3
  64. pdd/preprocess.py +228 -15
  65. pdd/preprocess_main.py +8 -5
  66. pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
  67. pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
  68. pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
  69. pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
  70. pdd/prompts/agentic_update_LLM.prompt +1071 -0
  71. pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
  72. pdd/prompts/auto_include_LLM.prompt +98 -101
  73. pdd/prompts/change_LLM.prompt +1 -3
  74. pdd/prompts/detect_change_LLM.prompt +562 -3
  75. pdd/prompts/example_generator_LLM.prompt +22 -1
  76. pdd/prompts/extract_code_LLM.prompt +5 -1
  77. pdd/prompts/extract_program_code_fix_LLM.prompt +14 -2
  78. pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
  79. pdd/prompts/extract_promptline_LLM.prompt +17 -11
  80. pdd/prompts/find_verification_errors_LLM.prompt +6 -0
  81. pdd/prompts/fix_code_module_errors_LLM.prompt +16 -4
  82. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +6 -41
  83. pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
  84. pdd/prompts/generate_test_LLM.prompt +21 -6
  85. pdd/prompts/increase_tests_LLM.prompt +1 -2
  86. pdd/prompts/insert_includes_LLM.prompt +1181 -6
  87. pdd/prompts/split_LLM.prompt +1 -62
  88. pdd/prompts/trace_LLM.prompt +25 -22
  89. pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
  90. pdd/prompts/update_prompt_LLM.prompt +22 -1
  91. pdd/prompts/xml_convertor_LLM.prompt +3246 -7
  92. pdd/pytest_output.py +188 -21
  93. pdd/python_env_detector.py +151 -0
  94. pdd/render_mermaid.py +236 -0
  95. pdd/setup_tool.py +648 -0
  96. pdd/simple_math.py +2 -0
  97. pdd/split_main.py +3 -2
  98. pdd/summarize_directory.py +56 -7
  99. pdd/sync_determine_operation.py +918 -186
  100. pdd/sync_main.py +82 -32
  101. pdd/sync_orchestration.py +1456 -453
  102. pdd/sync_tui.py +848 -0
  103. pdd/template_registry.py +264 -0
  104. pdd/templates/architecture/architecture_json.prompt +242 -0
  105. pdd/templates/generic/generate_prompt.prompt +174 -0
  106. pdd/trace.py +168 -12
  107. pdd/trace_main.py +4 -3
  108. pdd/track_cost.py +151 -61
  109. pdd/unfinished_prompt.py +49 -3
  110. pdd/update_main.py +549 -67
  111. pdd/update_model_costs.py +2 -2
  112. pdd/update_prompt.py +19 -4
  113. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/METADATA +20 -7
  114. pdd_cli-0.0.90.dist-info/RECORD +153 -0
  115. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/licenses/LICENSE +1 -1
  116. pdd_cli-0.0.42.dist-info/RECORD +0 -115
  117. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/WHEEL +0 -0
  118. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/entry_points.txt +0 -0
  119. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,162 @@
1
+ from __future__ import annotations
2
+ import os
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+
7
+ def _which(cmd: str) -> bool:
8
+ return shutil.which(cmd) is not None
9
+
10
+ def _find_project_root(start_path: str, boundary: Path | None = None) -> Path:
11
+ """
12
+ Find the project root by searching for common project files.
13
+
14
+ Args:
15
+ start_path: The path to start searching from (typically a test file).
16
+ boundary: Optional boundary path that the search will not traverse above.
17
+ Defaults to the current working directory to prevent escaping
18
+ into parent projects.
19
+
20
+ Returns:
21
+ The path to the project root, or the start_path's parent directory if
22
+ no project root markers are found within the boundary.
23
+ """
24
+ if boundary is None:
25
+ boundary = Path.cwd().resolve()
26
+ else:
27
+ boundary = boundary.resolve()
28
+
29
+ p = Path(start_path).resolve()
30
+ start_parent = p.parent # Fallback if nothing found
31
+
32
+ while p != p.parent:
33
+ # Stop if we've reached or passed the boundary
34
+ if p == boundary or boundary not in p.parents:
35
+ # Check boundary itself before giving up
36
+ if any((boundary / f).exists() for f in ["build.gradle", "build.gradle.kts", "pom.xml", "package.json", "jest.config.js"]):
37
+ return boundary
38
+ break
39
+
40
+ if any((p / f).exists() for f in ["build.gradle", "build.gradle.kts", "pom.xml", "package.json", "jest.config.js"]):
41
+ return p
42
+ p = p.parent
43
+
44
+ return start_parent
45
+
46
+ def default_verify_cmd_for(lang: str, unit_test_file: str) -> str | None:
47
+ """
48
+ Return a conservative shell command (bash -lc) that compiles/tests
49
+ and exits 0 on success. Users can override with PDD_AGENTIC_VERIFY_CMD.
50
+ """
51
+ test_rel = unit_test_file
52
+ lang = lang.lower()
53
+ if lang == "python":
54
+ return f'{os.sys.executable} -m pytest "{test_rel}" -q'
55
+
56
+
57
+ if lang == "javascript" or lang == "typescript":
58
+ example_dir = str(_find_project_root(unit_test_file))
59
+ rel_test_path = os.path.relpath(unit_test_file, example_dir)
60
+ return (
61
+ "set -e\n"
62
+ f'cd "{example_dir}" && '
63
+ "command -v npm >/dev/null 2>&1 || { echo 'npm missing'; exit 127; } && "
64
+ "if [ -f package.json ]; then "
65
+ " npm install && npm test; "
66
+ "else "
67
+ f' echo "No package.json in {example_dir}; running test file directly"; '
68
+ f' node -e "try {{ require(\'./{rel_test_path}\'); }} catch (e) {{ console.error(e); process.exit(1); }}"; '
69
+ "fi"
70
+ )
71
+
72
+ if lang == "java":
73
+ # detect maven or gradle?
74
+ root_dir = str(_find_project_root(unit_test_file))
75
+ if "pom.xml" in os.listdir(root_dir):
76
+ return (f"cd '{root_dir}' && mvn test")
77
+ elif "build.gradle" in os.listdir(root_dir) or "build.gradle.kts" in os.listdir(root_dir):
78
+ if "gradlew" in os.listdir(root_dir):
79
+ return f"cd '{root_dir}' && ./gradlew test"
80
+ else:
81
+ return f"cd `{root_dir}` gradle test"
82
+ else:
83
+ return None
84
+
85
+ # if lang == "cpp":
86
+ # # very lightweight: if *_test*.c* exists, build & run; otherwise compile sources only
87
+ # import shutil
88
+ # compiler = shutil.which("g++") or shutil.which("clang++")
89
+ # if compiler is None:
90
+ # # You can still return a generic command (will be accompanied by missing_tool_hints)
91
+ # compiler = "g++"
92
+ # # Example: simple build+smoke or test compile; adapt to your scheme
93
+ # return (
94
+ # 'set -e\n'
95
+ # f'cd "{current_working_directory}" && '
96
+ # 'if ls tests/*.cpp >/dev/null 2>&1; then '
97
+ # f'mkdir -p build && {compiler} -std=c++17 tests/*.cpp src/*.c* -o build/tests && ./build/tests; '
98
+ # 'else '
99
+ # "echo 'No C++ tests found; building sources only'; "
100
+ # f'mkdir -p build && {compiler} -std=c++17 -c src/*.c* -o build/obj.o; '
101
+ # 'fi'
102
+ # )
103
+
104
+ return None
105
+
106
+ def missing_tool_hints(lang: str, verify_cmd: str | None, project_root: Path) -> str | None:
107
+ """
108
+ If a required tool looks missing, return a one-time guidance string.
109
+ We do not install automatically; we just hint.
110
+ """
111
+ if not verify_cmd:
112
+ return None
113
+
114
+ need = []
115
+ if lang in ("typescript", "javascript"):
116
+ if not _which("npm"):
117
+ need.append("npm (Node.js)")
118
+ if lang == "java":
119
+ if not _which("javac") or not _which("java"):
120
+ need.append("Java JDK (javac, java)")
121
+ jar_present = any(
122
+ p.name.endswith(".jar") and "junit" in p.name.lower() and "console" in p.name.lower()
123
+ for p in project_root.glob("*.jar")
124
+ )
125
+ if not jar_present:
126
+ need.append("JUnit ConsoleLauncher jar (e.g. junit-platform-console-standalone.jar)")
127
+ if lang == "cpp":
128
+ if not _which("g++"):
129
+ need.append("g++")
130
+
131
+ if not need:
132
+ return None
133
+
134
+ install_lines = []
135
+ if "npm (Node.js)" in need:
136
+ install_lines += [
137
+ "macOS: brew install node",
138
+ "Ubuntu: sudo apt-get update && sudo apt-get install -y nodejs npm",
139
+ ]
140
+ if "Java JDK (javac, java)" in need:
141
+ install_lines += [
142
+ "macOS: brew install openjdk",
143
+ "Ubuntu: sudo apt-get update && sudo apt-get install -y openjdk-17-jdk",
144
+ ]
145
+ if "JUnit ConsoleLauncher jar (e.g. junit-platform-console-standalone.jar)" in need:
146
+ install_lines += [
147
+ "Download the ConsoleLauncher jar from Maven Central and place it in your project root, e.g.:",
148
+ " curl -LO https://repo1.maven.org/maven2/org/junit/platform/junit-platform-console-standalone/1.10.2/junit-platform-console-standalone-1.10.2.jar",
149
+ ]
150
+ if "g++" in need:
151
+ install_lines += [
152
+ "macOS: xcode-select --install # or: brew install gcc",
153
+ "Ubuntu: sudo apt-get update && sudo apt-get install -y build-essential",
154
+ ]
155
+
156
+ return (
157
+ "[yellow]Some tools required to run non-Python tests seem missing.[/yellow]\n - "
158
+ + "\n - ".join(need)
159
+ + "\n[dim]Suggested installs:\n "
160
+ + "\n ".join(install_lines)
161
+ + "[/dim]"
162
+ )
pdd/agentic_update.py ADDED
@@ -0,0 +1,370 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Agentic prompt update utilities.
5
+
6
+ This module coordinates an "agentic" update of a prompt file using an external
7
+ CLI agent (Claude, Gemini, Codex, etc.). It prepares a task instruction from
8
+ a prompt template, discovers relevant test files, runs the agent, and reports
9
+ whether the prompt file was modified, along with cost and provider details.
10
+ """
11
+
12
+ from pathlib import Path
13
+ from typing import Dict, Iterable, List, Optional, Sequence, Tuple
14
+
15
+ import os
16
+ import traceback
17
+
18
+ from rich.console import Console
19
+
20
+ from .agentic_common import get_available_agents, run_agentic_task
21
+ from .load_prompt_template import load_prompt_template
22
+
23
+ # Optional globals from package root; ignore if not present.
24
+ try: # pragma: no cover - purely optional integration
25
+ from . import DEFAULT_STRENGTH, DEFAULT_TIME # type: ignore[unused-ignore]
26
+ except Exception: # pragma: no cover - defensive import
27
+ DEFAULT_STRENGTH = None # type: ignore[assignment]
28
+ DEFAULT_TIME = None # type: ignore[assignment]
29
+
30
+ console = Console()
31
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
32
+
33
+ __all__ = ["run_agentic_update"]
34
+
35
+
36
+ def _relativize(path: Path, base: Path) -> str:
37
+ """
38
+ Format a path as relative to a base directory when possible, otherwise absolute.
39
+
40
+ Args:
41
+ path: Path to format.
42
+ base: Base directory to relativize against.
43
+
44
+ Returns:
45
+ String representation of the path (relative if possible, else absolute).
46
+ """
47
+ try:
48
+ return str(path.resolve().relative_to(base.resolve()))
49
+ except Exception:
50
+ return str(path.resolve())
51
+
52
+
53
+ def _snapshot_mtimes(paths: Iterable[Path]) -> Dict[Path, float]:
54
+ """
55
+ Take a snapshot of modification times for a set of files.
56
+
57
+ Args:
58
+ paths: Iterable of file paths to inspect.
59
+
60
+ Returns:
61
+ Mapping from existing file paths to their last modification timestamps.
62
+ """
63
+ mtimes: Dict[Path, float] = {}
64
+ for path in paths:
65
+ try:
66
+ if path.is_file():
67
+ mtimes[path] = os.path.getmtime(path)
68
+ except OSError:
69
+ # If we cannot read mtime, skip the file rather than failing the run.
70
+ continue
71
+ return mtimes
72
+
73
+
74
+ def _detect_changed_files(
75
+ before: Dict[Path, float],
76
+ after: Dict[Path, float],
77
+ ) -> List[Path]:
78
+ """
79
+ Compute which files changed between two mtime snapshots.
80
+
81
+ A file is considered changed if:
82
+ - It existed before and after and the mtime increased, or
83
+ - It was newly created, or
84
+ - It was deleted.
85
+
86
+ Args:
87
+ before: Mapping of paths to mtimes before the task.
88
+ after: Mapping of paths to mtimes after the task.
89
+
90
+ Returns:
91
+ Sorted list of changed file paths.
92
+ """
93
+ changed: List[Path] = []
94
+ all_paths = set(before.keys()) | set(after.keys())
95
+
96
+ for path in all_paths:
97
+ before_ts = before.get(path)
98
+ after_ts = after.get(path)
99
+
100
+ if before_ts is None and after_ts is not None:
101
+ changed.append(path)
102
+ elif before_ts is not None and after_ts is None:
103
+ changed.append(path)
104
+ elif before_ts is not None and after_ts is not None and after_ts > before_ts:
105
+ changed.append(path)
106
+
107
+ return sorted({p.resolve() for p in changed})
108
+
109
+
110
+ def _discover_test_files(code_path: Path) -> List[Path]:
111
+ """
112
+ Discover test files associated with a given code file.
113
+
114
+ Uses pattern: ``test_{code_stem}*{code_suffix}`` and searches in:
115
+ 1. ``tests/`` relative to the code file directory
116
+ 2. The same directory as the code file
117
+ 3. Project root ``tests/``
118
+
119
+ Args:
120
+ code_path: Path to the main code file.
121
+
122
+ Returns:
123
+ Ordered list of discovered test file paths (deduplicated).
124
+ """
125
+ code_path = code_path.resolve()
126
+ stem = code_path.stem
127
+ suffix = code_path.suffix
128
+ pattern = f"test_{stem}*{suffix}"
129
+
130
+ search_dirs: List[Path] = [
131
+ code_path.parent / "tests",
132
+ code_path.parent,
133
+ PROJECT_ROOT / "tests",
134
+ ]
135
+
136
+ seen: set[Path] = set()
137
+ discovered: List[Path] = []
138
+
139
+ for directory in search_dirs:
140
+ if not directory.is_dir():
141
+ continue
142
+ for path in sorted(directory.glob(pattern)):
143
+ resolved = path.resolve()
144
+ if resolved not in seen and resolved.is_file():
145
+ seen.add(resolved)
146
+ discovered.append(resolved)
147
+
148
+ return discovered
149
+
150
+
151
+ def _normalize_explicit_tests(
152
+ test_files: Sequence[Path],
153
+ ) -> Tuple[Optional[List[Path]], Optional[str]]:
154
+ """
155
+ Normalize and validate an explicit list of test files.
156
+
157
+ Args:
158
+ test_files: Sequence of paths provided by the caller.
159
+
160
+ Returns:
161
+ (normalized_tests, error_message)
162
+ - normalized_tests: List of resolved Path objects if all exist; None on error.
163
+ - error_message: Description if any test file is missing; None if OK.
164
+ """
165
+ normalized: List[Path] = []
166
+ missing: List[str] = []
167
+
168
+ for tf in test_files:
169
+ path = Path(tf).expanduser().resolve()
170
+ if path.is_file():
171
+ normalized.append(path)
172
+ else:
173
+ missing.append(str(path))
174
+
175
+ if missing:
176
+ return None, f"Test file(s) not found: {', '.join(missing)}"
177
+
178
+ return normalized, None
179
+
180
+
181
+ def run_agentic_update(
182
+ prompt_file: str,
183
+ code_file: str,
184
+ test_files: Optional[List[Path]] = None,
185
+ *,
186
+ verbose: bool = False,
187
+ quiet: bool = False,
188
+ ) -> Tuple[bool, str, float, str, List[str]]:
189
+ """
190
+ Run an agentic update on a prompt file using an external LLM CLI agent.
191
+
192
+ The function:
193
+ 1. Validates inputs and agent availability.
194
+ 2. Discovers relevant test files (if not explicitly provided).
195
+ 3. Loads the ``agentic_update_LLM`` prompt template and formats it with
196
+ ``prompt_path``, ``code_path`` and ``test_paths``.
197
+ 4. Invokes :func:`run_agentic_task` from ``agentic_common``.
198
+ 5. Detects changed files via mtime comparison.
199
+ 6. Returns success if and only if the prompt file was modified.
200
+
201
+ Args:
202
+ prompt_file: Path to the prompt file to be updated.
203
+ code_file: Path to the primary code file the prompt concerns.
204
+ test_files: Optional explicit list of test file paths. When ``None``,
205
+ test files are auto-discovered using the pattern
206
+ ``test_{code_stem}*{code_suffix}`` in the configured search
207
+ locations.
208
+ verbose: If True, enable verbose logging for the underlying agent task.
209
+ quiet: If True, suppress informational logging from this function.
210
+ (Passed through to the agent as well; ``quiet`` takes precedence
211
+ over ``verbose`` for this wrapper's own logging.)
212
+
213
+ Returns:
214
+ A 5-tuple:
215
+ (success, message, cost, model_used, changed_files)
216
+
217
+ Where:
218
+ - success: ``True`` iff the prompt file was modified.
219
+ - message: Human-readable summary of what happened.
220
+ - cost: Estimated cost reported by the agent (0.0 on early failure).
221
+ - model_used: Identifier of the model/agent used, if any.
222
+ - changed_files: List of changed files (as string paths).
223
+
224
+ Notes:
225
+ If no agent CLI is available, returns:
226
+ (False, "No agentic CLI available", 0.0, "", [])
227
+ """
228
+ # Resolve core paths
229
+ prompt_path = Path(prompt_file).expanduser().resolve()
230
+ code_path = Path(code_file).expanduser().resolve()
231
+
232
+ # Basic input validation
233
+ if not prompt_path.is_file():
234
+ message = f"Prompt file not found: {prompt_path}"
235
+ if not quiet:
236
+ console.print(f"[red]{message}[/red]")
237
+ return False, message, 0.0, "", []
238
+
239
+ if not code_path.is_file():
240
+ message = f"Code file not found: {code_path}"
241
+ if not quiet:
242
+ console.print(f"[red]{message}[/red]")
243
+ return False, message, 0.0, "", []
244
+
245
+ # Check agent availability
246
+ try:
247
+ agents: Sequence[str] = get_available_agents()
248
+ except Exception as exc: # Defensive; get_available_agents should be robust
249
+ message = f"Failed to check agent availability: {exc}"
250
+ if not quiet:
251
+ console.print(f"[red]{message}[/red]")
252
+ if verbose:
253
+ console.print(traceback.format_exc())
254
+ return False, message, 0.0, "", []
255
+
256
+ if not agents:
257
+ message = "No agentic CLI available"
258
+ if not quiet:
259
+ console.print(f"[yellow]{message}[/yellow]")
260
+ return False, message, 0.0, "", []
261
+
262
+ # Determine which tests to pass into the prompt
263
+ if test_files is not None:
264
+ normalized_tests, error = _normalize_explicit_tests(test_files)
265
+ if error is not None:
266
+ if not quiet:
267
+ console.print(f"[red]{error}[/red]")
268
+ return False, error, 0.0, "", []
269
+ selected_tests = normalized_tests or []
270
+ else:
271
+ selected_tests = _discover_test_files(code_path)
272
+
273
+ # Paths to track *before* running the agent (for mtime comparison)
274
+ before_paths: set[Path] = {prompt_path.resolve(), code_path.resolve()}
275
+ before_paths.update(p.resolve() for p in selected_tests)
276
+
277
+ before_mtimes = _snapshot_mtimes(before_paths)
278
+
279
+ # Load and format the prompt template
280
+ try:
281
+ template = load_prompt_template("agentic_update_LLM")
282
+ except Exception as exc:
283
+ message = f"Error while loading prompt template 'agentic_update_LLM': {exc}"
284
+ if not quiet:
285
+ console.print(f"[red]{message}[/red]")
286
+ if verbose:
287
+ console.print(traceback.format_exc())
288
+ return False, message, 0.0, "", []
289
+
290
+ if not template:
291
+ message = "Prompt template 'agentic_update_LLM' could not be loaded or is empty."
292
+ if not quiet:
293
+ console.print(f"[red]{message}[/red]")
294
+ return False, message, 0.0, "", []
295
+
296
+ # Build a human-friendly representation of test paths for the template
297
+ if selected_tests:
298
+ test_paths_str = "\n".join(
299
+ f"- {_relativize(path, PROJECT_ROOT)}" for path in selected_tests
300
+ )
301
+ else:
302
+ test_paths_str = "No tests were found for this code file."
303
+
304
+ try:
305
+ instruction = template.format(
306
+ prompt_path=_relativize(prompt_path, PROJECT_ROOT),
307
+ code_path=_relativize(code_path, PROJECT_ROOT),
308
+ test_paths=test_paths_str,
309
+ )
310
+ except Exception as exc:
311
+ message = f"Error formatting 'agentic_update_LLM' template: {exc}"
312
+ if not quiet:
313
+ console.print(f"[red]{message}[/red]")
314
+ if verbose:
315
+ console.print(traceback.format_exc())
316
+ return False, message, 0.0, "", []
317
+
318
+ # Run the agentic task
319
+ try:
320
+ agent_success, output_message, cost, provider_used = run_agentic_task(
321
+ instruction=instruction,
322
+ cwd=PROJECT_ROOT,
323
+ verbose=bool(verbose and not quiet),
324
+ quiet=quiet,
325
+ label=f"agentic_update:{code_path.stem}",
326
+ )
327
+ except Exception as exc:
328
+ message = f"Agentic task failed with an exception: {exc}"
329
+ if not quiet:
330
+ console.print(f"[red]{message}[/red]")
331
+ if verbose:
332
+ console.print(traceback.format_exc())
333
+ return False, message, 0.0, "", []
334
+
335
+ # After running the agent, re-discover tests to include any newly created ones
336
+ after_tests = _discover_test_files(code_path)
337
+
338
+ after_paths: set[Path] = {prompt_path.resolve(), code_path.resolve()}
339
+ after_paths.update(p.resolve() for p in selected_tests)
340
+ after_paths.update(p.resolve() for p in after_tests)
341
+
342
+ after_mtimes = _snapshot_mtimes(after_paths)
343
+ changed_paths = _detect_changed_files(before_mtimes, after_mtimes)
344
+
345
+ prompt_modified = any(p.resolve() == prompt_path.resolve() for p in changed_paths)
346
+
347
+ # Final success criterion: did the prompt file change?
348
+ success = bool(prompt_modified)
349
+
350
+ # Build a user-facing message
351
+ if success:
352
+ base_msg = "Prompt file updated successfully."
353
+ else:
354
+ base_msg = "Agentic update did not modify the prompt file."
355
+
356
+ # Incorporate agent's own status for clarity
357
+ if not agent_success:
358
+ base_msg += " Underlying agent reported failure."
359
+ if output_message:
360
+ base_msg += f" Agent output: {output_message}"
361
+
362
+ if not quiet:
363
+ if success:
364
+ console.print(f"[green]{base_msg}[/green]")
365
+ else:
366
+ console.print(f"[yellow]{base_msg}[/yellow]")
367
+
368
+ changed_files_str = [str(p) for p in changed_paths]
369
+
370
+ return success, base_msg, float(cost), provider_used or "", changed_files_str