pdd-cli 0.0.118__py3-none-any.whl → 0.0.121__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pdd/__init__.py +1 -1
- pdd/agentic_bug_orchestrator.py +15 -6
- pdd/agentic_change_orchestrator.py +18 -7
- pdd/agentic_common.py +68 -40
- pdd/agentic_crash.py +2 -1
- pdd/agentic_e2e_fix_orchestrator.py +165 -9
- pdd/agentic_update.py +2 -1
- pdd/agentic_verify.py +3 -2
- pdd/auto_include.py +51 -0
- pdd/commands/analysis.py +32 -25
- pdd/commands/connect.py +69 -1
- pdd/commands/fix.py +31 -13
- pdd/commands/generate.py +5 -0
- pdd/commands/modify.py +47 -11
- pdd/commands/utility.py +12 -7
- pdd/core/cli.py +17 -4
- pdd/core/dump.py +68 -20
- pdd/fix_main.py +4 -2
- pdd/frontend/dist/assets/index-CUWd8al1.js +450 -0
- pdd/frontend/dist/index.html +1 -1
- pdd/llm_invoke.py +82 -12
- pdd/operation_log.py +342 -0
- pdd/postprocess.py +122 -100
- pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +11 -2
- pdd/prompts/generate_test_LLM.prompt +0 -1
- pdd/prompts/generate_test_from_example_LLM.prompt +251 -0
- pdd/prompts/prompt_code_diff_LLM.prompt +29 -25
- pdd/server/routes/prompts.py +26 -1
- pdd/server/terminal_spawner.py +15 -7
- pdd/sync_orchestration.py +164 -147
- pdd/sync_order.py +304 -0
- pdd/update_main.py +48 -24
- {pdd_cli-0.0.118.dist-info → pdd_cli-0.0.121.dist-info}/METADATA +3 -3
- {pdd_cli-0.0.118.dist-info → pdd_cli-0.0.121.dist-info}/RECORD +37 -35
- pdd/frontend/dist/assets/index-DQ3wkeQ2.js +0 -449
- {pdd_cli-0.0.118.dist-info → pdd_cli-0.0.121.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.118.dist-info → pdd_cli-0.0.121.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.118.dist-info → pdd_cli-0.0.121.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.118.dist-info → pdd_cli-0.0.121.dist-info}/top_level.txt +0 -0
pdd/commands/analysis.py
CHANGED
|
@@ -5,7 +5,7 @@ Analysis commands (detect-change, conflicts, bug, crash, trace).
|
|
|
5
5
|
"""
|
|
6
6
|
import os
|
|
7
7
|
import click
|
|
8
|
-
from typing import Optional, Tuple, List
|
|
8
|
+
from typing import Optional, Tuple, List, Dict, Any
|
|
9
9
|
|
|
10
10
|
from ..detect_change_main import detect_change_main
|
|
11
11
|
from ..conflicts_main import conflicts_main
|
|
@@ -15,6 +15,11 @@ from ..crash_main import crash_main
|
|
|
15
15
|
from ..trace_main import trace_main
|
|
16
16
|
from ..track_cost import track_cost
|
|
17
17
|
from ..core.errors import handle_error
|
|
18
|
+
from ..operation_log import log_operation
|
|
19
|
+
|
|
20
|
+
def get_context_obj(ctx: click.Context) -> Dict[str, Any]:
|
|
21
|
+
"""Safely retrieve the context object, defaulting to empty dict if None."""
|
|
22
|
+
return ctx.obj or {}
|
|
18
23
|
|
|
19
24
|
@click.command("detect")
|
|
20
25
|
@click.argument("files", nargs=-1, type=click.Path(exists=True, dir_okay=False))
|
|
@@ -28,8 +33,8 @@ from ..core.errors import handle_error
|
|
|
28
33
|
@track_cost
|
|
29
34
|
def detect_change(
|
|
30
35
|
ctx: click.Context,
|
|
31
|
-
files: Tuple[str, ...],
|
|
32
|
-
output: Optional[str],
|
|
36
|
+
files: Tuple[str, ...] = (),
|
|
37
|
+
output: Optional[str] = None,
|
|
33
38
|
) -> Optional[Tuple[List, float, str]]:
|
|
34
39
|
"""Detect if prompts need to be changed based on a description.
|
|
35
40
|
|
|
@@ -53,7 +58,7 @@ def detect_change(
|
|
|
53
58
|
except (click.Abort, click.ClickException):
|
|
54
59
|
raise
|
|
55
60
|
except Exception as exception:
|
|
56
|
-
handle_error(exception, "detect", ctx.
|
|
61
|
+
handle_error(exception, "detect", get_context_obj(ctx).get("quiet", False))
|
|
57
62
|
return None
|
|
58
63
|
|
|
59
64
|
|
|
@@ -72,7 +77,7 @@ def conflicts(
|
|
|
72
77
|
ctx: click.Context,
|
|
73
78
|
prompt1: str,
|
|
74
79
|
prompt2: str,
|
|
75
|
-
output: Optional[str],
|
|
80
|
+
output: Optional[str] = None,
|
|
76
81
|
) -> Optional[Tuple[List, float, str]]:
|
|
77
82
|
"""Check for conflicts between two prompt files."""
|
|
78
83
|
try:
|
|
@@ -81,13 +86,13 @@ def conflicts(
|
|
|
81
86
|
prompt1=prompt1,
|
|
82
87
|
prompt2=prompt2,
|
|
83
88
|
output=output,
|
|
84
|
-
verbose=ctx.
|
|
89
|
+
verbose=get_context_obj(ctx).get("verbose", False),
|
|
85
90
|
)
|
|
86
91
|
return result, total_cost, model_name
|
|
87
92
|
except (click.Abort, click.ClickException):
|
|
88
93
|
raise
|
|
89
94
|
except Exception as exception:
|
|
90
|
-
handle_error(exception, "conflicts", ctx.
|
|
95
|
+
handle_error(exception, "conflicts", get_context_obj(ctx).get("quiet", False))
|
|
91
96
|
return None
|
|
92
97
|
|
|
93
98
|
|
|
@@ -127,12 +132,12 @@ def conflicts(
|
|
|
127
132
|
@track_cost
|
|
128
133
|
def bug(
|
|
129
134
|
ctx: click.Context,
|
|
130
|
-
manual: bool,
|
|
131
|
-
args: Tuple[str, ...],
|
|
132
|
-
output: Optional[str],
|
|
133
|
-
language: str,
|
|
134
|
-
timeout_adder: float,
|
|
135
|
-
no_github_state: bool,
|
|
135
|
+
manual: bool = False,
|
|
136
|
+
args: Tuple[str, ...] = (),
|
|
137
|
+
output: Optional[str] = None,
|
|
138
|
+
language: str = "Python",
|
|
139
|
+
timeout_adder: float = 0.0,
|
|
140
|
+
no_github_state: bool = False,
|
|
136
141
|
) -> Optional[Tuple[str, float, str]]:
|
|
137
142
|
"""Generate a unit test (manual) or investigate a bug (agentic).
|
|
138
143
|
|
|
@@ -143,6 +148,7 @@ def bug(
|
|
|
143
148
|
pdd bug --manual PROMPT_FILE CODE_FILE PROGRAM_FILE CURRENT_OUTPUT DESIRED_OUTPUT
|
|
144
149
|
"""
|
|
145
150
|
try:
|
|
151
|
+
obj = get_context_obj(ctx)
|
|
146
152
|
if manual:
|
|
147
153
|
if len(args) != 5:
|
|
148
154
|
raise click.UsageError(
|
|
@@ -179,8 +185,8 @@ def bug(
|
|
|
179
185
|
|
|
180
186
|
success, message, cost, model, changed_files = run_agentic_bug(
|
|
181
187
|
issue_url=issue_url,
|
|
182
|
-
verbose=
|
|
183
|
-
quiet=
|
|
188
|
+
verbose=obj.get("verbose", False),
|
|
189
|
+
quiet=obj.get("quiet", False),
|
|
184
190
|
timeout_adder=timeout_adder,
|
|
185
191
|
use_github_state=not no_github_state,
|
|
186
192
|
)
|
|
@@ -191,7 +197,7 @@ def bug(
|
|
|
191
197
|
except (click.Abort, click.ClickException):
|
|
192
198
|
raise
|
|
193
199
|
except Exception as exception:
|
|
194
|
-
handle_error(exception, "bug", ctx.
|
|
200
|
+
handle_error(exception, "bug", get_context_obj(ctx).get("quiet", False))
|
|
195
201
|
return None
|
|
196
202
|
|
|
197
203
|
|
|
@@ -231,6 +237,7 @@ def bug(
|
|
|
231
237
|
help="Maximum cost allowed for the fixing process (default: 5.0).",
|
|
232
238
|
)
|
|
233
239
|
@click.pass_context
|
|
240
|
+
@log_operation("crash", clears_run_report=True)
|
|
234
241
|
@track_cost
|
|
235
242
|
def crash(
|
|
236
243
|
ctx: click.Context,
|
|
@@ -238,11 +245,11 @@ def crash(
|
|
|
238
245
|
code_file: str,
|
|
239
246
|
program_file: str,
|
|
240
247
|
error_file: str,
|
|
241
|
-
output: Optional[str],
|
|
242
|
-
output_program: Optional[str],
|
|
243
|
-
loop: bool,
|
|
244
|
-
max_attempts: Optional[int],
|
|
245
|
-
budget: Optional[float],
|
|
248
|
+
output: Optional[str] = None,
|
|
249
|
+
output_program: Optional[str] = None,
|
|
250
|
+
loop: bool = False,
|
|
251
|
+
max_attempts: Optional[int] = None,
|
|
252
|
+
budget: Optional[float] = None,
|
|
246
253
|
) -> Optional[Tuple[str, float, str]]:
|
|
247
254
|
"""Analyze a crash and fix the code and program."""
|
|
248
255
|
try:
|
|
@@ -265,7 +272,7 @@ def crash(
|
|
|
265
272
|
except (click.Abort, click.ClickException):
|
|
266
273
|
raise
|
|
267
274
|
except Exception as exception:
|
|
268
|
-
handle_error(exception, "crash", ctx.
|
|
275
|
+
handle_error(exception, "crash", get_context_obj(ctx).get("quiet", False))
|
|
269
276
|
return None
|
|
270
277
|
|
|
271
278
|
|
|
@@ -286,7 +293,7 @@ def trace(
|
|
|
286
293
|
prompt_file: str,
|
|
287
294
|
code_file: str,
|
|
288
295
|
code_line: int,
|
|
289
|
-
output: Optional[str],
|
|
296
|
+
output: Optional[str] = None,
|
|
290
297
|
) -> Optional[Tuple[str, float, str]]:
|
|
291
298
|
"""Trace execution flow back to the prompt."""
|
|
292
299
|
try:
|
|
@@ -302,5 +309,5 @@ def trace(
|
|
|
302
309
|
except (click.Abort, click.ClickException):
|
|
303
310
|
raise
|
|
304
311
|
except Exception as exception:
|
|
305
|
-
handle_error(exception, "trace", ctx.
|
|
306
|
-
return None
|
|
312
|
+
handle_error(exception, "trace", get_context_obj(ctx).get("quiet", False))
|
|
313
|
+
return None
|
pdd/commands/connect.py
CHANGED
|
@@ -8,13 +8,44 @@ REST server to enable the web frontend to interact with PDD.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
|
+
import errno
|
|
11
12
|
import os
|
|
13
|
+
import socket
|
|
12
14
|
import webbrowser
|
|
13
15
|
from pathlib import Path
|
|
14
16
|
from typing import Optional
|
|
15
17
|
|
|
16
18
|
import click
|
|
17
19
|
|
|
20
|
+
|
|
21
|
+
# Default port and range for auto-assignment
|
|
22
|
+
DEFAULT_PORT = 9876
|
|
23
|
+
PORT_RANGE_START = 9876
|
|
24
|
+
PORT_RANGE_END = 9899 # Try up to 24 ports
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_port_available(port: int, host: str = "127.0.0.1") -> bool:
|
|
28
|
+
"""Check if a port is available for binding."""
|
|
29
|
+
try:
|
|
30
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
31
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
32
|
+
s.bind((host, port))
|
|
33
|
+
return True
|
|
34
|
+
except OSError as exc:
|
|
35
|
+
# If we lack permission to bind (common in sandboxed environments),
|
|
36
|
+
# treat availability as unknown and allow the caller to proceed.
|
|
37
|
+
if exc.errno in (errno.EACCES, errno.EPERM):
|
|
38
|
+
return True
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def find_available_port(start_port: int, end_port: int, host: str = "127.0.0.1") -> Optional[int]:
|
|
43
|
+
"""Find an available port in the given range."""
|
|
44
|
+
for port in range(start_port, end_port + 1):
|
|
45
|
+
if is_port_available(port, host):
|
|
46
|
+
return port
|
|
47
|
+
return None
|
|
48
|
+
|
|
18
49
|
# Handle optional dependencies - uvicorn may not be installed
|
|
19
50
|
try:
|
|
20
51
|
import uvicorn
|
|
@@ -131,6 +162,43 @@ def connect(
|
|
|
131
162
|
fg="yellow"
|
|
132
163
|
))
|
|
133
164
|
|
|
165
|
+
# 2.5 Smart Port Detection
|
|
166
|
+
# Check if user explicitly specified a port
|
|
167
|
+
port_source = ctx.get_parameter_source("port")
|
|
168
|
+
user_specified_port = port_source == click.core.ParameterSource.COMMANDLINE
|
|
169
|
+
|
|
170
|
+
# For port checking, use the effective bind host
|
|
171
|
+
check_host = "0.0.0.0" if host == "0.0.0.0" else "127.0.0.1"
|
|
172
|
+
|
|
173
|
+
if not is_port_available(port, check_host):
|
|
174
|
+
if user_specified_port:
|
|
175
|
+
# User explicitly requested this port, show error
|
|
176
|
+
click.echo(click.style(
|
|
177
|
+
f"Error: Port {port} is already in use.",
|
|
178
|
+
fg="red", bold=True
|
|
179
|
+
))
|
|
180
|
+
click.echo("Please specify a different port with --port or stop the process using this port.")
|
|
181
|
+
ctx.exit(1)
|
|
182
|
+
else:
|
|
183
|
+
# Auto-detect an available port
|
|
184
|
+
click.echo(click.style(
|
|
185
|
+
f"Port {port} is in use, looking for an available port...",
|
|
186
|
+
fg="yellow"
|
|
187
|
+
))
|
|
188
|
+
available_port = find_available_port(PORT_RANGE_START, PORT_RANGE_END, check_host)
|
|
189
|
+
if available_port is None:
|
|
190
|
+
click.echo(click.style(
|
|
191
|
+
f"Error: No available ports found in range {PORT_RANGE_START}-{PORT_RANGE_END}.",
|
|
192
|
+
fg="red", bold=True
|
|
193
|
+
))
|
|
194
|
+
click.echo("Please specify a port manually with --port or free up a port in this range.")
|
|
195
|
+
ctx.exit(1)
|
|
196
|
+
port = available_port
|
|
197
|
+
click.echo(click.style(
|
|
198
|
+
f"Using port {port} instead.",
|
|
199
|
+
fg="green"
|
|
200
|
+
))
|
|
201
|
+
|
|
134
202
|
# 3. Determine URLs
|
|
135
203
|
# The server URL is where the API lives
|
|
136
204
|
server_url = f"http://{host}:{port}"
|
|
@@ -287,4 +355,4 @@ def connect(
|
|
|
287
355
|
except Exception as e:
|
|
288
356
|
click.echo(click.style(f"Warning: Error during session cleanup: {e}", fg="yellow"))
|
|
289
357
|
|
|
290
|
-
click.echo(click.style("Goodbye!", fg="blue"))
|
|
358
|
+
click.echo(click.style("Goodbye!", fg="blue"))
|
pdd/commands/fix.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
import sys
|
|
4
5
|
import click
|
|
5
6
|
from typing import Optional, Tuple, Any
|
|
@@ -9,6 +10,7 @@ from rich.console import Console
|
|
|
9
10
|
from ..fix_main import fix_main
|
|
10
11
|
from ..agentic_e2e_fix import run_agentic_e2e_fix
|
|
11
12
|
from ..track_cost import track_cost
|
|
13
|
+
from ..operation_log import log_operation
|
|
12
14
|
from ..core.errors import handle_error
|
|
13
15
|
|
|
14
16
|
console = Console()
|
|
@@ -31,6 +33,7 @@ console = Console()
|
|
|
31
33
|
@click.option("--auto-submit", is_flag=True, help="Automatically submit example if tests pass.")
|
|
32
34
|
@click.option("--agentic-fallback/--no-agentic-fallback", default=True, help="Enable agentic fallback in loop mode.")
|
|
33
35
|
@click.pass_context
|
|
36
|
+
@log_operation(operation="fix", clears_run_report=True)
|
|
34
37
|
@track_cost
|
|
35
38
|
def fix(
|
|
36
39
|
ctx: click.Context,
|
|
@@ -72,8 +75,8 @@ def fix(
|
|
|
72
75
|
console.print("[yellow]Warning: Extra arguments ignored in Agentic E2E Fix mode.[/yellow]")
|
|
73
76
|
|
|
74
77
|
issue_url = args[0]
|
|
75
|
-
verbose = ctx.obj.get("verbose", False)
|
|
76
|
-
quiet = ctx.obj.get("quiet", False)
|
|
78
|
+
verbose = ctx.obj.get("verbose", False) if ctx.obj else False
|
|
79
|
+
quiet = ctx.obj.get("quiet", False) if ctx.obj else False
|
|
77
80
|
|
|
78
81
|
# Call the agentic fix workflow
|
|
79
82
|
success, message, cost, model, _ = run_agentic_e2e_fix(
|
|
@@ -97,17 +100,31 @@ def fix(
|
|
|
97
100
|
# --- Manual Mode ---
|
|
98
101
|
else:
|
|
99
102
|
# Validate arguments for manual mode
|
|
100
|
-
# Expected structure:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
# Expected structure:
|
|
104
|
+
# - Loop mode: PROMPT_FILE CODE_FILE UNIT_TEST_FILE [UNIT_TEST_FILE...]
|
|
105
|
+
# - Non-loop mode: PROMPT_FILE CODE_FILE UNIT_TEST_FILE [UNIT_TEST_FILE...] ERROR_FILE
|
|
106
|
+
min_args = 3 if loop else 4
|
|
107
|
+
if len(args) < min_args:
|
|
108
|
+
if loop:
|
|
109
|
+
raise click.UsageError(
|
|
110
|
+
"Loop mode requires at least 3 arguments: PROMPT_FILE CODE_FILE UNIT_TEST_FILE..."
|
|
111
|
+
)
|
|
112
|
+
else:
|
|
113
|
+
raise click.UsageError(
|
|
114
|
+
"Non-loop mode requires at least 4 arguments: PROMPT_FILE CODE_FILE UNIT_TEST_FILE... ERROR_FILE"
|
|
115
|
+
)
|
|
105
116
|
|
|
106
117
|
prompt_file = args[0]
|
|
107
118
|
code_file = args[1]
|
|
108
|
-
|
|
109
|
-
#
|
|
110
|
-
|
|
119
|
+
|
|
120
|
+
# In loop mode, error_file is optional (generated during loop)
|
|
121
|
+
# In non-loop mode, last argument is the error_file
|
|
122
|
+
if loop:
|
|
123
|
+
error_file = None
|
|
124
|
+
unit_test_files = args[2:] # All remaining args are test files
|
|
125
|
+
else:
|
|
126
|
+
error_file = args[-1]
|
|
127
|
+
unit_test_files = args[2:-1] # All args between code file and error file
|
|
111
128
|
|
|
112
129
|
total_cost = 0.0
|
|
113
130
|
last_model = "unknown"
|
|
@@ -156,8 +173,9 @@ def fix(
|
|
|
156
173
|
else:
|
|
157
174
|
return f"Some files failed to fix.\n{summary_str}", total_cost, last_model
|
|
158
175
|
|
|
159
|
-
except click.Abort:
|
|
176
|
+
except (click.Abort, click.UsageError, click.BadArgumentUsage, click.FileError, click.BadParameter):
|
|
160
177
|
raise
|
|
161
178
|
except Exception as e:
|
|
162
|
-
|
|
163
|
-
|
|
179
|
+
quiet = ctx.obj.get("quiet", False) if ctx.obj else False
|
|
180
|
+
handle_error(e, "fix", quiet)
|
|
181
|
+
sys.exit(1)
|
pdd/commands/generate.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Generate, test, and example commands.
|
|
3
3
|
"""
|
|
4
|
+
from __future__ import annotations
|
|
4
5
|
import click
|
|
5
6
|
from typing import Dict, Optional, Tuple, List
|
|
6
7
|
|
|
@@ -9,6 +10,7 @@ from ..context_generator_main import context_generator_main
|
|
|
9
10
|
from ..cmd_test_main import cmd_test_main
|
|
10
11
|
from ..track_cost import track_cost
|
|
11
12
|
from ..core.errors import handle_error, console
|
|
13
|
+
from ..operation_log import log_operation
|
|
12
14
|
|
|
13
15
|
class GenerateCommand(click.Command):
|
|
14
16
|
"""Ensure help shows PROMPT_FILE as required even when validated at runtime."""
|
|
@@ -69,6 +71,7 @@ class GenerateCommand(click.Command):
|
|
|
69
71
|
help="Do not automatically include test files found in the default tests directory.",
|
|
70
72
|
)
|
|
71
73
|
@click.pass_context
|
|
74
|
+
@log_operation("generate", clears_run_report=True, updates_fingerprint=True)
|
|
72
75
|
@track_cost
|
|
73
76
|
def generate(
|
|
74
77
|
ctx: click.Context,
|
|
@@ -159,6 +162,7 @@ def generate(
|
|
|
159
162
|
help="Specify where to save the generated example code (file or directory).",
|
|
160
163
|
)
|
|
161
164
|
@click.pass_context
|
|
165
|
+
@log_operation("example", updates_fingerprint=True)
|
|
162
166
|
@track_cost
|
|
163
167
|
def example(
|
|
164
168
|
ctx: click.Context,
|
|
@@ -222,6 +226,7 @@ def example(
|
|
|
222
226
|
help="Merge new tests with existing test file instead of creating a separate file.",
|
|
223
227
|
)
|
|
224
228
|
@click.pass_context
|
|
229
|
+
@log_operation("test", updates_run_report=True)
|
|
225
230
|
@track_cost
|
|
226
231
|
def test(
|
|
227
232
|
ctx: click.Context,
|
pdd/commands/modify.py
CHANGED
|
@@ -14,6 +14,7 @@ from ..agentic_change import run_agentic_change
|
|
|
14
14
|
from ..update_main import update_main
|
|
15
15
|
from ..track_cost import track_cost
|
|
16
16
|
from ..core.errors import handle_error
|
|
17
|
+
from ..operation_log import log_operation
|
|
17
18
|
|
|
18
19
|
console = Console()
|
|
19
20
|
|
|
@@ -123,7 +124,7 @@ def change(
|
|
|
123
124
|
raise click.UsageError("INPUT_PROMPT_FILE is required when not using --csv")
|
|
124
125
|
else:
|
|
125
126
|
raise click.UsageError(
|
|
126
|
-
"Manual mode requires
|
|
127
|
+
"Manual mode requires 3 arguments: CHANGE_PROMPT INPUT_CODE INPUT_PROMPT"
|
|
127
128
|
)
|
|
128
129
|
|
|
129
130
|
# Validate file existence
|
|
@@ -191,6 +192,7 @@ def change(
|
|
|
191
192
|
@click.option("--output", help="Output path for the updated prompt.")
|
|
192
193
|
@click.option("--simple", is_flag=True, default=False, help="Use legacy simple update.")
|
|
193
194
|
@click.pass_context
|
|
195
|
+
@log_operation(operation="update", clears_run_report=True)
|
|
194
196
|
@track_cost
|
|
195
197
|
def update(
|
|
196
198
|
ctx: click.Context,
|
|
@@ -209,18 +211,55 @@ def update(
|
|
|
209
211
|
"""
|
|
210
212
|
ctx.ensure_object(dict)
|
|
211
213
|
try:
|
|
212
|
-
#
|
|
213
|
-
|
|
214
|
+
# Handle argument counts per modify_python.prompt spec (aligned with README)
|
|
215
|
+
if len(files) == 0:
|
|
216
|
+
# Repo-wide mode
|
|
217
|
+
is_repo_mode = True
|
|
218
|
+
input_prompt_file = None
|
|
219
|
+
modified_code_file = None
|
|
220
|
+
input_code_file = None
|
|
221
|
+
elif len(files) == 1:
|
|
222
|
+
# Regeneration mode: just the code file
|
|
223
|
+
is_repo_mode = False
|
|
224
|
+
input_prompt_file = None
|
|
225
|
+
modified_code_file = files[0]
|
|
226
|
+
input_code_file = None
|
|
227
|
+
elif len(files) == 2:
|
|
228
|
+
# Git-based update: prompt + modified_code (requires --git)
|
|
229
|
+
if not git:
|
|
230
|
+
raise click.UsageError(
|
|
231
|
+
"Two arguments require --git flag: pdd update --git <prompt> <modified_code>"
|
|
232
|
+
)
|
|
233
|
+
is_repo_mode = False
|
|
234
|
+
input_prompt_file = files[0]
|
|
235
|
+
modified_code_file = files[1]
|
|
236
|
+
input_code_file = None
|
|
237
|
+
elif len(files) == 3:
|
|
238
|
+
# Manual update: prompt + modified_code + original_code
|
|
239
|
+
if git:
|
|
240
|
+
raise click.UsageError(
|
|
241
|
+
"Cannot use --git with 3 arguments (--git and original_code are mutually exclusive)"
|
|
242
|
+
)
|
|
243
|
+
is_repo_mode = False
|
|
244
|
+
input_prompt_file = files[0]
|
|
245
|
+
modified_code_file = files[1]
|
|
246
|
+
input_code_file = files[2]
|
|
247
|
+
else:
|
|
248
|
+
raise click.UsageError("Too many arguments. Max 3: <prompt> <modified_code> <original_code>")
|
|
214
249
|
|
|
215
250
|
# Validate mode-specific options
|
|
216
251
|
if is_repo_mode:
|
|
217
252
|
# Repo-wide mode: --git and --output are not allowed
|
|
218
253
|
if git:
|
|
219
254
|
raise click.UsageError(
|
|
220
|
-
"Cannot use
|
|
255
|
+
"Cannot use --git in repository-wide mode"
|
|
256
|
+
)
|
|
257
|
+
if output:
|
|
258
|
+
raise click.UsageError(
|
|
259
|
+
"Cannot use --output in repository-wide mode"
|
|
221
260
|
)
|
|
222
261
|
else:
|
|
223
|
-
#
|
|
262
|
+
# File modes: --extensions and --directory are not allowed
|
|
224
263
|
if extensions:
|
|
225
264
|
raise click.UsageError(
|
|
226
265
|
"--extensions can only be used in repository-wide mode"
|
|
@@ -230,15 +269,12 @@ def update(
|
|
|
230
269
|
"--directory can only be used in repository-wide mode"
|
|
231
270
|
)
|
|
232
271
|
|
|
233
|
-
# In single-file mode, the one arg is the modified code file
|
|
234
|
-
modified_code_file = files[0] if len(files) > 0 else None
|
|
235
|
-
|
|
236
272
|
# Call update_main with correct parameters
|
|
237
273
|
result, cost, model = update_main(
|
|
238
274
|
ctx=ctx,
|
|
239
|
-
input_prompt_file=
|
|
275
|
+
input_prompt_file=input_prompt_file,
|
|
240
276
|
modified_code_file=modified_code_file,
|
|
241
|
-
input_code_file=
|
|
277
|
+
input_code_file=input_code_file,
|
|
242
278
|
output=output,
|
|
243
279
|
use_git=git,
|
|
244
280
|
repo=is_repo_mode,
|
|
@@ -253,4 +289,4 @@ def update(
|
|
|
253
289
|
raise
|
|
254
290
|
except Exception as e:
|
|
255
291
|
handle_error(e, "update", ctx.obj.get("quiet", False))
|
|
256
|
-
return None
|
|
292
|
+
return None
|
pdd/commands/utility.py
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Utility commands (install_completion, verify/fix-verification).
|
|
3
3
|
"""
|
|
4
|
+
from __future__ import annotations
|
|
4
5
|
import click
|
|
5
|
-
from typing import Optional, Tuple
|
|
6
|
+
from typing import Optional, Tuple, Dict, Any
|
|
6
7
|
|
|
7
8
|
from ..fix_verification_main import fix_verification_main
|
|
8
9
|
from ..track_cost import track_cost
|
|
9
10
|
from ..core.errors import handle_error
|
|
11
|
+
from ..operation_log import log_operation
|
|
10
12
|
|
|
11
13
|
@click.command("install_completion")
|
|
12
14
|
@click.pass_context
|
|
13
|
-
def install_completion_cmd(ctx: click.Context):
|
|
15
|
+
def install_completion_cmd(ctx: click.Context) -> None:
|
|
14
16
|
"""Install shell completion for the PDD CLI."""
|
|
17
|
+
# Safely retrieve quiet flag, defaulting to False if ctx.obj is None
|
|
18
|
+
quiet = (ctx.obj or {}).get("quiet", False)
|
|
15
19
|
try:
|
|
16
20
|
from .. import cli as cli_module # Import parent module for proper patching
|
|
17
|
-
quiet = ctx.obj.get("quiet", False)
|
|
18
21
|
# Call through cli_module so patches to pdd.cli.install_completion work
|
|
19
22
|
cli_module.install_completion(quiet=quiet)
|
|
20
23
|
except Exception as e:
|
|
21
|
-
handle_error(e, "install_completion",
|
|
24
|
+
handle_error(e, "install_completion", quiet)
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
@click.command("verify")
|
|
@@ -64,6 +67,7 @@ def install_completion_cmd(ctx: click.Context):
|
|
|
64
67
|
help="Enable agentic fallback if the primary fix mechanism fails.",
|
|
65
68
|
)
|
|
66
69
|
@click.pass_context
|
|
70
|
+
@log_operation(operation="verify", clears_run_report=True, updates_run_report=True)
|
|
67
71
|
@track_cost
|
|
68
72
|
def verify(
|
|
69
73
|
ctx: click.Context,
|
|
@@ -76,7 +80,7 @@ def verify(
|
|
|
76
80
|
max_attempts: int,
|
|
77
81
|
budget: float,
|
|
78
82
|
agentic_fallback: bool,
|
|
79
|
-
) -> Optional[Tuple]:
|
|
83
|
+
) -> Optional[Tuple[Dict[str, Any], float, str]]:
|
|
80
84
|
"""Verify code using a verification program."""
|
|
81
85
|
try:
|
|
82
86
|
# verify command implies a loop if max_attempts > 1, but let's enable loop by default
|
|
@@ -106,5 +110,6 @@ def verify(
|
|
|
106
110
|
except click.Abort:
|
|
107
111
|
raise
|
|
108
112
|
except Exception as exception:
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
quiet = (ctx.obj or {}).get("quiet", False)
|
|
114
|
+
handle_error(exception, "verify", quiet)
|
|
115
|
+
return None
|
pdd/core/cli.py
CHANGED
|
@@ -262,11 +262,17 @@ class PDDCLI(click.Group):
|
|
|
262
262
|
help="List available contexts from .pddrc and exit.",
|
|
263
263
|
)
|
|
264
264
|
@click.option(
|
|
265
|
-
"--core-dump",
|
|
265
|
+
"--core-dump/--no-core-dump",
|
|
266
266
|
"core_dump",
|
|
267
|
-
|
|
268
|
-
default
|
|
269
|
-
|
|
267
|
+
default=True,
|
|
268
|
+
help="Write a JSON core dump for this run into .pdd/core_dumps (default: on). Use --no-core-dump to disable.",
|
|
269
|
+
)
|
|
270
|
+
@click.option(
|
|
271
|
+
"--keep-core-dumps",
|
|
272
|
+
"keep_core_dumps",
|
|
273
|
+
type=click.IntRange(min=0),
|
|
274
|
+
default=10,
|
|
275
|
+
help="Number of core dumps to keep (default: 10, min: 0). Older dumps are garbage collected after each dump write.",
|
|
270
276
|
)
|
|
271
277
|
@click.version_option(version=__version__, package_name="pdd-cli")
|
|
272
278
|
@click.pass_context
|
|
@@ -284,6 +290,7 @@ def cli(
|
|
|
284
290
|
context_override: Optional[str],
|
|
285
291
|
list_contexts: bool,
|
|
286
292
|
core_dump: bool,
|
|
293
|
+
keep_core_dumps: int,
|
|
287
294
|
):
|
|
288
295
|
"""
|
|
289
296
|
Main entry point for the PDD CLI. Handles global options and initializes context.
|
|
@@ -317,6 +324,12 @@ def cli(
|
|
|
317
324
|
# Persist context override for downstream calls
|
|
318
325
|
ctx.obj["context"] = context_override
|
|
319
326
|
ctx.obj["core_dump"] = core_dump
|
|
327
|
+
ctx.obj["keep_core_dumps"] = keep_core_dumps
|
|
328
|
+
|
|
329
|
+
# Garbage collect old core dumps on every CLI invocation (Issue #231)
|
|
330
|
+
# This runs regardless of --no-core-dump to ensure cleanup always happens
|
|
331
|
+
from .dump import garbage_collect_core_dumps
|
|
332
|
+
garbage_collect_core_dumps(keep=keep_core_dumps)
|
|
320
333
|
|
|
321
334
|
# Set up terminal output capture if core_dump is enabled
|
|
322
335
|
if core_dump:
|