pdd-cli 0.0.45__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.
- pdd/__init__.py +4 -4
- pdd/agentic_common.py +863 -0
- pdd/agentic_crash.py +534 -0
- pdd/agentic_fix.py +1179 -0
- pdd/agentic_langtest.py +162 -0
- pdd/agentic_update.py +370 -0
- pdd/agentic_verify.py +183 -0
- pdd/auto_deps_main.py +15 -5
- pdd/auto_include.py +63 -5
- pdd/bug_main.py +3 -2
- pdd/bug_to_unit_test.py +2 -0
- pdd/change_main.py +11 -4
- pdd/cli.py +22 -1181
- pdd/cmd_test_main.py +73 -21
- pdd/code_generator.py +58 -18
- pdd/code_generator_main.py +672 -25
- pdd/commands/__init__.py +42 -0
- pdd/commands/analysis.py +248 -0
- pdd/commands/fix.py +140 -0
- pdd/commands/generate.py +257 -0
- pdd/commands/maintenance.py +174 -0
- pdd/commands/misc.py +79 -0
- pdd/commands/modify.py +230 -0
- pdd/commands/report.py +144 -0
- pdd/commands/templates.py +215 -0
- pdd/commands/utility.py +110 -0
- pdd/config_resolution.py +58 -0
- pdd/conflicts_main.py +8 -3
- pdd/construct_paths.py +258 -82
- pdd/context_generator.py +10 -2
- pdd/context_generator_main.py +113 -11
- pdd/continue_generation.py +47 -7
- pdd/core/__init__.py +0 -0
- pdd/core/cli.py +503 -0
- pdd/core/dump.py +554 -0
- pdd/core/errors.py +63 -0
- pdd/core/utils.py +90 -0
- pdd/crash_main.py +44 -11
- pdd/data/language_format.csv +71 -63
- pdd/data/llm_model.csv +20 -18
- pdd/detect_change_main.py +5 -4
- pdd/fix_code_loop.py +330 -76
- pdd/fix_error_loop.py +207 -61
- pdd/fix_errors_from_unit_tests.py +4 -3
- pdd/fix_main.py +75 -18
- pdd/fix_verification_errors.py +12 -100
- pdd/fix_verification_errors_loop.py +306 -272
- pdd/fix_verification_main.py +28 -9
- pdd/generate_output_paths.py +93 -10
- pdd/generate_test.py +16 -5
- pdd/get_jwt_token.py +9 -2
- pdd/get_run_command.py +73 -0
- pdd/get_test_command.py +68 -0
- pdd/git_update.py +70 -19
- pdd/incremental_code_generator.py +2 -2
- pdd/insert_includes.py +11 -3
- pdd/llm_invoke.py +1269 -103
- pdd/load_prompt_template.py +36 -10
- pdd/pdd_completion.fish +25 -2
- pdd/pdd_completion.sh +30 -4
- pdd/pdd_completion.zsh +79 -4
- pdd/postprocess.py +10 -3
- pdd/preprocess.py +228 -15
- pdd/preprocess_main.py +8 -5
- pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
- pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
- pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
- pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
- pdd/prompts/agentic_update_LLM.prompt +1071 -0
- pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
- pdd/prompts/auto_include_LLM.prompt +100 -905
- pdd/prompts/detect_change_LLM.prompt +122 -20
- pdd/prompts/example_generator_LLM.prompt +22 -1
- pdd/prompts/extract_code_LLM.prompt +5 -1
- pdd/prompts/extract_program_code_fix_LLM.prompt +7 -1
- pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
- pdd/prompts/extract_promptline_LLM.prompt +17 -11
- pdd/prompts/find_verification_errors_LLM.prompt +6 -0
- pdd/prompts/fix_code_module_errors_LLM.prompt +4 -2
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +8 -0
- pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
- pdd/prompts/generate_test_LLM.prompt +21 -6
- pdd/prompts/increase_tests_LLM.prompt +1 -5
- pdd/prompts/insert_includes_LLM.prompt +228 -108
- pdd/prompts/trace_LLM.prompt +25 -22
- pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
- pdd/prompts/update_prompt_LLM.prompt +22 -1
- pdd/pytest_output.py +127 -12
- pdd/render_mermaid.py +236 -0
- pdd/setup_tool.py +648 -0
- pdd/simple_math.py +2 -0
- pdd/split_main.py +3 -2
- pdd/summarize_directory.py +49 -6
- pdd/sync_determine_operation.py +543 -98
- pdd/sync_main.py +81 -31
- pdd/sync_orchestration.py +1334 -751
- pdd/sync_tui.py +848 -0
- pdd/template_registry.py +264 -0
- pdd/templates/architecture/architecture_json.prompt +242 -0
- pdd/templates/generic/generate_prompt.prompt +174 -0
- pdd/trace.py +168 -12
- pdd/trace_main.py +4 -3
- pdd/track_cost.py +151 -61
- pdd/unfinished_prompt.py +49 -3
- pdd/update_main.py +549 -67
- pdd/update_model_costs.py +2 -2
- pdd/update_prompt.py +19 -4
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/METADATA +19 -6
- pdd_cli-0.0.90.dist-info/RECORD +153 -0
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/licenses/LICENSE +1 -1
- pdd_cli-0.0.45.dist-info/RECORD +0 -116
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/top_level.txt +0 -0
pdd/agentic_langtest.py
ADDED
|
@@ -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
|