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.
Files changed (39) hide show
  1. pdd/__init__.py +1 -1
  2. pdd/agentic_bug_orchestrator.py +15 -6
  3. pdd/agentic_change_orchestrator.py +18 -7
  4. pdd/agentic_common.py +68 -40
  5. pdd/agentic_crash.py +2 -1
  6. pdd/agentic_e2e_fix_orchestrator.py +165 -9
  7. pdd/agentic_update.py +2 -1
  8. pdd/agentic_verify.py +3 -2
  9. pdd/auto_include.py +51 -0
  10. pdd/commands/analysis.py +32 -25
  11. pdd/commands/connect.py +69 -1
  12. pdd/commands/fix.py +31 -13
  13. pdd/commands/generate.py +5 -0
  14. pdd/commands/modify.py +47 -11
  15. pdd/commands/utility.py +12 -7
  16. pdd/core/cli.py +17 -4
  17. pdd/core/dump.py +68 -20
  18. pdd/fix_main.py +4 -2
  19. pdd/frontend/dist/assets/index-CUWd8al1.js +450 -0
  20. pdd/frontend/dist/index.html +1 -1
  21. pdd/llm_invoke.py +82 -12
  22. pdd/operation_log.py +342 -0
  23. pdd/postprocess.py +122 -100
  24. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +11 -2
  25. pdd/prompts/generate_test_LLM.prompt +0 -1
  26. pdd/prompts/generate_test_from_example_LLM.prompt +251 -0
  27. pdd/prompts/prompt_code_diff_LLM.prompt +29 -25
  28. pdd/server/routes/prompts.py +26 -1
  29. pdd/server/terminal_spawner.py +15 -7
  30. pdd/sync_orchestration.py +164 -147
  31. pdd/sync_order.py +304 -0
  32. pdd/update_main.py +48 -24
  33. {pdd_cli-0.0.118.dist-info → pdd_cli-0.0.121.dist-info}/METADATA +3 -3
  34. {pdd_cli-0.0.118.dist-info → pdd_cli-0.0.121.dist-info}/RECORD +37 -35
  35. pdd/frontend/dist/assets/index-DQ3wkeQ2.js +0 -449
  36. {pdd_cli-0.0.118.dist-info → pdd_cli-0.0.121.dist-info}/WHEEL +0 -0
  37. {pdd_cli-0.0.118.dist-info → pdd_cli-0.0.121.dist-info}/entry_points.txt +0 -0
  38. {pdd_cli-0.0.118.dist-info → pdd_cli-0.0.121.dist-info}/licenses/LICENSE +0 -0
  39. {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.obj.get("quiet", False))
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.obj.get("verbose", False),
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.obj.get("quiet", False))
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=ctx.obj.get("verbose", False),
183
- quiet=ctx.obj.get("quiet", False),
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.obj.get("quiet", False))
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.obj.get("quiet", False))
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.obj.get("quiet", False))
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: PROMPT_FILE CODE_FILE UNIT_TEST_FILE [UNIT_TEST_FILE...] ERROR_FILE
101
- if len(args) < 4:
102
- raise click.UsageError(
103
- "Manual mode requires at least 4 arguments: PROMPT_FILE CODE_FILE UNIT_TEST_FILE... ERROR_FILE"
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
- error_file = args[-1]
109
- # All arguments between code file and error file are treated as unit test files
110
- unit_test_files = args[2:-1]
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
- handle_error(e)
163
- return None
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 2 or 3 arguments: CHANGE_PROMPT INPUT_CODE [INPUT_PROMPT]"
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
- # Determine mode based on argument count
213
- is_repo_mode = len(files) == 0
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 file-specific arguments or flags like --git in repository-wide mode"
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
- # Single-file mode: --extensions and --directory are not allowed
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=None,
275
+ input_prompt_file=input_prompt_file,
240
276
  modified_code_file=modified_code_file,
241
- input_code_file=None,
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", ctx.obj.get("quiet", False))
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
- handle_error(exception, "verify", ctx.obj.get("quiet", False))
110
- return None
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
- is_flag=True,
268
- default=False,
269
- help="Write a JSON core dump for this run into .pdd/core_dumps (for bug reports).",
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: